Merge branch 'flow/m3te/P10.10' into m3te-plan
This commit is contained in:
@@ -144,18 +144,25 @@ BoardAnim :: struct {
|
||||
active: bool;
|
||||
elapsed: f32;
|
||||
move: AnimMove;
|
||||
// Highest 1-based cascade round whose ascending combo cue has already played,
|
||||
// so the frame loop's per-round SFX is edge-triggered: a round's cue fires once,
|
||||
// when its clear begins, never re-fired every frame. Reset whenever a move
|
||||
// (re)starts; advanced by the frame loop as rounds clear.
|
||||
cascade_fired: s64;
|
||||
|
||||
init :: (self: *BoardAnim) {
|
||||
self.active = false;
|
||||
self.elapsed = 0.0;
|
||||
self.move.legal = false;
|
||||
self.move.rounds = List(AnimRound).{};
|
||||
self.cascade_fired = 0;
|
||||
}
|
||||
|
||||
begin :: (self: *BoardAnim, m: AnimMove) {
|
||||
self.move = m;
|
||||
self.elapsed = 0.0;
|
||||
self.active = true;
|
||||
self.cascade_fired = 0;
|
||||
}
|
||||
|
||||
// Total wall-clock length: the swap segment plus a clear+fall pair per round.
|
||||
@@ -190,6 +197,22 @@ BoardAnim :: struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-round cascade-cue timing (P10.10): how many cascade rounds have BEGUN their
|
||||
// clear (pop) by `elapsed`, on the SAME swap→(clear,fall)* timeline `phase` walks.
|
||||
// Round k (0-based) starts clearing at SWAP_ANIM_DUR + k*(CLEAR_ANIM_DUR +
|
||||
// FALL_ANIM_DUR), so this is the count of rounds whose ascending combo cue should
|
||||
// have fired by now (clamped to the move's round count). The frame loop diffs it
|
||||
// against `BoardAnim.cascade_fired` to play one cue per newly-cleared round. Pure +
|
||||
// headless so the per-round playback is snapshot-testable without audio.
|
||||
cascade_rounds_started :: (elapsed: f32, num_rounds: s64) -> s64 {
|
||||
if num_rounds <= 0 { return 0; }
|
||||
if elapsed < SWAP_ANIM_DUR { return 0; }
|
||||
seg := CLEAR_ANIM_DUR + FALL_ANIM_DUR;
|
||||
started := cast(s64) ((elapsed - SWAP_ANIM_DUR) / seg) + 1;
|
||||
if started > num_rounds { return num_rounds; }
|
||||
started
|
||||
}
|
||||
|
||||
// Input gate: the board accepts a new swipe/tap gesture only when no move
|
||||
// animation is in flight. The view checks this at gesture START (mouse_down),
|
||||
// not at commit (mouse_up), so a gesture begun while a timeline is playing never
|
||||
|
||||
@@ -730,17 +730,17 @@ impl View for BoardView {
|
||||
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); }
|
||||
// SFX (P10.3): additive cues for the committed gesture —
|
||||
// never reads or writes board/score/move state. The swap
|
||||
// slide cue plays for any committed gesture (legal or the
|
||||
// reverted ping-back); a legal move adds the match pop on its
|
||||
// first clearing round; a multi-round chain adds the escalating
|
||||
// cascade cue keyed to recorded depth (mv.rounds.len), distinct
|
||||
// from the match pop so a single clear is never doubled.
|
||||
// SFX: additive cues for the committed gesture — never reads
|
||||
// or writes board/score/move state. The swap slide cue plays
|
||||
// for any committed gesture (legal or the reverted ping-back);
|
||||
// a legal move adds the match pop on its first clearing round.
|
||||
// A multi-round chain's ascending combo cues are NOT fired here:
|
||||
// the frame loop plays one per round, edge-triggered as each
|
||||
// round visually clears (combo1, combo2, …), so the cascade
|
||||
// reads as an audible ascending run instead of one cue at commit.
|
||||
sfx_swap();
|
||||
if mv.legal {
|
||||
sfx_match();
|
||||
if mv.rounds.len >= 2 { sfx_cascade(mv.rounds.len); }
|
||||
}
|
||||
self.sel.clear();
|
||||
} else {
|
||||
|
||||
14
main.sx
14
main.sx
@@ -197,6 +197,20 @@ frame :: () {
|
||||
if g_fx != null { g_fx.tick(g_delta_time); }
|
||||
}
|
||||
|
||||
// Per-round cascade SFX (P10.10): as each cascade round's clear begins on the
|
||||
// move timeline, play the NEXT ascending combo cue (round 1 → combo1, round 2
|
||||
// → combo2, … clamped at combo5). Edge-triggered off `cascade_fired` so each
|
||||
// round's cue fires exactly once; only a real multi-round chain (rounds >= 2)
|
||||
// gets the run, so a single match stays the lone match pop. Additive — reads
|
||||
// only the recorded timeline, never board/score/move state.
|
||||
if g_anim != null and g_anim.move.rounds.len >= 2 {
|
||||
started := cascade_rounds_started(g_anim.elapsed, g_anim.move.rounds.len);
|
||||
while g_anim.cascade_fired < started {
|
||||
g_anim.cascade_fired += 1;
|
||||
sfx_cascade(g_anim.cascade_fired);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// move timeline settles, stamp the landing bounce on every cell the move
|
||||
|
||||
64
tests/cascade_rounds.sx
Normal file
64
tests/cascade_rounds.sx
Normal file
@@ -0,0 +1,64 @@
|
||||
// P10.10 — Per-round cascade-cue timing snapshot: prove the frame loop plays ONE
|
||||
// ascending combo cue PER cascade round, edge-triggered as each round's clear
|
||||
// begins — combo1, combo2, … clamped at combo5, an audible ascending run — NOT a
|
||||
// single combo cue keyed to the final cascade depth. The pure timing helper
|
||||
// `cascade_rounds_started` (board_anim.sx) reports how many rounds have begun
|
||||
// clearing by a given `elapsed` on the SAME swap→(clear,fall)* timeline the view
|
||||
// animates; the round→cue mapping reuses `cascade_cue_index`/`cascade_cue_name`
|
||||
// (audio.sx), the exact functions `sfx_cascade` plays. Simulating the frame loop
|
||||
// over a 5-round chain yields the locked combo1..combo5 run. Links headless like
|
||||
// tests/anim_plan.sx (board_anim pulls no GL) + tests/cascade_cue.sx (audio alone
|
||||
// links). Failure is a non-zero exit code.
|
||||
#import "modules/std.sx";
|
||||
#import "board_anim.sx";
|
||||
#import "audio.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
print("== per-round cascade cue timing ==\n");
|
||||
|
||||
// `cascade_rounds_started` = how many cascade rounds have BEGUN clearing by
|
||||
// `elapsed`, on the swap(0.16s)→[clear(0.14s),fall(0.22s)] per-round timeline.
|
||||
// Round k (0-based) starts clearing at 0.16 + k*0.36; sampled safely INSIDE
|
||||
// each round window so the integer step is unambiguous. Locked for 5 rounds.
|
||||
print("-- started-count across a 5-round chain --\n");
|
||||
rounds : s64 = 5;
|
||||
print("e=0.00 -> {}\n", cascade_rounds_started(0.00, rounds));
|
||||
print("e=0.10 -> {}\n", cascade_rounds_started(0.10, rounds));
|
||||
print("e=0.20 -> {}\n", cascade_rounds_started(0.20, rounds));
|
||||
print("e=0.50 -> {}\n", cascade_rounds_started(0.50, rounds));
|
||||
print("e=0.55 -> {}\n", cascade_rounds_started(0.55, rounds));
|
||||
print("e=0.90 -> {}\n", cascade_rounds_started(0.90, rounds));
|
||||
print("e=1.30 -> {}\n", cascade_rounds_started(1.30, rounds));
|
||||
print("e=1.70 -> {}\n", cascade_rounds_started(1.70, rounds));
|
||||
print("e=5.00 -> {}\n", cascade_rounds_started(5.00, rounds));
|
||||
|
||||
// Edge-triggered playback: stepping the clock, each rising edge of the
|
||||
// started-count plays the NEXT round's cue — combo1..combo5, once each,
|
||||
// ascending. This IS the loop main's frame loop runs; the emitted run is the
|
||||
// locked acceptance ordering.
|
||||
print("-- ascending per-round run --\n");
|
||||
fired : s64 = 0;
|
||||
elapsed : f32 = 0.0;
|
||||
while fired < rounds {
|
||||
started := cascade_rounds_started(elapsed, rounds);
|
||||
while fired < started {
|
||||
fired += 1;
|
||||
print("round {} -> {}\n", fired, cascade_cue_name(cascade_cue_index(fired)));
|
||||
}
|
||||
elapsed += 0.02;
|
||||
}
|
||||
|
||||
// Non-cascade guards: a 1-round (single match) and 0-round (illegal) move. The
|
||||
// frame loop gates the run on rounds>=2, so neither plays a combo run; the
|
||||
// helper still reports the lone/zero round so the gate lives in the caller.
|
||||
print("-- non-cascade --\n");
|
||||
print("1-round@end {}\n", cascade_rounds_started(5.0, 1));
|
||||
print("0-round@end {}\n", cascade_rounds_started(5.0, 0));
|
||||
|
||||
// Deep chain: the cue tail clamps at combo5 for round >= 5 (cascade_cue_index).
|
||||
print("-- deep-chain cue clamp --\n");
|
||||
for 1..8: (r) { print("round {} -> {}\n", r, cascade_cue_name(cascade_cue_index(r))); }
|
||||
|
||||
print("ok: one ascending combo cue per cascade round, clamped at combo5\n");
|
||||
return 0;
|
||||
}
|
||||
1
tests/expected/cascade_rounds.exit
Normal file
1
tests/expected/cascade_rounds.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
29
tests/expected/cascade_rounds.stdout
Normal file
29
tests/expected/cascade_rounds.stdout
Normal file
@@ -0,0 +1,29 @@
|
||||
== per-round cascade cue timing ==
|
||||
-- started-count across a 5-round chain --
|
||||
e=0.00 -> 0
|
||||
e=0.10 -> 0
|
||||
e=0.20 -> 1
|
||||
e=0.50 -> 1
|
||||
e=0.55 -> 2
|
||||
e=0.90 -> 3
|
||||
e=1.30 -> 4
|
||||
e=1.70 -> 5
|
||||
e=5.00 -> 5
|
||||
-- ascending per-round run --
|
||||
round 1 -> [sx] audio: cue combo1
|
||||
round 2 -> [sx] audio: cue combo2
|
||||
round 3 -> [sx] audio: cue combo3
|
||||
round 4 -> [sx] audio: cue combo4
|
||||
round 5 -> [sx] audio: cue combo5
|
||||
-- non-cascade --
|
||||
1-round@end 1
|
||||
0-round@end 0
|
||||
-- deep-chain cue clamp --
|
||||
round 1 -> [sx] audio: cue combo1
|
||||
round 2 -> [sx] audio: cue combo2
|
||||
round 3 -> [sx] audio: cue combo3
|
||||
round 4 -> [sx] audio: cue combo4
|
||||
round 5 -> [sx] audio: cue combo5
|
||||
round 6 -> [sx] audio: cue combo5
|
||||
round 7 -> [sx] audio: cue combo5
|
||||
ok: one ascending combo cue per cascade round, clamped at combo5
|
||||
Reference in New Issue
Block a user