Merge branch 'flow/m3te/P7.2' into m3te-plan
This commit is contained in:
31
README.md
31
README.md
@@ -84,3 +84,34 @@ env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
|
||||
|
||||
With no variable set the game runs fully live (the clock advances by
|
||||
`delta_time`). `tests/gem_pose.sx` locks the `t==0`-rest invariant headlessly.
|
||||
|
||||
### Level-state capture (P7.2)
|
||||
|
||||
The win/lose banner and restart button are driven by the model's `level_status`
|
||||
(score vs. goal vs. move budget). Three more env hooks force a terminal status
|
||||
(or a restart) so the banner / restart states can be screenshot deterministically
|
||||
without scripting a winning swipe — combine them with `M3TE_ANIM_TIME` to pin the
|
||||
idle clock:
|
||||
|
||||
- `M3TE_TARGET=<n>` overrides the per-level score goal. `0` makes the fresh board
|
||||
read **won** immediately (`score 0 ≥ goal 0`).
|
||||
- `M3TE_MOVE_LIMIT=<n>` overrides the move budget. `0` makes it read **lost**
|
||||
(budget spent below the goal).
|
||||
- `M3TE_RESTART=<non-zero>` runs `board.restart` after the overrides, capturing
|
||||
the fresh `in_progress` board the restart button produces.
|
||||
|
||||
```bash
|
||||
# Win banner + restart over the board: goldens/p7_win.png
|
||||
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
|
||||
xcrun simctl launch booted co.swipelab.m3te
|
||||
# Lose banner ("OUT OF MOVES") + restart: goldens/p7_lose.png
|
||||
env SIMCTL_CHILD_M3TE_MOVE_LIMIT=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
|
||||
xcrun simctl launch booted co.swipelab.m3te
|
||||
# Fresh in_progress board after restart: goldens/p7_restart.png
|
||||
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_RESTART=1 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
|
||||
xcrun simctl launch booted co.swipelab.m3te
|
||||
```
|
||||
|
||||
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
|
||||
rect ↔ hit-test round-trip headlessly.
|
||||
|
||||
@@ -49,4 +49,47 @@ BoardLayout :: struct {
|
||||
if col >= BOARD_COLS or row >= BOARD_ROWS { return null; }
|
||||
Cell.{ col = col, row = row }
|
||||
}
|
||||
|
||||
// Frame of the whole 8×8 grid (origin + cols/rows × cell_size). The banner
|
||||
// and its dimming overlay are sized off this so they cover exactly the board.
|
||||
grid_frame :: (self: *BoardLayout) -> Frame {
|
||||
Frame.make(
|
||||
self.origin.x, self.origin.y,
|
||||
self.cell_size * cast(f32) BOARD_COLS,
|
||||
self.cell_size * cast(f32) BOARD_ROWS
|
||||
)
|
||||
}
|
||||
|
||||
// Win/lose banner geometry (P7.2): an overlay panel centered over the board
|
||||
// grid, with the title band and the restart button inside it. Derived purely
|
||||
// from the SAME grid layout the gems use, so the restart hit-test in
|
||||
// BoardView.handle_event lands on exactly the button BoardView draws. The
|
||||
// headless banner_layout test locks the button-rect ↔ hit-test round-trip.
|
||||
banner :: (self: *BoardLayout) -> BannerLayout {
|
||||
grid := self.grid_frame();
|
||||
cx := grid.mid_x();
|
||||
cy := grid.mid_y();
|
||||
|
||||
panel_w := grid.size.width * 0.84;
|
||||
panel_h := grid.size.height * 0.44;
|
||||
panel := Frame.make(cx - panel_w * 0.5, cy - panel_h * 0.5, panel_w, panel_h);
|
||||
|
||||
title := Frame.make(panel.origin.x, panel.origin.y + panel_h * 0.18, panel_w, panel_h * 0.30);
|
||||
|
||||
btn_w := panel_w * 0.60;
|
||||
btn_h := panel_h * 0.24;
|
||||
btn_y := panel.origin.y + panel_h - btn_h - panel_h * 0.16;
|
||||
button := Frame.make(cx - btn_w * 0.5, btn_y, btn_w, btn_h);
|
||||
|
||||
BannerLayout.{ panel = panel, title = title, button = button }
|
||||
}
|
||||
}
|
||||
|
||||
// Resolved rectangles of the win/lose banner: the centered `panel`, the `title`
|
||||
// band where the win/lose headline is centered, and the restart `button` rect
|
||||
// (also the hit-test target). All in the same view-local space as BoardLayout.
|
||||
BannerLayout :: struct {
|
||||
panel: Frame;
|
||||
title: Frame;
|
||||
button: Frame;
|
||||
}
|
||||
|
||||
115
board_view.sx
115
board_view.sx
@@ -40,6 +40,19 @@ HUD_LINE_GAP :f32: 6.0;
|
||||
HUD_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||
HUD_PANEL :: Color.{ r = 12, g = 14, b = 22, a = 185 };
|
||||
|
||||
// Win/lose banner (P7.2): a dim over the board, an opaque panel, the win/lose
|
||||
// headline, and a restart button. Built from text + rects only — the engine's
|
||||
// image path can't tint/fade at draw time (issue 0002), but rects and text DO
|
||||
// honour colour + alpha, so the whole overlay is drawn with them.
|
||||
BANNER_DIM :: Color.{ r = 6, g = 8, b = 14, a = 188 };
|
||||
BANNER_PANEL :: Color.{ r = 20, g = 24, b = 38, a = 240 };
|
||||
BANNER_WIN_TEXT :: Color.{ r = 120, g = 240, b = 150, a = 255 };
|
||||
BANNER_LOSE_TEXT :: Color.{ r = 255, g = 120, b = 110, a = 255 };
|
||||
BANNER_BTN :: Color.{ r = 64, g = 132, b = 224, a = 255 };
|
||||
BANNER_BTN_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||
BANNER_TITLE_FONT :f32: 52.0;
|
||||
BANNER_BTN_FONT :f32: 30.0;
|
||||
|
||||
// UV sub-rect of one gem column, spanning the sheet's full height.
|
||||
GemUV :: struct {
|
||||
uv_min: Point;
|
||||
@@ -181,6 +194,9 @@ BoardView :: struct {
|
||||
fxassets: *BoardFxAssets;
|
||||
motion: *GemMotion;
|
||||
safe: EdgeInsets;
|
||||
// Seed for `restart`: the same fixed seed main seeded the board with, so the
|
||||
// restart button reproduces the identical starting level.
|
||||
seed: s64;
|
||||
|
||||
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
|
||||
// event from the current frame so the hit-test matches what was drawn.
|
||||
@@ -430,6 +446,68 @@ BoardView :: struct {
|
||||
self.draw_gem(ctx, gf, cast(s64) g);
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the win/lose banner is up: the level is over AND any in-flight move
|
||||
// animation has settled, so a winning/losing cascade plays to completion
|
||||
// before the banner covers the board. Board input stays frozen the whole time
|
||||
// the level is terminal (see handle_event), independent of this.
|
||||
banner_up :: (self: *BoardView) -> bool {
|
||||
if level_status(self.board) == .in_progress { return false; }
|
||||
self.anim == null or !self.anim.active
|
||||
}
|
||||
|
||||
// Win/lose overlay (P7.2): dim the board, draw the centered panel, the
|
||||
// win/lose headline, and the restart button — all text + rects so colour and
|
||||
// alpha are honoured. The button rect comes from the shared BannerLayout, so
|
||||
// it sits exactly where handle_event hit-tests the restart tap.
|
||||
render_banner :: (self: *BoardView, ctx: *RenderContext, status: Status) {
|
||||
ctx.add_rect(self.layout.grid_frame(), BANNER_DIM);
|
||||
|
||||
bl := self.layout.banner();
|
||||
ctx.add_rounded_rect(bl.panel, BANNER_PANEL, 18.0);
|
||||
|
||||
title := if status == .won then "YOU WIN!" else "OUT OF MOVES";
|
||||
tcol := if status == .won then BANNER_WIN_TEXT else BANNER_LOSE_TEXT;
|
||||
tfont := fit_font(title, BANNER_TITLE_FONT, bl.title.size.width);
|
||||
tsz := measure_text(title, tfont);
|
||||
ctx.add_text(
|
||||
Frame.make(bl.title.mid_x() - tsz.width * 0.5, bl.title.mid_y() - tsz.height * 0.5, tsz.width, tsz.height),
|
||||
title, tfont, tcol
|
||||
);
|
||||
|
||||
ctx.add_rounded_rect(bl.button, BANNER_BTN, 12.0);
|
||||
btxt := "PLAY AGAIN";
|
||||
bfont := fit_font(btxt, BANNER_BTN_FONT, bl.button.size.width * 0.86);
|
||||
bsz := measure_text(btxt, bfont);
|
||||
ctx.add_text(
|
||||
Frame.make(bl.button.mid_x() - bsz.width * 0.5, bl.button.mid_y() - bsz.height * 0.5, bsz.width, bsz.height),
|
||||
btxt, bfont, BANNER_BTN_TEXT
|
||||
);
|
||||
}
|
||||
|
||||
// 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, 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Scale `base` font size down so `text` fits within `max_w` (measure_text scales
|
||||
// linearly with font size, so one division lands it). Never scales up — a short
|
||||
// headline keeps its size; only an over-wide one shrinks to fit the panel.
|
||||
fit_font :: (text: string, base: f32, max_w: f32) -> f32 {
|
||||
sz := measure_text(text, base);
|
||||
if sz.width <= max_w or sz.width <= 0.0 { return base; }
|
||||
base * max_w / sz.width
|
||||
}
|
||||
|
||||
impl View for BoardView {
|
||||
@@ -489,6 +567,13 @@ impl View for BoardView {
|
||||
// self-pruning, so they vanish once the move settles.
|
||||
self.render_fx_particles(ctx);
|
||||
self.render_fx_popups(ctx);
|
||||
|
||||
// 6. Win/lose banner over everything, once the level is over and the
|
||||
// final cascade has settled. Status comes from the model (P7.1); the
|
||||
// view never recomputes win/lose.
|
||||
if self.banner_up() {
|
||||
self.render_banner(ctx, level_status(self.board));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch input. A press records the drag start; the release resolves the
|
||||
@@ -500,6 +585,24 @@ impl View for BoardView {
|
||||
// behaviour: toggle the selection on the pressed cell, or clear it off-board.
|
||||
handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
|
||||
self.compute_layout(frame);
|
||||
|
||||
// A finished level (won/lost) freezes board input: swipes/taps on cells
|
||||
// are ignored. Status comes from the model (P7.1) — never recomputed
|
||||
// here. Once the banner is up its restart button is the only live target;
|
||||
// a tap inside it reseeds a fresh level through board.restart.
|
||||
if level_status(self.board) != .in_progress {
|
||||
if event.* == {
|
||||
case .mouse_down: (d) { return true; }
|
||||
case .mouse_up: (d) {
|
||||
if self.banner_up() and self.layout.banner().button.contains(d.position) {
|
||||
self.do_restart();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if event.* == {
|
||||
case .mouse_down: (d) {
|
||||
// Gate input at gesture START: while a move animation is in
|
||||
@@ -536,13 +639,13 @@ impl View for BoardView {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the HUD card — current score and remaining moves (out of the move limit)
|
||||
// — centered horizontally in the top of `avail`, the safe-area-inset region the
|
||||
// grid is centered in. Reads live model state, so it tracks score/moves as the
|
||||
// game progresses. A translucent panel sits behind the text for legibility over
|
||||
// the board art.
|
||||
// Draw the HUD card — current score against the per-level goal and the remaining
|
||||
// moves (out of the move limit) — centered horizontally in the top of `avail`,
|
||||
// the safe-area-inset region the grid is centered in. Reads live model state
|
||||
// (score, target_score, moves), so it tracks the goal progress as the game runs.
|
||||
// A translucent panel sits behind the text for legibility over the board art.
|
||||
render_hud :: (ctx: *RenderContext, board: *Board, avail: Frame) {
|
||||
score_str := format("SCORE {}", board.score);
|
||||
score_str := format("SCORE {} / {}", board.score, board.target_score);
|
||||
moves_str := format("MOVES {}/{}", board.moves_remaining(), board.move_limit);
|
||||
|
||||
score_sz := measure_text(score_str, HUD_FONT);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
BIN
goldens/p7_lose.png
Normal file
BIN
goldens/p7_lose.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
BIN
goldens/p7_restart.png
Normal file
BIN
goldens/p7_restart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
BIN
goldens/p7_win.png
Normal file
BIN
goldens/p7_win.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
14
main.sx
14
main.sx
@@ -82,7 +82,7 @@ g_anim_prev_active : bool = false;
|
||||
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
||||
// insets so the grid stays inside the notch / home-indicator region.
|
||||
build_ui :: () -> View {
|
||||
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, anim = g_anim, fx = g_fx, fxassets = g_fxassets, motion = g_motion, safe = g_safe_insets }
|
||||
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, anim = g_anim, fx = g_fx, fxassets = g_fxassets, motion = g_motion, safe = g_safe_insets, seed = BOARD_SEED }
|
||||
}
|
||||
|
||||
// Deterministic capture (P6.3). The idle loop is always-on, so a live screenshot
|
||||
@@ -300,6 +300,18 @@ main :: () -> void {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// makes the fresh board read WON immediately (score 0 ≥ goal 0);
|
||||
// M3TE_MOVE_LIMIT=0 makes it read LOST (budget spent below the goal). With
|
||||
// M3TE_RESTART set non-zero the board is then restart()-ed, capturing the
|
||||
// fresh in_progress board the restart button produces.
|
||||
if tg := read_env("M3TE_TARGET") { g_board.target_score = parse_s64(tg); }
|
||||
if ml := read_env("M3TE_MOVE_LIMIT") { g_board.move_limit = parse_s64(ml); }
|
||||
if rs := read_env("M3TE_RESTART") {
|
||||
if parse_s64(rs) != 0 { g_board.restart(BOARD_SEED); }
|
||||
}
|
||||
|
||||
g_pipeline.set_body(closure(build_ui));
|
||||
|
||||
g_plat.run_frame_loop(closure(frame));
|
||||
|
||||
81
tests/banner_layout.sx
Normal file
81
tests/banner_layout.sx
Normal file
@@ -0,0 +1,81 @@
|
||||
// Banner / restart hit-test golden (P7.2): lock the win/lose banner geometry and
|
||||
// the restart-button hit-test that `BoardView.handle_event` relies on. The button
|
||||
// rect is derived from the SAME grid layout the gems use, and a finished level
|
||||
// freezes every board-cell tap, so the button is the only live target — if its
|
||||
// rect drifted from what `render_banner` draws, a tap on the visible button would
|
||||
// miss and the level could never be restarted. This test checks, headlessly:
|
||||
//
|
||||
// * the button is centered over the grid and sits fully inside the panel,
|
||||
// * the button centre hit-tests INTO the button (tap → restart),
|
||||
// * an off-button tap (a board corner) hit-tests OUT (frozen, no restart).
|
||||
//
|
||||
// Imports BoardLayout (no GL/stb), not BoardView, so it links headless — same
|
||||
// shape and rationale as tests/hit_test.sx. Failure is signalled via a non-zero
|
||||
// exit code (the runner checks exit code AND stdout).
|
||||
#import "modules/std.sx";
|
||||
#import "board.sx";
|
||||
#import "board_layout.sx";
|
||||
|
||||
irect :: (f: Frame) -> string {
|
||||
format("({},{},{},{})",
|
||||
cast(s64) f.origin.x, cast(s64) f.origin.y,
|
||||
cast(s64) f.size.width, cast(s64) f.size.height)
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
// 800×600, no safe inset → 600px square grid, cell 75, origin (100,0): the
|
||||
// same layout tests/hit_test.sx pins, so the numbers are checkable by hand.
|
||||
lay : BoardLayout = ---;
|
||||
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
|
||||
|
||||
grid := lay.grid_frame();
|
||||
bl := lay.banner();
|
||||
print("grid {}\n", irect(grid));
|
||||
print("panel {}\n", irect(bl.panel));
|
||||
print("title {}\n", irect(bl.title));
|
||||
print("button {}\n", irect(bl.button));
|
||||
|
||||
fails : s64 = 0;
|
||||
|
||||
// The button is horizontally centered on the grid (centred banner).
|
||||
bcx := bl.button.mid_x();
|
||||
if cast(s64) bcx != cast(s64) grid.mid_x() { fails += 1; }
|
||||
print("button mid_x {} grid mid_x {}\n", cast(s64) bcx, cast(s64) grid.mid_x());
|
||||
|
||||
// The whole button sits inside the panel — its four corners are contained,
|
||||
// so it can never spill outside the drawn card.
|
||||
bx0 := bl.button.origin.x; by0 := bl.button.origin.y;
|
||||
bx1 := bl.button.max_x(); by1 := bl.button.max_y();
|
||||
corners_in : s64 = 0;
|
||||
if bl.panel.contains(Point.{ x = bx0, y = by0 }) { corners_in += 1; }
|
||||
if bl.panel.contains(Point.{ x = bx1, y = by0 }) { corners_in += 1; }
|
||||
if bl.panel.contains(Point.{ x = bx0, y = by1 }) { corners_in += 1; }
|
||||
if bl.panel.contains(Point.{ x = bx1, y = by1 }) { corners_in += 1; }
|
||||
if corners_in != 4 { fails += 1; }
|
||||
print("button corners inside panel: {}/4\n", corners_in);
|
||||
|
||||
// A tap on the button centre restarts (hit-test true); the panel itself is
|
||||
// also contained, so the centre is unambiguously on the button.
|
||||
center := Point.{ x = bl.button.mid_x(), y = bl.button.mid_y() };
|
||||
hit_center := bl.button.contains(center);
|
||||
if !hit_center { fails += 1; }
|
||||
print("button center hit: {}\n", hit_center);
|
||||
|
||||
// Off-button taps that a finished level must NOT treat as restart: the grid's
|
||||
// top-left corner cell centre, and a point just outside the panel. Neither is
|
||||
// in the button, so each leaves the level frozen.
|
||||
corner_cell := Point.{ x = grid.origin.x + lay.cell_size * 0.5, y = grid.origin.y + lay.cell_size * 0.5 };
|
||||
outside := Point.{ x = bl.panel.origin.x - 5.0, y = bl.panel.mid_y() };
|
||||
off_hits : s64 = 0;
|
||||
if bl.button.contains(corner_cell) { off_hits += 1; }
|
||||
if bl.button.contains(outside) { off_hits += 1; }
|
||||
if off_hits != 0 { fails += 1; }
|
||||
print("off-button taps that hit the button: {}\n", off_hits);
|
||||
|
||||
if fails == 0 {
|
||||
print("ok: restart button hit-test matches the drawn banner\n");
|
||||
return 0;
|
||||
}
|
||||
print("FAIL: {} banner hit-test checks failed\n", fails);
|
||||
return 1;
|
||||
}
|
||||
1
tests/expected/banner_layout.exit
Normal file
1
tests/expected/banner_layout.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
9
tests/expected/banner_layout.stdout
Normal file
9
tests/expected/banner_layout.stdout
Normal file
@@ -0,0 +1,9 @@
|
||||
grid (100,0,600,600)
|
||||
panel (148,168,503,264)
|
||||
title (148,215,503,79)
|
||||
button (248,326,302,63)
|
||||
button mid_x 400 grid mid_x 400
|
||||
button corners inside panel: 4/4
|
||||
button center hit: true
|
||||
off-button taps that hit the button: 0
|
||||
ok: restart button hit-test matches the drawn banner
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user