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:
@@ -18,6 +18,7 @@
|
||||
#import "board.sx";
|
||||
#import "board_layout.sx";
|
||||
#import "board_anim.sx";
|
||||
#import "board_fx.sx";
|
||||
#import "swipe.sx";
|
||||
|
||||
// Fraction of a cell each gem occupies; the remainder is margin so a gem sits
|
||||
@@ -171,6 +172,8 @@ BoardView :: struct {
|
||||
sel: *BoardSelection;
|
||||
drag: *DragInput;
|
||||
anim: *BoardAnim;
|
||||
fx: *BoardFx;
|
||||
fxassets: *BoardFxAssets;
|
||||
safe: EdgeInsets;
|
||||
|
||||
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
|
||||
@@ -319,6 +322,59 @@ BoardView :: struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Transient match FX (P6.2): coloured glow bursts at the cleared cells,
|
||||
// clipped to the grid so a burst's glow never bleeds over the HUD. Each
|
||||
// burst grows then shrinks to nothing; the soft texture carries the fade.
|
||||
render_fx_particles :: (self: *BoardView, ctx: *RenderContext) {
|
||||
if self.fx == null or self.fxassets == null or !self.fxassets.loaded { return; }
|
||||
if self.fx.particles.len == 0 { return; }
|
||||
cs := self.layout.cell_size;
|
||||
grid := Frame.make(
|
||||
self.layout.origin.x, self.layout.origin.y,
|
||||
cs * cast(f32) BOARD_COLS, cs * cast(f32) BOARD_ROWS
|
||||
);
|
||||
ctx.push_clip(grid);
|
||||
for 0..self.fx.particles.len: (i) {
|
||||
p := self.fx.particles.items[i];
|
||||
lt := (p.age - p.delay) / p.life;
|
||||
env := fx_pop_env(lt);
|
||||
if env > 0.0 {
|
||||
size := env * p.peak * cs;
|
||||
cx := self.layout.origin.x + p.col * cs;
|
||||
cy := self.layout.origin.y + p.row * cs;
|
||||
gf := Frame.make(cx - size * 0.5, cy - size * 0.5, size, size);
|
||||
ctx.add_image(gf, self.fxassets.tex[p.tint]);
|
||||
}
|
||||
}
|
||||
ctx.pop_clip();
|
||||
}
|
||||
|
||||
// Floating "+points" popups: rise and fade above the initial clear. Drawn
|
||||
// unclipped (over everything) so the number stays legible as it lifts off
|
||||
// the grid. The text path honours the colour's alpha, so these truly fade.
|
||||
render_fx_popups :: (self: *BoardView, ctx: *RenderContext) {
|
||||
if self.fx == null or self.fx.popups.len == 0 { return; }
|
||||
cs := self.layout.cell_size;
|
||||
for 0..self.fx.popups.len: (i) {
|
||||
q := self.fx.popups.items[i];
|
||||
lt := (q.age - q.delay) / q.life;
|
||||
if lt >= 0.0 {
|
||||
fade := fx_popup_fade(lt);
|
||||
font := if q.combo then FX_POPUP_COMBO_FONT else FX_POPUP_FONT;
|
||||
base := if q.combo then FX_POPUP_COMBO_COLOR else FX_POPUP_COLOR;
|
||||
col := Color.{ r = base.r, g = base.g, b = base.b, a = cast(u8) (fade * 255.0) };
|
||||
txt := format("+{}", q.points);
|
||||
sz := measure_text(txt, font);
|
||||
cx := self.layout.origin.x + q.col * cs;
|
||||
cy := self.layout.origin.y + (q.row - lt * FX_POPUP_RISE) * cs;
|
||||
ctx.add_text(
|
||||
Frame.make(cx - sz.width * 0.5, cy - sz.height * 0.5, sz.width, sz.height),
|
||||
txt, font, col
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall segment: every gem of the round's settled board slides from its source
|
||||
// row (above the board for refills) down to its destination cell.
|
||||
render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, inset: f32, dim: f32, t: f32) {
|
||||
@@ -387,6 +443,12 @@ impl View for BoardView {
|
||||
// 4. HUD card with score + remaining moves, in the band above the grid.
|
||||
avail := frame.inset(self.safe);
|
||||
render_hud(ctx, self.board, avail);
|
||||
|
||||
// 5. Transient match FX over the board: coloured bursts at the cleared
|
||||
// cells, then the floating "+points" popup on top. Purely visual and
|
||||
// self-pruning, so they vanish once the move settles.
|
||||
self.render_fx_particles(ctx);
|
||||
self.render_fx_popups(ctx);
|
||||
}
|
||||
|
||||
// Touch input. A press records the drag start; the release resolves the
|
||||
@@ -416,6 +478,7 @@ impl View for BoardView {
|
||||
if intent := swipe_intent(@self.layout, start, d.position) {
|
||||
mv := plan_and_commit(self.board, intent.a, intent.b);
|
||||
if self.anim != null { self.anim.begin(mv); }
|
||||
if self.fx != null { self.fx.begin(@mv); }
|
||||
self.sel.clear();
|
||||
} else {
|
||||
if hit := self.layout.point_to_cell(start) {
|
||||
|
||||
Reference in New Issue
Block a user