P11.1: juicier match pops & brighter bursts (sx / iOS)
Visual-juice vibe-pass, FX-only — no logic/state changes, input gating still owned by BoardAnim.active. - board_fx.sx: bigger, punchier match bursts — peak size 1.95->2.50 cells, combo bonus 0.55->0.72, and the per-gem fx tints saturated a touch (low channel trimmed, dominant/mid lifted) so every burst pops as a brighter, more vivid candy colour. The hot per-pixel tint loop's hoisted locals are preserved (issue 0001). - gem_anim.sx: snappier clear pop — faster rise (0.30->0.18 of the window) to a bigger overshoot (CLEAR_POP_A 0.22->0.34) so the matched-gem clear reads as a candy snap. gem_pose's clear-pop invariants still hold. - main.sx: M3TE_FX=<n> deterministic match-FX capture hook, mirroring the M3TE_SELECT pattern. Commits the n-th currently-legal swap at startup via the normal plan_and_commit path and begins the move timeline + burst/popup FX; M3TE_ANIM_TIME pins the phase and the frame loop holds the move/FX frozen while pinned, so the burst + "+points" screenshot identically every run. A larger M3TE_ANIM_TIME captures the settled, FX-gone board. Startup- only and guarded, so normal play is untouched. - README.md: document the new M3TE_FX pin alongside the other capture hooks. - goldens/p6_fx_match.png: updated deterministic golden (iOS 26 sim, SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22) — the vertical red 3-match, burst region +1.4% mean luminance / 3.2:1 brighter:dimmer vs the same scene on the pre-juice constants. Gate: ios-sim build links, 19/19 logic tests green (incl. gem_pose t=0 rest).
This commit is contained in:
29
README.md
29
README.md
@@ -115,3 +115,32 @@ env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_RESTART=1 SIMCTL_CHILD_M3TE_ANI
|
|||||||
While a banner is up the board freezes (only the restart button is live, per
|
While a banner is up the board freezes (only the restart button is live, per
|
||||||
P7.1's finished-level rule); `tests/banner_layout.sx` locks the restart button's
|
P7.1's finished-level rule); `tests/banner_layout.sx` locks the restart button's
|
||||||
rect ↔ hit-test round-trip headlessly.
|
rect ↔ hit-test round-trip headlessly.
|
||||||
|
|
||||||
|
### Match-FX capture (P11.1)
|
||||||
|
|
||||||
|
The match bursts + score popup (`board_fx.sx`) only spawn off a *committed* move,
|
||||||
|
which the simulator can't script (there is no public touch injection). One more
|
||||||
|
env hook forces a representative match at startup so the FX can be screenshot
|
||||||
|
deterministically — combine it with `M3TE_ANIM_TIME` to freeze the phase:
|
||||||
|
|
||||||
|
- `M3TE_FX=<n>` commits the **n-th currently-legal swap** (1-based, clamped; `=1`
|
||||||
|
is the first) through the normal `plan_and_commit` path, then begins the move
|
||||||
|
timeline + its burst/popup FX. While `M3TE_ANIM_TIME` is set the move/FX
|
||||||
|
timelines are pinned at that phase (the frame loop holds them frozen), so the
|
||||||
|
burst and floating `+points` render identically every run. A larger
|
||||||
|
`M3TE_ANIM_TIME` lands past the timeline, capturing the settled board with the
|
||||||
|
FX fully pruned. Startup-only and guarded by the var, so normal play is
|
||||||
|
untouched.
|
||||||
|
|
||||||
|
The legal-swap order is the fixed enumeration in `tests/expected/swap_legality.stdout`
|
||||||
|
(row-major, right-before-down). For seed 1337, `M3TE_FX=3` is the vertical red
|
||||||
|
3-match used by the golden.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Punchy match burst + "+30" popup, pinned mid-clear: goldens/p6_fx_match.png
|
||||||
|
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22 \
|
||||||
|
xcrun simctl launch booted co.swipelab.m3te
|
||||||
|
# Same match, later phase — FX fully gone over the settled board (no golden):
|
||||||
|
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=2.0 \
|
||||||
|
xcrun simctl launch booted co.swipelab.m3te
|
||||||
|
```
|
||||||
|
|||||||
22
board_fx.sx
22
board_fx.sx
@@ -25,8 +25,8 @@
|
|||||||
// lingers a touch into the fall; combos (cascade rounds past the first) burst
|
// lingers a touch into the fall; combos (cascade rounds past the first) burst
|
||||||
// bigger. Sizes are in CELL units (1.0 == one grid cell).
|
// bigger. Sizes are in CELL units (1.0 == one grid cell).
|
||||||
FX_BURST_LIFE :f32: 0.70;
|
FX_BURST_LIFE :f32: 0.70;
|
||||||
FX_BURST_BASE :f32: 1.95;
|
FX_BURST_BASE :f32: 2.50;
|
||||||
FX_BURST_COMBO :f32: 0.55; // extra peak size per cascade depth (capped)
|
FX_BURST_COMBO :f32: 0.72; // extra peak size per cascade depth (capped)
|
||||||
|
|
||||||
// Popup timing/motion. Rises ~1.2 cells over its life and fades out; a combo
|
// Popup timing/motion. Rises ~1.2 cells over its life and fades out; a combo
|
||||||
// (depth > 1) popup is larger and gold.
|
// (depth > 1) popup is larger and gold.
|
||||||
@@ -35,15 +35,17 @@ FX_POPUP_RISE :f32: 1.2;
|
|||||||
FX_POPUP_FONT :f32: 34.0;
|
FX_POPUP_FONT :f32: 34.0;
|
||||||
FX_POPUP_COMBO_FONT :f32: 48.0;
|
FX_POPUP_COMBO_FONT :f32: 48.0;
|
||||||
|
|
||||||
// Bright, slightly pastel tints so a soft glow reads over the dark board, in gem
|
// Vivid candy tints so a soft glow reads brightly over the dark board, in gem
|
||||||
// order (red, orange, yellow, green, blue, purple).
|
// order (red, orange, yellow, green, blue, purple). Saturated a touch past the
|
||||||
|
// pastel — the low channel is trimmed while the dominant/mid channel is lifted —
|
||||||
|
// so every burst pops as a punchier colour without losing luminance.
|
||||||
fx_tint :: (i: s64) -> Color {
|
fx_tint :: (i: s64) -> Color {
|
||||||
if i == 0 { return Color.{ r = 255, g = 86, b = 86, a = 255 }; }
|
if i == 0 { return Color.{ r = 255, g = 92, b = 62, a = 255 }; }
|
||||||
if i == 1 { return Color.{ r = 255, g = 158, b = 64, a = 255 }; }
|
if i == 1 { return Color.{ r = 255, g = 164, b = 44, a = 255 }; }
|
||||||
if i == 2 { return Color.{ r = 255, g = 234, b = 96, a = 255 }; }
|
if i == 2 { return Color.{ r = 255, g = 240, b = 72, a = 255 }; }
|
||||||
if i == 3 { return Color.{ r = 120, g = 240, b = 120, a = 255 }; }
|
if i == 3 { return Color.{ r = 112, g = 250, b = 112, a = 255 }; }
|
||||||
if i == 4 { return Color.{ r = 110, g = 184, b = 255, a = 255 }; }
|
if i == 4 { return Color.{ r = 96, g = 192, b = 255, a = 255 }; }
|
||||||
Color.{ r = 206, g = 132, b = 255, a = 255 }
|
Color.{ r = 224, g = 124, b = 255, a = 255 }
|
||||||
}
|
}
|
||||||
|
|
||||||
FX_POPUP_COLOR :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
FX_POPUP_COLOR :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||||
|
|||||||
16
gem_anim.sx
16
gem_anim.sx
@@ -74,19 +74,21 @@ land_squash :: (tl: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Clear pop ---------------------------------------------------------------
|
// --- Clear pop ---------------------------------------------------------------
|
||||||
// The matched-gem clear: a brief outward pop then a collapse to nothing over its
|
// The matched-gem clear: a snappy outward pop then a collapse to nothing over its
|
||||||
// local 0..1, so the clear reads as a satisfying pop rather than a plain shrink.
|
// local 0..1, so the clear reads as a satisfying candy pop rather than a plain
|
||||||
// Composes with the existing particle burst / score popup (board_fx.sx).
|
// shrink. A fast rise to a bigger overshoot makes the snap read; the soft
|
||||||
CLEAR_POP_A :f32: 0.22;
|
// particle burst / score popup (board_fx.sx) compose on top.
|
||||||
|
CLEAR_POP_A :f32: 0.34; // overshoot height above resting scale
|
||||||
|
CLEAR_POP_RISE :f32: 0.18; // fraction of the window spent rising to the peak
|
||||||
|
|
||||||
clear_pop_scale :: (t: f32) -> f32 {
|
clear_pop_scale :: (t: f32) -> f32 {
|
||||||
if t <= 0.0 { return 1.0; }
|
if t <= 0.0 { return 1.0; }
|
||||||
if t >= 1.0 { return 0.0; }
|
if t >= 1.0 { return 0.0; }
|
||||||
if t < 0.30 {
|
if t < CLEAR_POP_RISE {
|
||||||
return 1.0 + CLEAR_POP_A * (t / 0.30);
|
return 1.0 + CLEAR_POP_A * (t / CLEAR_POP_RISE);
|
||||||
}
|
}
|
||||||
peak := 1.0 + CLEAR_POP_A;
|
peak := 1.0 + CLEAR_POP_A;
|
||||||
u := (t - 0.30) / 0.70;
|
u := (t - CLEAR_POP_RISE) / (1.0 - CLEAR_POP_RISE);
|
||||||
peak * (1.0 - u * u)
|
peak * (1.0 - u * u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 3.7 MiB |
37
main.sx
37
main.sx
@@ -180,10 +180,15 @@ frame :: () {
|
|||||||
g_pipeline.dispatch_event(ev);
|
g_pipeline.dispatch_event(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the in-flight move animation by this frame's delta before rendering,
|
// Advance the in-flight move animation + its match FX by this frame's delta
|
||||||
// so the board view draws the timeline slice for the current wall-clock time.
|
// before rendering, so the board view draws the timeline slice for the current
|
||||||
if g_anim != null { g_anim.tick(g_delta_time); }
|
// wall-clock time. Capture mode pins the animation clock (M3TE_ANIM_TIME);
|
||||||
if g_fx != null { g_fx.tick(g_delta_time); }
|
// while pinned the move/FX timelines stay frozen at the phase the startup
|
||||||
|
// hooks set, so the FX-match scene (M3TE_FX) screenshots identically each run.
|
||||||
|
if g_motion == null or !g_motion.pinned {
|
||||||
|
if g_anim != null { g_anim.tick(g_delta_time); }
|
||||||
|
if g_fx != null { g_fx.tick(g_delta_time); }
|
||||||
|
}
|
||||||
|
|
||||||
// Advance the always-on per-gem animation clock (idle/select/land). Capture
|
// Advance the always-on per-gem animation clock (idle/select/land). Capture
|
||||||
// mode pins the clock, so it only moves when not pinned. On the exact frame a
|
// mode pins the clock, so it only moves when not pinned. On the exact frame a
|
||||||
@@ -326,6 +331,30 @@ main :: () -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match-FX capture hook (P11.1). The bursts/popups spawn off a committed move,
|
||||||
|
// which the sim can't script (no public touch injection), so M3TE_FX forces a
|
||||||
|
// representative match at startup the same way a swipe would: it commits the
|
||||||
|
// n-th currently-legal swap (1-based, clamped; =1 is the first) via the normal
|
||||||
|
// plan_and_commit path, then begins the move timeline + its FX. M3TE_ANIM_TIME
|
||||||
|
// pins the phase — advancing both to that time, after which the frozen frame
|
||||||
|
// loop holds them there — so the burst + "+points" popup screenshot identically
|
||||||
|
// every run. A larger M3TE_ANIM_TIME lands past the timeline, capturing the
|
||||||
|
// settled board with the FX fully pruned. Startup-only and unset → fully live.
|
||||||
|
if fx := read_env("M3TE_FX") {
|
||||||
|
swaps := legal_swaps(g_board);
|
||||||
|
if swaps.len > 0 {
|
||||||
|
n := parse_s64(fx);
|
||||||
|
if n < 1 { n = 1; }
|
||||||
|
if n > swaps.len { n = swaps.len; }
|
||||||
|
sw := swaps.items[n - 1];
|
||||||
|
mv := plan_and_commit(g_board, sw.a, sw.b);
|
||||||
|
g_anim.begin(mv);
|
||||||
|
g_fx.begin(@mv);
|
||||||
|
g_anim.tick(g_motion.clock);
|
||||||
|
g_fx.tick(g_motion.clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Level-state capture hooks (P7.2): override the goal / move budget so a
|
// Level-state capture hooks (P7.2): override the goal / move budget so a
|
||||||
// terminal status can be screenshot without scripting a swipe. M3TE_TARGET=0
|
// terminal status can be screenshot without scripting a swipe. M3TE_TARGET=0
|
||||||
// makes the fresh board read WON immediately (score 0 ≥ goal 0);
|
// makes the fresh board read WON immediately (score 0 ≥ goal 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user