package com.craigvg.lichun_android.ui.components.feedback import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.craigvg.lichun_android.domain.models.StatDelta import com.craigvg.lichun_android.ui.theme.AppColors import com.craigvg.lichun_android.ui.theme.AppSpacing import com.craigvg.lichun_android.ui.theme.AppTypography import com.craigvg.lichun_android.viewmodel.GameStateViewModel import kotlinx.coroutines.delay /** * Tier 3 polish: transient floating "+5 ⚡" / "-3 💰" chips that rise and fade * near the top of the screen when a lightweight "u" update changes a tracked * resource. Makes gains and losses feel earned. * * Android parity with iOS FloatingDeltaView / FloatingDeltaOverlay (T014). The * overlay observes [GameStateViewModel.statDeltas] and prunes each delta after it * finishes animating. */ @Composable fun FloatingDeltaOverlay( gameStateViewModel: GameStateViewModel = hiltViewModel(), modifier: Modifier = Modifier ) { val deltas by gameStateViewModel.statDeltas.collectAsStateWithLifecycle() Box(modifier = modifier.fillMaxSize()) { Row( modifier = Modifier .align(Alignment.TopEnd) .padding(top = AppSpacing.xs, end = AppSpacing.md), horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs) ) { deltas.forEach { delta -> key(delta.id) { FloatingDeltaChip( delta = delta, onFinished = { gameStateViewModel.pruneStatDelta(delta.id) } ) } } } } } @Composable private fun FloatingDeltaChip(delta: StatDelta, onFinished: () -> Unit) { val rise = remember { Animatable(0f) } val opacity = remember { Animatable(0f) } val scale = remember { Animatable(0.7f) } LaunchedEffect(delta.id) { // Pop in. opacity.animateTo(1f, tween(180)) scale.animateTo(1f, tween(220)) // Rise + linger. rise.animateTo(-36f, tween(1400)) delay(200) opacity.animateTo(0f, tween(400)) onFinished() } Row( modifier = Modifier .graphicsLayer { translationY = rise.value } .alpha(opacity.value) .scale(scale.value) .clip(RoundedCornerShape(AppSpacing.pillCornerRadius)) .background(AppColors.surfaceElevated.copy(alpha = 0.92f)) .border(1.dp, delta.kind.tint.copy(alpha = 0.35f), RoundedCornerShape(AppSpacing.pillCornerRadius)) .padding(horizontal = AppSpacing.sm, vertical = AppSpacing.xxs), verticalAlignment = Alignment.CenterVertically ) { Text(delta.kind.emoji, style = AppTypography.caption) Spacer(Modifier.width(4.dp)) Text( delta.label, style = AppTypography.captionBold, color = if (delta.amount > 0) delta.kind.tint else AppColors.error ) } }