Этап 7: бонус-еда с таймером
Иногда при поедании обычной еды появляется золотая бонус-еда на ограниченное время и даёт больше очков. - bonusFood/bonusStepsLeft; шанс выката 1 из 4 при обычном поедании - бонус живёт BONUS_LIFETIME_STEPS шагов (таймер в шагах -> учитывает паузу) - съеденный бонус даёт BONUS_POINTS очков, звук выше тоном, вспышка - отрисовка золотой клетки + дуга-таймер (drawArc) в drawBoard() - randomFreeCell учитывает и еду, и бонус, чтобы не наложить их Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c3529f08a0
commit
50e6677989
@ -92,4 +92,10 @@ app/src/main/
|
||||
паузу (свайпы по-прежнему рулят направлением). Смотри флаг `paused`,
|
||||
условие в игровом цикле и оверлей «ПАУЗА».
|
||||
|
||||
Идеи для следующих этапов: выбор размера поля, экран настроек, светлая тема.
|
||||
- **`snake-stage-7`** — бонус-еда с таймером: иногда (1 из 4 при поедании
|
||||
обычной еды) появляется золотая еда на ~36 шагов и даёт +3 очка; вокруг
|
||||
неё рисуется дуга-таймер, по истечении она исчезает. Время считается в
|
||||
шагах игры, поэтому пауза учитывается автоматически. Смотри `bonusFood` /
|
||||
`bonusStepsLeft`, логику в `step()` и отрисовку (`drawArc`) в `drawBoard()`.
|
||||
|
||||
Идеи для следующих этапов: препятствия и уровни, режим «сквозь стены», два игрока.
|
||||
|
||||
@ -58,6 +58,11 @@ private fun stepDelay(score: Int): Long =
|
||||
private fun speedLevel(score: Int): Int =
|
||||
1 + minOf(score, ((START_STEP_MS - MIN_STEP_MS) / SPEEDUP_MS).toInt())
|
||||
|
||||
// --- Бонус-еда (этап 7) ---
|
||||
private const val BONUS_LIFETIME_STEPS = 36 // сколько шагов живёт бонус (~5 c на старте)
|
||||
private const val BONUS_POINTS = 3 // бонус стоит как 3 обычные еды
|
||||
private const val BONUS_CHANCE = 4 // шанс выката бонуса при поедании обычной еды: 1 из N
|
||||
|
||||
/** Клетка поля / сегмент змейки. Direction тоже храним как Cell (dx, dy). */
|
||||
private data class Cell(val x: Int, val y: Int)
|
||||
|
||||
@ -68,6 +73,7 @@ private val GRID = Color(0xFF171C28)
|
||||
private val BODY = Color(0xFF4CD964)
|
||||
private val HEAD = Color(0xFF2ECC71)
|
||||
private val FOOD = Color(0xFFFF453A)
|
||||
private val BONUS = Color(0xFFFFD60A) // золотая бонус-еда
|
||||
|
||||
@Composable
|
||||
fun SnakeGame() {
|
||||
@ -83,6 +89,8 @@ fun SnakeGame() {
|
||||
var started by remember { mutableStateOf(false) }
|
||||
var gameOver by remember { mutableStateOf(false) }
|
||||
var paused by remember { mutableStateOf(false) } // этап 6: пауза
|
||||
var bonusFood by remember { mutableStateOf<Cell?>(null) } // этап 7: бонус-еда (null = нет)
|
||||
var bonusStepsLeft by remember { mutableIntStateOf(0) } // сколько шагов ей осталось жить
|
||||
|
||||
// Этап 3: контекст и scope для работы с DataStore (рекорд хранится на диске)
|
||||
val context = LocalContext.current
|
||||
@ -124,6 +132,8 @@ fun SnakeGame() {
|
||||
score = 0
|
||||
gameOver = false
|
||||
paused = false
|
||||
bonusFood = null
|
||||
bonusStepsLeft = 0
|
||||
}
|
||||
|
||||
// Один шаг змейки
|
||||
@ -138,25 +148,45 @@ fun SnakeGame() {
|
||||
return
|
||||
}
|
||||
|
||||
val grew = nh == food
|
||||
// голова добавляется в начало; хвост убираем, если не съели еду
|
||||
val ateFood = nh == food
|
||||
val ateBonus = bonusFood == nh
|
||||
val grew = ateFood || ateBonus
|
||||
// голова добавляется в начало; хвост убираем, если ничего не съели
|
||||
snake = if (grew) listOf(nh) + snake else listOf(nh) + snake.dropLast(1)
|
||||
|
||||
if (grew) {
|
||||
score++
|
||||
// звук поедания (left=1f, right=1f, prio=1, без зацикливания, обычная скорость)
|
||||
soundPool.play(eatSoundId, 1f, 1f, 1, 0, 1f)
|
||||
// запускаем анимацию вспышки на месте съеденной еды
|
||||
// звук (для бонуса — выше тоном) и анимация вспышки на месте укуса
|
||||
soundPool.play(eatSoundId, 1f, 1f, 1, 0, if (ateBonus) 1.5f else 1f)
|
||||
popCell = nh
|
||||
scope.launch {
|
||||
popProgress.snapTo(0f)
|
||||
popProgress.animateTo(1f, animationSpec = tween(durationMillis = 260))
|
||||
}
|
||||
if (score > best) {
|
||||
best = score
|
||||
// новый рекорд — сохраняем на диск (асинхронно, UI не блокируется)
|
||||
scope.launch { ScoreStore.saveBest(context, best) }
|
||||
}
|
||||
if (ateFood) {
|
||||
score++
|
||||
food = randomFreeCell(snake + listOfNotNull(bonusFood))
|
||||
// иногда выкатываем бонус (если его сейчас нет на поле)
|
||||
if (bonusFood == null && Random.nextInt(BONUS_CHANCE) == 0) {
|
||||
bonusFood = randomFreeCell(snake + listOf(food))
|
||||
bonusStepsLeft = BONUS_LIFETIME_STEPS
|
||||
}
|
||||
food = randomFreeCell(snake)
|
||||
}
|
||||
if (ateBonus) {
|
||||
score += BONUS_POINTS
|
||||
bonusFood = null
|
||||
bonusStepsLeft = 0
|
||||
}
|
||||
if (grew && score > best) {
|
||||
best = score
|
||||
// новый рекорд — сохраняем на диск (асинхронно, UI не блокируется)
|
||||
scope.launch { ScoreStore.saveBest(context, best) }
|
||||
}
|
||||
|
||||
// таймер бонуса: считаем в шагах, поэтому пауза учитывается автоматически
|
||||
if (bonusFood != null) {
|
||||
bonusStepsLeft--
|
||||
if (bonusStepsLeft <= 0) bonusFood = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,8 +243,9 @@ fun SnakeGame() {
|
||||
}
|
||||
) {
|
||||
// Игровое поле
|
||||
val bonusFrac = if (bonusStepsLeft > 0) bonusStepsLeft.toFloat() / BONUS_LIFETIME_STEPS else 0f
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawBoard(snake, food, popCell, popProgress.value)
|
||||
drawBoard(snake, food, bonusFood, bonusFrac, popCell, popProgress.value)
|
||||
}
|
||||
|
||||
// Счёт сверху (+ индикатор скорости — видно ускорение)
|
||||
@ -251,7 +282,14 @@ private fun Overlay(title: String, subtitle: String, modifier: Modifier = Modifi
|
||||
}
|
||||
|
||||
/** Вся отрисовка поля внутри DrawScope. */
|
||||
private fun DrawScope.drawBoard(snake: List<Cell>, food: Cell, popCell: Cell, popProgress: Float) {
|
||||
private fun DrawScope.drawBoard(
|
||||
snake: List<Cell>,
|
||||
food: Cell,
|
||||
bonusFood: Cell?,
|
||||
bonusFrac: Float,
|
||||
popCell: Cell,
|
||||
popProgress: Float
|
||||
) {
|
||||
val cell = minOf(size.width / COLS, size.height / ROWS)
|
||||
val offX = (size.width - cell * COLS) / 2f
|
||||
val offY = (size.height - cell * ROWS) / 2f
|
||||
@ -272,6 +310,21 @@ private fun DrawScope.drawBoard(snake: List<Cell>, food: Cell, popCell: Cell, po
|
||||
// еда
|
||||
drawCell(food, FOOD, cell, offX, offY)
|
||||
|
||||
// бонус-еда + дуга-таймер вокруг неё (тает по часовой)
|
||||
if (bonusFood != null) {
|
||||
drawCell(bonusFood, BONUS, cell, offX, offY)
|
||||
val pad = cell * 0.06f
|
||||
drawArc(
|
||||
color = BONUS,
|
||||
startAngle = -90f,
|
||||
sweepAngle = 360f * bonusFrac,
|
||||
useCenter = false,
|
||||
topLeft = Offset(offX + bonusFood.x * cell - pad, offY + bonusFood.y * cell - pad),
|
||||
size = Size(cell + 2 * pad, cell + 2 * pad),
|
||||
style = Stroke(width = cell * 0.12f)
|
||||
)
|
||||
}
|
||||
|
||||
// змейка (голова — другим цветом)
|
||||
snake.forEachIndexed { idx, c ->
|
||||
drawCell(c, if (idx == 0) HEAD else BODY, cell, offX, offY)
|
||||
@ -300,10 +353,10 @@ private fun DrawScope.drawCell(c: Cell, color: Color, cell: Float, offX: Float,
|
||||
)
|
||||
}
|
||||
|
||||
/** Случайная свободная клетка (не занятая змейкой) — для появления еды. */
|
||||
private fun randomFreeCell(snake: List<Cell>): Cell {
|
||||
/** Случайная свободная клетка (не занятая переданными клетками) — для появления еды/бонуса. */
|
||||
private fun randomFreeCell(occupied: List<Cell>): Cell {
|
||||
while (true) {
|
||||
val p = Cell(Random.nextInt(COLS), Random.nextInt(ROWS))
|
||||
if (!snake.contains(p)) return p
|
||||
if (!occupied.contains(p)) return p
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user