From ec0a8567dd48f8cfd62c613bf0f5522a4f0b2476 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 29 Apr 2026 16:24:05 +0700 Subject: [PATCH] feat: confetti celebration tier rule and emoji set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fire confetti on 2nd bingo OR 1st bingo when another row is in Chờ (waiting for one number). Replaces old ≥3 threshold that rarely fired. - Add Vietnamese hội-chợ emoji: 🥢 🎋 🏮 (chopsticks, bamboo, lantern) - Per-particle size jitter (1.5–2.4x) for visual interest Upstream: a60ea08 --- .../miti99/loto/state/PlayerBoardViewModel.kt | 35 ++++++++++++++++++- .../java/com/miti99/loto/ui/board/Confetti.kt | 11 ++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/miti99/loto/state/PlayerBoardViewModel.kt b/android/app/src/main/java/com/miti99/loto/state/PlayerBoardViewModel.kt index 86cb812..d287840 100644 --- a/android/app/src/main/java/com/miti99/loto/state/PlayerBoardViewModel.kt +++ b/android/app/src/main/java/com/miti99/loto/state/PlayerBoardViewModel.kt @@ -55,6 +55,7 @@ class PlayerBoardViewModel( grid = grid, crossed = crossed, rowComplete = List(grid.size) { false }, + sectionHasWaiting = listOf(false, false, false), ) persistDebounced() } @@ -72,6 +73,7 @@ class PlayerBoardViewModel( notifiedWaitingRows = emptySet(), bingoEvent = null, waitingToast = null, + sectionHasWaiting = listOf(false, false, false), ) } persistDebounced() @@ -108,6 +110,7 @@ class PlayerBoardViewModel( rowComplete = rowComplete, celebratedRows = celebrated, notifiedWaitingRows = notifiedWaiting, + sectionHasWaiting = computeSectionHasWaiting(gridArr, crossedArr, rowComplete), ) } @@ -163,7 +166,18 @@ class PlayerBoardViewModel( if (i !in celebrated && rowComplete[i]) { celebrated = celebrated + i notifiedWaiting = notifiedWaiting + i - val tier = if (celebrated.size >= 3) 2 else 1 + // Tier 2 confetti: 2nd bingo, OR 1st bingo while another row + // is one cell from completion. Old "≥3 bingos" rule rarely + // fired on a 9-row card so most wins felt under-celebrated. + val hasActiveCho = grid.indices.any { otherRow -> + otherRow !in celebrated && + GameLogic.getWaitingNumber(gridArr, crossedArr, otherRow) != null + } + val tier = when { + celebrated.size >= 2 -> 2 + celebrated.size >= 1 && hasActiveCho -> 2 + else -> 1 + } bingoEvent = BingoEvent(row1Based = i + 1, tier = tier) if (announce) voicePlayer.playBingo() return@run @@ -193,6 +207,7 @@ class PlayerBoardViewModel( bingoEvent = bingoEvent, waitingToast = waitingToast, lastConsumedEventId = lastConsumedEventId ?: it.lastConsumedEventId, + sectionHasWaiting = computeSectionHasWaiting(gridArr, crossedArr, rowComplete), ) } persistDebounced() @@ -241,4 +256,22 @@ class PlayerBoardViewModel( private fun toBoolArray(crossed: List>): Array = Array(crossed.size) { r -> BooleanArray(crossed[r].size) { c -> crossed[r][c] } } + + /** + * Per-section waiting flag (rows 0-2, 3-5, 6-8). True when any row in + * the section has exactly 1 uncrossed number AND is not yet complete. + */ + private fun computeSectionHasWaiting( + gridArr: Array, + crossedArr: Array, + rowComplete: List, + ): List { + val waitingRows = gridArr.indices.map { r -> + !rowComplete.getOrElse(r) { false } && + GameLogic.getWaitingNumber(gridArr, crossedArr, r) != null + } + return listOf(0, 3, 6).map { start -> + (start until start + 3).any { r -> waitingRows.getOrElse(r) { false } } + } + } } diff --git a/android/app/src/main/java/com/miti99/loto/ui/board/Confetti.kt b/android/app/src/main/java/com/miti99/loto/ui/board/Confetti.kt index 78009bf..bd78199 100644 --- a/android/app/src/main/java/com/miti99/loto/ui/board/Confetti.kt +++ b/android/app/src/main/java/com/miti99/loto/ui/board/Confetti.kt @@ -18,7 +18,9 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -private val CONFETTI_EMOJI = listOf("🎊", "✨", "🎉", "🥳") +// Hội-chợ flavour added (lantern, bamboo, chopsticks) so the celebration +// set reads as Vietnamese fair, not generic party. +private val CONFETTI_EMOJI = listOf("🎊", "✨", "🎉", "🥳", "🥢", "🎋", "🏮") private const val PARTICLE_COUNT = 12 private const val DURATION_NS = 3_000_000_000L // 3 seconds @@ -28,6 +30,7 @@ private data class Particle( val speed: Float, // px per second (screen height fraction) val rotationSpeed: Float, // degrees per second val delayNs: Long, // stagger start + val fontSizeSp: Float, // per-piece size jitter ) /** @@ -49,12 +52,16 @@ fun Confetti( val particles = remember { List(PARTICLE_COUNT) { i -> + // Match upstream jitter range 1.5..2.4rem; render as sp. + // 1rem ≈ 16sp on default density. + val sizeRem = 1.5f + ((i * 13) % 11) / 10f Particle( emoji = CONFETTI_EMOJI[i % CONFETTI_EMOJI.size], startXFraction = (i.toFloat() / PARTICLE_COUNT) + (((i * 17) % 10) / 100f), speed = screenHeightPx * (0.25f + (i % 5) * 0.05f), rotationSpeed = 60f + (i % 7) * 30f, delayNs = (i * 150_000_000L), // 150 ms stagger + fontSizeSp = (sizeRem * 16f).coerceIn(24f, 38f), ) } } @@ -86,7 +93,7 @@ fun Confetti( Text( text = p.emoji, - fontSize = 20.sp, + fontSize = p.fontSizeSp.sp, modifier = Modifier .offset(x = xDp.dp, y = yDp.dp) .graphicsLayer { rotationZ = rotation % 360f },