mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 20:15:13 +00:00
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:
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 },
|
||||
|
||||
Reference in new issue
Block a user