feat: confetti celebration tier rule and emoji set

- 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
This commit is contained in:
tiennm99 committed 2026-04-29 16:24:05 +07:00
1 parent 435a57a02b
commit ec0a8567dd
2 files changed
+43 -3

No files matched your search

@@ -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<List<Boolean>>): Array<BooleanArray> =
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<IntArray>,
crossedArr: Array<BooleanArray>,
rowComplete: List<Boolean>,
): List<Boolean> {
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 } }
}
}
}
@@ -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 },