diff --git a/board_view.sx b/board_view.sx index 22884b4..0c27434 100644 --- a/board_view.sx +++ b/board_view.sx @@ -487,14 +487,17 @@ BoardView :: struct { // Restart action behind the banner's button: reseed the SAME starting level // through the model (board.restart) and drop every transient view layer - // (selection, in-flight drag, move animation, FX) so the board returns to a - // clean in_progress state. + // (selection, in-flight drag, move animation, FX, and the per-gem landing + // bounce) so the board returns to a clean, resting in_progress state. Without + // the motion reset a restart fired right after a terminal cascade would carry + // that move's landing squash onto the freshly seeded board. do_restart :: (self: *BoardView) { self.board.restart(self.seed); self.sel.clear(); self.drag.clear(); if self.anim != null { self.anim.init(); } if self.fx != null { self.fx.clear(); } + self.motion.reset_landings(); } } diff --git a/gem_anim.sx b/gem_anim.sx index 1cfca42..6cdead0 100644 --- a/gem_anim.sx +++ b/gem_anim.sx @@ -103,6 +103,14 @@ GemMotion :: struct { init :: (self: *GemMotion) { self.clock = 0.0; self.pinned = false; + self.reset_landings(); + } + + // Drop every landing stamp back to the never-landed sentinel so no cell + // carries a squash-bounce. `restart` calls this so a reseeded board starts at + // its resting pose instead of replaying the prior move's landing wobble; the + // idle clock keeps running, so the always-on idle simply resumes from rest. + reset_landings :: (self: *GemMotion) { for 0..BOARD_CELLS: (i) { self.land_at[i] = -1000.0; } } diff --git a/tests/expected/gem_pose.stdout b/tests/expected/gem_pose.stdout index c9af136..390066a 100644 --- a/tests/expected/gem_pose.stdout +++ b/tests/expected/gem_pose.stdout @@ -10,4 +10,6 @@ land_start_rest true land_end_rest true land_mid_wobbles true clear_start_full true clear_end_gone true clear_overshoots true == gem motion land bookkeeping == motion_init true motion_no_land true motion_fresh_land true +== gem motion restart resets landings == +restart_pre_squashing true restart_post_rest true restart_clock_kept true ok: per-gem animation rests at t=0 and stays bounded diff --git a/tests/gem_pose.sx b/tests/gem_pose.sx index 9a7c480..29e26fe 100644 --- a/tests/gem_pose.sx +++ b/tests/gem_pose.sx @@ -96,6 +96,33 @@ main :: () -> s32 { if !no_land { fails += 1; } if !fresh_land { fails += 1; } + // 7. Restart resets the landing bounce. The restart button (BoardView. + // do_restart) reseeds the model AND must clear the per-gem landing state, + // or a restart fired right after a terminal cascade carries that move's + // squash onto the freshly seeded board. reset_landings is the factored + // reset do_restart calls: a cell stamped a moment ago is mid-squash, and + // after reset_landings every cell is back at rest — while the idle clock + // is deliberately left running (idle resumes from its normal phase). + print("== gem motion restart resets landings ==\n"); + r : GemMotion = ---; + r.init(); + r.clock = 5.0; + r.stamp_land(3); + r.stamp_land(42); + r.clock = 5.08; // 0.08s past impact: well inside LAND_DUR, so mid-squash. + pre_squashing := fabs(land_squash(r.land_local(3))) > 0.01 + and fabs(land_squash(r.land_local(42))) > 0.01; + r.reset_landings(); + post_rest := land_squash(r.land_local(3)) == 0.0 + and land_squash(r.land_local(42)) == 0.0 + and land_squash(r.land_local(0)) == 0.0; + clock_kept := r.clock == 5.08; + print("restart_pre_squashing {} restart_post_rest {} restart_clock_kept {}\n", + pre_squashing, post_rest, clock_kept); + if !pre_squashing { fails += 1; } + if !post_rest { fails += 1; } + if !clock_kept { fails += 1; } + if fails == 0 { print("ok: per-gem animation rests at t=0 and stays bounded\n"); return 0;