P11.2: escalating combo emphasis tied to cascade depth (sx / iOS)
Scale the combo FX with cascade depth (mv.rounds.len) — the same depth the cascade SFX (play_cascade) steps up on — so deeper cascades read as more exciting and land in lockstep with the audio escalation. Purely visual and self-pruning: no board / score / move state changes, and input stays gated by BoardAnim.active alone. - board_fx.sx: add fx_combo_level (mirrors audio's cascade_cue_index clamp: depth<=1 -> floor, depth>=5 -> ceiling). The +points popup now carries the cascade depth and grows one font step + lerps gold -> hot-gold per level (fx_popup_font / fx_popup_color). Every burst of a deep cascade gets a whole-move depth boost (FX_BURST_DEPTH) on top of the existing per-round bump. - board_view.sx: render_fx_popups derives styling from depth and tops a combo with a "COMBO xN" label naming the true cascade depth. - tests/fx_combo.sx: headless snapshot locking the depth->level/font table and asserting fx_combo_level matches the cascade-cue index column entry-for-entry. - goldens/p11_combo_deep.png + README: deterministic depth-5 capture (M3TE_FX=11) vs the depth-1 single clear (M3TE_FX=3); FX gone after settle at a later phase.
This commit is contained in:
76
board_fx.sx
76
board_fx.sx
@@ -22,18 +22,27 @@
|
||||
#import "board_anim.sx";
|
||||
|
||||
// Burst timing/size. A burst fires when its round's gems start clearing and
|
||||
// lingers a touch into the fall; combos (cascade rounds past the first) burst
|
||||
// bigger. Sizes are in CELL units (1.0 == one grid cell).
|
||||
// lingers a touch into the fall, growing in two ways so deeper cascades read as
|
||||
// more exciting: a per-move DEPTH boost lifts every burst of a deep cascade from
|
||||
// its first round (`FX_BURST_DEPTH` per combo level, escalating in lockstep with
|
||||
// the cascade SFX), and a per-round bump grows the later rounds within a move
|
||||
// (`FX_BURST_COMBO`, capped). Sizes are in CELL units (1.0 == one grid cell).
|
||||
FX_BURST_LIFE :f32: 0.70;
|
||||
FX_BURST_BASE :f32: 2.50;
|
||||
FX_BURST_COMBO :f32: 0.72; // extra peak size per cascade depth (capped)
|
||||
FX_BURST_COMBO :f32: 0.72; // extra peak size per round index within a move (capped)
|
||||
FX_BURST_DEPTH :f32: 0.45; // extra peak size per cascade combo level (whole move)
|
||||
|
||||
// Popup timing/motion. Rises ~1.2 cells over its life and fades out; a combo
|
||||
// (depth > 1) popup is larger and gold.
|
||||
// Popup timing/motion. Rises ~1.2 cells over its life and fades out. A combo
|
||||
// (depth > 1) popup is gold and grows one step per combo level, topped by a
|
||||
// `COMBO xN` label naming the cascade depth; both escalate in lockstep with the
|
||||
// cascade SFX cue (see `fx_combo_level`).
|
||||
FX_POPUP_LIFE :f32: 1.40;
|
||||
FX_POPUP_RISE :f32: 1.2;
|
||||
FX_POPUP_FONT :f32: 34.0;
|
||||
FX_POPUP_COMBO_FONT :f32: 48.0;
|
||||
FX_POPUP_COMBO_STEP :f32: 8.0; // extra popup font per combo level past the base
|
||||
FX_COMBO_LABEL_RATIO :f32: 0.55; // `COMBO xN` label font as a fraction of the +points font
|
||||
FX_COMBO_LABEL_GAP :f32: 0.12; // gap (cell units) between the label and +points
|
||||
|
||||
// Vivid candy tints so a soft glow reads brightly over the dark board, in gem
|
||||
// order (red, orange, yellow, green, blue, purple). Saturated a touch past the
|
||||
@@ -49,7 +58,45 @@ fx_tint :: (i: s64) -> Color {
|
||||
}
|
||||
|
||||
FX_POPUP_COLOR :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||
FX_POPUP_COMBO_COLOR :: Color.{ r = 255, g = 222, b = 130, a = 255 };
|
||||
FX_POPUP_COMBO_COLOR :: Color.{ r = 255, g = 222, b = 130, a = 255 }; // base gold (depth 2)
|
||||
FX_POPUP_COMBO_HOT :: Color.{ r = 255, g = 248, b = 214, a = 255 }; // hot near-white gold (deepest)
|
||||
|
||||
// Cascade depth (`mv.rounds.len`) -> combo emphasis level. Mirrors audio.sx's
|
||||
// `cascade_cue_index` clamp EXACTLY (depth <= 1 -> 0, depth >= 5 -> the max), so
|
||||
// the on-screen combo emphasis (popup size/colour + burst boost) steps up in
|
||||
// lockstep with the cascade SFX cue. Pure arithmetic, OS-agnostic, and the
|
||||
// equivalence to `cascade_cue_index` is locked headlessly (tests/fx_combo.sx).
|
||||
FX_COMBO_MAX_LEVEL :: 4; // == audio.sx COMBO_CLIPS - 1
|
||||
fx_combo_level :: (depth: s64) -> s64 {
|
||||
if depth <= 1 { return 0; }
|
||||
if depth >= FX_COMBO_MAX_LEVEL + 1 { return FX_COMBO_MAX_LEVEL; }
|
||||
depth - 1
|
||||
}
|
||||
|
||||
// Popup font size for a cascade `depth` rounds deep: a single clear (depth <= 1)
|
||||
// uses the plain size; a combo starts at the base combo size and grows one step
|
||||
// per combo level past the first, clamped at the deepest level.
|
||||
fx_popup_font :: (depth: s64) -> f32 {
|
||||
if depth <= 1 { return FX_POPUP_FONT; }
|
||||
FX_POPUP_COMBO_FONT + FX_POPUP_COMBO_STEP * cast(f32) (fx_combo_level(depth) - 1)
|
||||
}
|
||||
|
||||
// Popup colour for a cascade `depth` rounds deep: white for a single clear, else
|
||||
// the gold lerped toward a hot near-white as the cascade deepens.
|
||||
fx_popup_color :: (depth: s64) -> Color {
|
||||
if depth <= 1 { return FX_POPUP_COLOR; }
|
||||
t := cast(f32) (fx_combo_level(depth) - 1) / cast(f32) (FX_COMBO_MAX_LEVEL - 1);
|
||||
Color.{
|
||||
r = fx_lerp_u8(FX_POPUP_COMBO_COLOR.r, FX_POPUP_COMBO_HOT.r, t),
|
||||
g = fx_lerp_u8(FX_POPUP_COMBO_COLOR.g, FX_POPUP_COMBO_HOT.g, t),
|
||||
b = fx_lerp_u8(FX_POPUP_COMBO_COLOR.b, FX_POPUP_COMBO_HOT.b, t),
|
||||
a = 255,
|
||||
}
|
||||
}
|
||||
|
||||
fx_lerp_u8 :: (lo: u8, hi: u8, t: f32) -> u8 {
|
||||
cast(u8) (cast(f32) lo + (cast(f32) hi - cast(f32) lo) * t)
|
||||
}
|
||||
|
||||
// Upload an RGBA buffer as a texture, returning its handle. Mirrors
|
||||
// board_view.load_texture's upload half but takes an in-memory buffer (the
|
||||
@@ -129,17 +176,19 @@ FxParticle :: struct {
|
||||
}
|
||||
|
||||
// A floating "+points" popup anchored at the initial clear's centroid, rising
|
||||
// and fading over its life. `combo` selects the larger gold styling. Stores the
|
||||
// raw points (not a formatted string): the label is built at render time in the
|
||||
// frame's arena, so nothing allocated here has to outlive the spawning event.
|
||||
// and fading over its life. `depth` is the cascade depth (`mv.rounds.len`): it
|
||||
// drives the combo styling at render time (gold size step + `COMBO xN` label for
|
||||
// depth > 1). Stores the raw points (not a formatted string): the label is built
|
||||
// at render time in the frame's arena, so nothing allocated here has to outlive
|
||||
// the spawning event.
|
||||
FxPopup :: struct {
|
||||
col: f32;
|
||||
row: f32;
|
||||
points: s64;
|
||||
depth: s64;
|
||||
delay: f32;
|
||||
age: f32;
|
||||
life: f32;
|
||||
combo: bool;
|
||||
}
|
||||
|
||||
// Live FX state for the in-flight move. Heap-allocated (like BoardAnim) so it
|
||||
@@ -167,10 +216,13 @@ BoardFx :: struct {
|
||||
self.clear();
|
||||
if !mv.legal or mv.rounds.len == 0 { return; }
|
||||
|
||||
// Whole-move depth boost: a deeper cascade makes every burst bigger from
|
||||
// its first round, escalating in lockstep with the cascade SFX cue.
|
||||
depth_boost := FX_BURST_DEPTH * cast(f32) fx_combo_level(mv.rounds.len);
|
||||
for 0..mv.rounds.len: (k) {
|
||||
rd := @mv.rounds.items[k];
|
||||
t0 := SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR);
|
||||
extra := FX_BURST_COMBO * cast(f32) min(k, 2);
|
||||
extra := depth_boost + FX_BURST_COMBO * cast(f32) min(k, 2);
|
||||
for 0..BOARD_CELLS: (idx) {
|
||||
if rd.matched.cells[idx] {
|
||||
g := rd.before[idx];
|
||||
@@ -206,10 +258,10 @@ BoardFx :: struct {
|
||||
col = cast(f32) sc / cast(f32) cnt + 0.5,
|
||||
row = cast(f32) sr / cast(f32) cnt + 0.5,
|
||||
points = mv.awarded,
|
||||
depth = mv.rounds.len,
|
||||
delay = SWAP_ANIM_DUR,
|
||||
age = 0.0,
|
||||
life = FX_POPUP_LIFE,
|
||||
combo = mv.rounds.len > 1,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user