explodorama / files / src / render / animate.ts
animate.ts
| 1 | import type { step } from '../sim/turn' |
| 2 | import type { Action, GameState, Vec } from '../sim/types' |
| 3 | import { FLAME_SECONDS, type Flame } from './board' |
| 4 | import { clearEffects, newEffects, playerTransition, pruneEffects, startEffects, type Effects, type PlayerMove, type PlayerPose } from './effects' |
| 5 | |
| 6 | /** |
| 7 | * Everything that animates a board between turns: what the last turns changed (effects.ts), the |
| 8 | * flame of a blast, the player's move and pose and which way they face, and each enemy's latest |
| 9 | * step. One for the game (and the demo, which plays on its board), one for each dialog's clip |
| 10 | * (ui/clip.ts), so a clip can play beside the game (PLAN.md §13c). |
| 11 | */ |
| 12 | export interface Animation { |
| 13 | fx: Effects |
| 14 | flames: Flame[] |
| 15 | playerMove: PlayerMove | null |
| 16 | playerPose: PlayerPose |
| 17 | /** The way the player last stepped. The character sprite faces right; this mirrors it. */ |
| 18 | facingLeft: boolean |
| 19 | /** Each enemy's latest step, by id: where from, and when (LittleJS `time`). */ |
| 20 | enemySteps: Map<number, { from: Vec; at: number }> |
| 21 | } |
| 22 | |
| 23 | export function newAnimation(): Animation { |
| 24 | return { fx: newEffects(), flames: [], playerMove: null, playerPose: { kind: 'none', at: 0 }, facingLeft: false, enemySteps: new Map() } |
| 25 | } |
| 26 | |
| 27 | /** A fresh board: nothing under way. */ |
| 28 | export function clearAnimation(a: Animation): void { |
| 29 | clearEffects(a.fx) |
| 30 | a.flames = [] |
| 31 | a.playerMove = null |
| 32 | a.playerPose = { kind: 'none', at: 0 } |
| 33 | a.enemySteps.clear() |
| 34 | } |
| 35 | |
| 36 | /** Each frame: what has finished playing, dropped. */ |
| 37 | export function pruneAnimation(a: Animation, now: number): void { |
| 38 | a.flames = a.flames.filter((f) => now - f.born < FLAME_SECONDS) |
| 39 | pruneEffects(a.fx, now) |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * A turn played, from `prev` to `result.state`: everything that animates it starts at `now`. |
| 44 | * True if a level started or restarted (the board is new: nothing walks over from the old one). |
| 45 | */ |
| 46 | export function animateTurn(a: Animation, prev: GameState, result: ReturnType<typeof step>, action: Action, now: number): boolean { |
| 47 | const next = result.state |
| 48 | startEffects(a.fx, prev, next, result.events, now) |
| 49 | // The player's move comes from their change of tile, the pose from what happened; the pose |
| 50 | // waits for the move, so nothing can skip it (effects.ts `playerTransition`). |
| 51 | const player = playerTransition(prev, next, result.events, action.id === 'teleport', now) |
| 52 | if (player.move) a.playerMove = player.move |
| 53 | // Hit by a blast that shows later (an ICBM's, when it lands): the hit pose waits for it too. |
| 54 | if (player.pose.kind === 'hit') player.pose.at = Math.max(player.pose.at, a.fx.blastAt) |
| 55 | if (player.move || player.pose.kind !== 'none') a.playerPose = player.pose |
| 56 | if (player.move && player.move.from.x !== next.player.x) a.facingLeft = next.player.x < player.move.from.x |
| 57 | const newLevel = result.events.some((e) => e.type === 'levelStarted' || e.type === 'levelRestarted') |
| 58 | if (newLevel) { |
| 59 | a.playerMove = null |
| 60 | a.playerPose = player.pose |
| 61 | a.enemySteps.clear() |
| 62 | } |
| 63 | // Flame shows when the blast does (an ICBM's, when it lands). |
| 64 | // Flame spread over oil or through bushes shows with it. |
| 65 | for (const e of result.events) if (e.type === 'exploded' || e.type === 'fireSpread') a.flames.push({ tiles: e.tiles, born: Math.max(now, a.fx.blastAt) }) |
| 66 | // Every enemy that changed tile steps from where it was: from the state, like the player. |
| 67 | if (!newLevel) { |
| 68 | for (const enemy of next.enemies) { |
| 69 | const was = prev.enemies.find((o) => o.id === enemy.id) |
| 70 | if (was && (was.pos.x !== enemy.pos.x || was.pos.y !== enemy.pos.y)) a.enemySteps.set(enemy.id, { from: was.pos, at: now }) |
| 71 | } |
| 72 | } |
| 73 | return newLevel |
| 74 | } |