P6.2: score popups & match FX (sx, iOS sim)

Add a purely-visual, transient juice layer over a committed move — score
popups + a tinted particle/flash burst at the clears — with no change to the
model, score, moves, or settled board.

- assets/fx/particle.png: key the painted transparency checkerboard out of the
  provided particle art to real alpha (8-connected border flood fill +
  smooth luminance falloff that preserves the soft glow), downscaled to a
  256x256 RGBA white sparkle. tools/key_particle.sx is the reproducible tool.
- board_fx.sx: BoardFx (live particle bursts + one "+points" popup) and
  BoardFxAssets. The engine image path samples texture*white (no draw-time
  tint), so the white sprite is tinted per gem colour at LOAD time into one
  texture per colour; a burst animates by scale (grow -> shrink) and the soft
  texture edges carry the fade. Combos (cascade depth > 1) burst bigger and the
  popup is larger + gold. All driven by delta_time and self-pruning.
- board_anim.sx: AnimMove carries the model's cascade.awarded so the popup
  shows the real payout without re-deriving any scoring in the view.
- board_view.sx / main.sx: wire BoardFx + the tinted assets, tick each frame,
  spawn on a legal commit, and render bursts (clipped to the grid) under the
  popups (drawn on top). Input-lock (BoardAnim.active) is untouched; FX never
  gate input and may outlast the move slightly before vanishing.

Goldens (iPhone-17-class sim, iOS 26): p6_fx.png (combo: gold "+480" + bursts
mid-cascade), p6_fx_match.png (single match: "+30" + red burst), p6_fx_after.png
(settled board, FX fully gone). Gate: ios-sim build links, 15/15 logic tests
green (scoring/cascade goldens unchanged).
This commit is contained in:
swipelab
2026-06-05 02:18:55 +03:00
parent 907de09372
commit c2548aa854
9 changed files with 581 additions and 1 deletions

View File

@@ -46,6 +46,8 @@ AnimRound :: struct {
// a legal swap has >=1 round and `final` is the settled board; an illegal swap
// has zero rounds, `pre == final`, and the view plays a slide-and-return. `a`/`b`
// are the swapped cells; `pre` is the board before the swap (the slide's start).
// `awarded` carries the model's own payout for this move (cascade.awarded) so the
// score-popup FX (P6.2) shows the real number without re-deriving any scoring.
AnimMove :: struct {
legal: bool;
a: Cell;
@@ -53,6 +55,7 @@ AnimMove :: struct {
pre: [BOARD_CELLS]Gem;
rounds: List(AnimRound);
final: [BOARD_CELLS]Gem;
awarded: s64;
}
// Commit the player's swap authoritatively AND record its visual timeline. The
@@ -65,6 +68,7 @@ plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
move.b = b;
move.rounds = List(AnimRound).{};
move.pre = board.cells;
move.awarded = 0;
// Snapshot the entire model state (cells + RNG + score + moves) before the
// commit so the replay below is bit-identical to what commit_swap does.
@@ -72,6 +76,7 @@ plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
mv := commit_swap(board, a, b);
move.legal = mv.legal;
move.awarded = mv.cascade.awarded;
if !mv.legal {
move.final = board.cells;
return move;