P17.2: organic fall — per-column stagger (cascade pour)
render_fall now offsets each COLUMN's drop START by a small bounded delay (fall_stagger_t) so a refilled/collapsed row pours in as a left-to-right cascade instead of every gem snapping down in one flat lockstep row. Column col waits FALL_STAGGER_MAX (0.30) * col/7 of the fall window, then falls over the remaining 1 - 0.30, with that local progress fed through ease_in_cubic so each column still accelerates under gravity within its own window. Bounded by construction: the last column lands EXACTLY at t=1 and every earlier column strictly before it, so no gem is ever left mid-air at the segment end — the seam to the next round / settled board stays invisible and move.final is untouched. FALL_ANIM_DUR (0.22s) and the timeline helpers (phase/total/cascade_rounds_started) are unchanged, so the per-round cascade-cue timing snapshots don't churn and live per-round audio is unaffected. Render-only — no board.sx model change. tests/easing.sx pins fall_stagger_t: f(0)=0, f(1)=1 across all columns (no gem unlanded), per-column monotonicity, and the mid-fall cascade ordering (each later column strictly behind the one before). tests/anim_plan.sx (final==model, contiguity) stays green. Golden goldens/p17_stagger.png: M3TE_FX=11 (depth-5 cascade, seed 1337) pinned at M3TE_ANIM_TIME=1.91 — round 4 refills columns 2-7 by one cell each, so the top row reads as a left-to-right staircase (vs the pre-stagger flat row in p17_fall.png).
This commit is contained in:
35
README.md
35
README.md
@@ -309,6 +309,41 @@ swap→(clear,fall)\* timeline (swap 0.16, then clear 0.14 + fall 0.22 per round
|
||||
`1.51` lands squarely in round 3's fall. The change is render-only — no `board.sx`
|
||||
model change, and normal play is byte-identical apart from the fall's motion curve.
|
||||
|
||||
### Organic fall — per-column stagger (P17.2)
|
||||
|
||||
The fall no longer drops a whole row in lockstep: `render_fall` offsets each
|
||||
COLUMN's drop START by a small bounded delay (`fall_stagger_t`), so a refilled row
|
||||
pours in as a left-to-right cascade. Column `col` waits `FALL_STAGGER_MAX·col/7`
|
||||
(`FALL_STAGGER_MAX = 0.30`) of the fall window, then falls over the remaining
|
||||
`1 - 0.30`, feeding that local progress through `ease_in_cubic` so each column still
|
||||
accelerates under gravity within its own window. The LAST column lands EXACTLY at
|
||||
`t=1` and every earlier column strictly before it, so NO gem is ever left mid-air at
|
||||
the segment end — the seam to the next round / settled board stays invisible and
|
||||
`move.final` is untouched (`tests/anim_plan.sx` contiguity stays green;
|
||||
`tests/easing.sx` pins `fall_stagger_t`'s `f(0)=0`, `f(1)=1`, monotonicity, and the
|
||||
mid-fall cascade ordering). `FALL_ANIM_DUR` (0.22 s) is unchanged, so the per-round
|
||||
cascade-cue snapshots don't churn.
|
||||
|
||||
The cleanest tell is a round where several adjacent columns refill the SAME
|
||||
distance: in lockstep their gems share one height (a flat row); staggered, they
|
||||
form a diagonal. On seed 1337, `M3TE_FX=11` **round 4** refills columns 2–7 by one
|
||||
cell each (its fall window `[1.74, 1.96)` s); at `1.91` the leading column has just
|
||||
landed while the trailing column is still ~⅔ cell high, so the top row reads as a
|
||||
left-to-right staircase instead of a flat band (contrast `goldens/p17_fall.png`,
|
||||
which is pre-stagger lockstep):
|
||||
|
||||
```bash
|
||||
# Refilled row pouring in as a staggered cascade: goldens/p17_stagger.png
|
||||
env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=1.91 \
|
||||
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
|
||||
# Same cascade past the timeline — every gem landed exactly on the model board:
|
||||
env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=3.0 \
|
||||
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
|
||||
```
|
||||
|
||||
The change is render-only — no `board.sx` model change, and normal play is
|
||||
byte-identical apart from the fall's per-column timing.
|
||||
|
||||
## Audio bank (P10) — final model
|
||||
|
||||
The SFX bank (`audio.sx`) is a purely additive layer over iOS **System Sound
|
||||
|
||||
@@ -97,6 +97,26 @@ bad_swap_bounce :: (t: f32) -> f32 {
|
||||
BADSWAP_LUNGE_AMP * (1.0 - spring(u))
|
||||
}
|
||||
|
||||
// Per-column fall stagger (P17.2): within the fall window, each column starts its
|
||||
// drop at a small BOUNDED delay so a refilled/collapsed row pours in as a cascade
|
||||
// instead of every gem snapping down in one flat lockstep row. Column `col` waits
|
||||
// FALL_STAGGER_MAX * col/(BOARD_COLS-1) of the window, then falls over the
|
||||
// remaining `1 - FALL_STAGGER_MAX`, so the LAST column lands EXACTLY at t==1 and
|
||||
// every earlier column lands strictly before it — no gem is ever left mid-air when
|
||||
// the segment ends (the seam to the next round / settled board stays invisible).
|
||||
// Returns the column's LOCAL 0..1 progress; render_fall feeds it through
|
||||
// ease_in_cubic so each column still accelerates under gravity within its window.
|
||||
// `tests/easing.sx` pins f(0)=0, f(1)=1, monotonicity, and the cascade ordering.
|
||||
FALL_STAGGER_MAX :f32: 0.30;
|
||||
fall_stagger_t :: (t: f32, col: s64) -> f32 {
|
||||
delay := FALL_STAGGER_MAX * (cast(f32) col / cast(f32) (BOARD_COLS - 1));
|
||||
window := 1.0 - FALL_STAGGER_MAX;
|
||||
lt := (t - delay) / window;
|
||||
if lt <= 0.0 { return 0.0; }
|
||||
if lt >= 1.0 { return 1.0; }
|
||||
lt
|
||||
}
|
||||
|
||||
// One recorded cascade round. `before` is the board at the round's start (the
|
||||
// swapped board for round 0, the previous round's `after` otherwise — never has
|
||||
// holes). `matched` flags the cells cleared this round (they scale out). `src`
|
||||
|
||||
@@ -531,16 +531,19 @@ BoardView :: struct {
|
||||
|
||||
// Fall segment: every gem of the round's settled board accelerates under
|
||||
// gravity from its source row (above the board for refills) down to its
|
||||
// destination cell. ease_in_cubic pins f(1)=1, so each gem lands exactly on
|
||||
// its cell and the seam to the next round / settled board stays invisible.
|
||||
// destination cell. Each COLUMN's drop starts at a small staggered delay
|
||||
// (fall_stagger_t) so a refilled row pours in as a cascade rather than a flat
|
||||
// lockstep row; ease_in_cubic pins each column's f(1)=1, and fall_stagger_t
|
||||
// guarantees every column reaches 1 by t==1, so each gem lands exactly on its
|
||||
// cell and the seam to the next round / settled board stays invisible.
|
||||
render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, inset: f32, dim: f32, t: f32) {
|
||||
te := ease_in_cubic(t);
|
||||
for 0..BOARD_CELLS: (i) {
|
||||
g := rd.after[i];
|
||||
if g == .empty { continue; }
|
||||
col := i % BOARD_COLS;
|
||||
drow := i / BOARD_COLS;
|
||||
src := rd.src[i];
|
||||
te := ease_in_cubic(fall_stagger_t(t, col));
|
||||
cur_row := cast(f32) src + (cast(f32) drow - cast(f32) src) * te;
|
||||
gf := self.gem_frame(cast(f32) col, cur_row, inset, dim);
|
||||
self.draw_gem(ctx, gf, cast(s64) g);
|
||||
|
||||
BIN
goldens/p17_stagger.png
Normal file
BIN
goldens/p17_stagger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -141,6 +141,41 @@ main :: () -> s32 {
|
||||
if !bb_overshoot_bounded { fails += 1; }
|
||||
if !bb_settles { fails += 1; }
|
||||
|
||||
// 6. Per-column fall stagger (P17.2): the fall window offsets each column's drop
|
||||
// START by a BOUNDED delay so a refilled row pours in as a cascade, yet EVERY
|
||||
// column still lands EXACTLY on its cell by the segment end. Lock: at t==0 no
|
||||
// column has moved; at t==1 EVERY column has reached local progress 1 (no gem
|
||||
// left mid-air — the seam to the next round stays invisible); per-column local
|
||||
// progress is monotonic in t; and MID-fall the columns form a cascade — each
|
||||
// later column has made STRICTLY LESS progress than the one before (its drop
|
||||
// starts later), the opposite of a flat lockstep row sharing one progress.
|
||||
print("== fall stagger bounded ==\n");
|
||||
stg_t0 := true; stg_t1 := true;
|
||||
for 0..BOARD_COLS: (c) {
|
||||
if fall_stagger_t(0.0, c) != 0.0 { stg_t0 = false; }
|
||||
if fall_stagger_t(1.0, c) != 1.0 { stg_t1 = false; }
|
||||
}
|
||||
stg_cascade := true;
|
||||
for 1..BOARD_COLS: (c) {
|
||||
if !(fall_stagger_t(0.5, c) < fall_stagger_t(0.5, c - 1)) { stg_cascade = false; }
|
||||
}
|
||||
stg_mono := true;
|
||||
for 0..BOARD_COLS: (c) {
|
||||
pp := fall_stagger_t(0.0, c);
|
||||
for 1..21: (i) {
|
||||
tt := cast(f32) i / 20.0;
|
||||
vv := fall_stagger_t(tt, c);
|
||||
if vv < pp - 0.000001 { stg_mono = false; }
|
||||
pp = vv;
|
||||
}
|
||||
}
|
||||
print("stagger_t0 {} stagger_t1 {} stagger_cascade {} stagger_mono {}\n",
|
||||
stg_t0, stg_t1, stg_cascade, stg_mono);
|
||||
if !stg_t0 { fails += 1; }
|
||||
if !stg_t1 { fails += 1; }
|
||||
if !stg_cascade { fails += 1; }
|
||||
if !stg_mono { fails += 1; }
|
||||
|
||||
if fails == 0 {
|
||||
print("ok: easing toolkit endpoints locked + amplitudes bounded\n");
|
||||
return 0;
|
||||
|
||||
@@ -8,4 +8,6 @@ back_overshoots true back_bounded true spring_overshoots true spring_bounded tru
|
||||
squash_moves true squash_two_sided true squash_bounded true
|
||||
== illegal-swap bounce ==
|
||||
bounce_ends true peak_amp true peak_loc true overshoots true overshoot_bounded true settles true
|
||||
== fall stagger bounded ==
|
||||
stagger_t0 true stagger_t1 true stagger_cascade true stagger_mono true
|
||||
ok: easing toolkit endpoints locked + amplitudes bounded
|
||||
|
||||
Reference in New Issue
Block a user