Merge branch 'flow/m3te/P18.1' into m3te-plan

This commit is contained in:
swipelab
2026-06-06 13:05:26 +03:00
6 changed files with 78 additions and 12 deletions

View File

@@ -384,6 +384,40 @@ env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=3.0 \
no column has landed yet, so neither carries squash. The change is render-only — no
`board.sx` model change, and a resting board is untouched.
### Organic combine — anticipation pop on clear (P18.1)
Matched gems no longer just pop-then-shrink flatly: `clear_pop_scale` now shapes the
clear as a candy pop in three beats over its local `0..1` — a tiny anticipation
squash dip (a "gather" ~8 % below rest), a snappy overshoot up to ~1.40× via P15.1's
`ease_out_back`, then an accelerating collapse to nothing (`ease_in_quad`). The
endpoints stay LOCKED — `t==0 → 1.0` (rest) and `t==1 → 0.0` (gone) — so the seam to
the model board is clean and `M3TE_ANIM_TIME=0` still reproduces the resting board;
the soft particle burst / `+points` popup (`board_fx.sx`) compose on top.
`tests/gem_pose.sx` pins the new envelope (locked rest endpoints, the anticipation
dip below rest, the overshoot above 1, and the strictly monotonic post-peak
collapse). `CLEAR_ANIM_DUR` (0.14 s) is unchanged, so the per-round cascade-cue
timing snapshots (`tests/cascade_rounds.sx` / `cascade_cue.sx`) don't churn.
The pop peaks at clear-phase local `t ≈ 0.37`; for `M3TE_FX=3` (the seed-1337
vertical red 3-match) the clear window is `[0.16, 0.30)` s, so `0.21` catches the
matched gems at their fullest overshoot, composed with the burst and "+30" popup:
```bash
# Anticipation/overshoot candy pop at its peak (composed with the burst):
# goldens/p18_pop.png
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.21 \
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
# Same match at exact rest (t=0) — board sits at its resting pose, no pop:
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
```
`goldens/p6_fx_match.png` (the P11.1 burst+popup reference, pinned at `0.22`) was
refreshed for the new pop shape. The per-gem STAGGER of the explosions is the next
step (P18.2); this step is the per-gem pop SHAPE only. The change is render-only —
no `board.sx` model change, and normal play is byte-identical apart from the clear's
pop curve.
### FPS counter — dev overlay (P20.1)
A small FPS readout for gauging frame cost while tuning the animations. It is a

View File

@@ -76,22 +76,39 @@ land_squash :: (tl: f32) -> f32 {
}
// --- Clear pop ---------------------------------------------------------------
// 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 candy pop rather than a plain
// shrink. A fast rise to a bigger overshoot makes the snap read; the soft
// 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
// The matched-gem clear, shaped as a candy pop in three beats over its local
// 0..1: a tiny anticipation squash dip (a "gather" just below rest), a snappy
// overshoot up to the peak via P15.1's ease_out_back, then an accelerating
// collapse to nothing. Endpoints are LOCKED — t==0 -> 1.0 (rest) and t==1 -> 0.0
// (gone) — so the seam to the model board stays clean; the soft particle burst /
// score popup (board_fx.sx) compose on top.
CLEAR_DIP_T :f32: 0.16; // fraction of the window spent on the anticipation dip
CLEAR_DIP_A :f32: 0.08; // how far the gem compresses below rest before popping
CLEAR_POP_RISE :f32: 0.52; // window fraction at which the overshoot peak is reached
CLEAR_POP_A :f32: 0.36; // overshoot height above resting scale
clear_pop_scale :: (t: f32) -> f32 {
if t <= 0.0 { return 1.0; }
if t >= 1.0 { return 0.0; }
if t < CLEAR_POP_RISE {
return 1.0 + CLEAR_POP_A * (t / CLEAR_POP_RISE);
if t < CLEAR_DIP_T {
// Anticipation gather: sin(PI*u) is 0 at both ends, so t==0 stays exactly
// at rest and the dip hands off to the rise at rest — a brief squash, not
// a step.
u := t / CLEAR_DIP_T;
return 1.0 - CLEAR_DIP_A * sin(PI * u);
}
if t < CLEAR_POP_RISE {
// Snap up to the peak: ease_out_back rises from rest, shoots a touch past
// 1+A, then eases back to exactly 1+A at the seam (its locked f(1)=1), so
// the maximum is a single clean overshoot with no second reversal.
u := (t - CLEAR_DIP_T) / (CLEAR_POP_RISE - CLEAR_DIP_T);
return 1.0 + CLEAR_POP_A * ease_out_back(u);
}
// Collapse to nothing: accelerate the shrink from the peak so the gem vanishes
// as the burst takes over. ease_in_quad pins the seam at the peak and t==1 at 0.
peak := 1.0 + CLEAR_POP_A;
u := (t - CLEAR_POP_RISE) / (1.0 - CLEAR_POP_RISE);
peak * (1.0 - u * u)
peak * (1.0 - ease_in_quad(u))
}
// Live per-gem animation state, heap-allocated (like BoardAnim/BoardFx) so it

BIN
goldens/p18_pop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -7,7 +7,7 @@ select_start_rest true select_end_rest true select_mid_pops true
== land squash envelope ==
land_start_rest true land_end_rest true land_mid_wobbles true
== clear pop envelope ==
clear_start_full true clear_end_gone true clear_overshoots true
clear_start_full true clear_end_gone true clear_dips true clear_overshoots true clear_collapses true
== gem motion land bookkeeping ==
motion_init true motion_no_land true motion_fresh_land true
== gem motion restart resets landings ==

View File

@@ -71,15 +71,30 @@ main :: () -> s32 {
if !l_end { fails += 1; }
if !l_mid { fails += 1; }
// 5. Clear pop: full at t=0, gone at t=1, overshoots above 1 in between.
// 5. Clear pop: full at t=0, gone at t=1, a tiny anticipation dip BELOW rest in
// the gather window, an overshoot above 1 mid-window, then — past the peak —
// a strictly monotonic collapse to nothing (a single clean pop, no second
// reversal). The locked t=0/t=1 endpoints keep the seam to the model board.
print("== clear pop envelope ==\n");
c_start := approx(clear_pop_scale(0.0), 1.0);
c_end := approx(clear_pop_scale(1.0), 0.0);
c_dip := clear_pop_scale(CLEAR_DIP_T * 0.5) < 1.0;
c_peak := clear_pop_scale(0.30) > 1.1;
print("clear_start_full {} clear_end_gone {} clear_overshoots {}\n", c_start, c_end, c_peak);
c_collapse := true;
pc := clear_pop_scale(CLEAR_POP_RISE);
for 1..21: (i) {
tt := CLEAR_POP_RISE + (1.0 - CLEAR_POP_RISE) * cast(f32) i / 20.0;
vv := clear_pop_scale(tt);
if vv > pc + 0.000001 { c_collapse = false; }
pc = vv;
}
print("clear_start_full {} clear_end_gone {} clear_dips {} clear_overshoots {} clear_collapses {}\n",
c_start, c_end, c_dip, c_peak, c_collapse);
if !c_start { fails += 1; }
if !c_end { fails += 1; }
if !c_dip { fails += 1; }
if !c_peak { fails += 1; }
if !c_collapse { fails += 1; }
// 6. GemMotion land bookkeeping: fresh state is unpinned at t=0, a never-
// landed cell rests, and a freshly-stamped land reads age 0.