Commit Graph
6 Commits
Author SHA1 Message Date
admin 7e5003b590 A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.

The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.

Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.

564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.

Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
2026-09-16 08:08:10 +02:00
rootandClaude Opus 5 0e0a21dee1 One clock under everything that moves by itself (#90)
game-maker / suite (push) Successful in 10m4s
Particle age and shader time each read performance.now() off their own
library, and the world got the frame's delta straight from the ticker. Three
clocks, so a global time scale would have been three multiplications and a
pause three separate freezes.

core/time.ts holds one clock instead: it accumulates dt x scale per frame
rather than reading the wall clock, and Editor.tick hands the very step it
took to the play session, the editor run and the animation preview. Delta
time, particle age, shader time and that preview are the same quantity now,
so the scale is a single multiplication and a pause is that scale at 0 —
nothing underneath needs rebasing, because the clock itself stands still.
The Math.min(dt, 0.1) caps in PlaySession and EditorRun are gone: the cap
(MAX_FRAME) now sits on the raw frame before scaling, and capping twice would
make the world run slower than the particles at scale 2.

A pause gates the scale rather than overwriting it — effectiveScale() answers
0 while paused, and for anything that is not a positive finite number, while
gameClock.scale keeps its value. A slider later survives a pause that way;
the issue asks for none yet.

The clock goes back to zero at both ends of a run, so a run is reproducible
from second 0 and the scene view looks freshly loaded after Stop. Resetting
only on Stop would not do it: the editor clock runs on afterwards, and the
next Play would start at "seconds since the last Stop".

The trap in that: ParticleField keeps its origin as an absolute second of
this clock, so after a reset it lies in the future and every particle would
age backwards. One line in sync() starts such a field over, which covers the
scene, the game and the tab preview at once — and fixes an older bug of the
same shape, where releasing a particleTime() freeze left the preview empty
for good.

Found while measuring that: ParticlePreview stood at second 0.5 by computing
the clock reading that would mean 0.5 since the field started. Over 200000
random origins the round trip misses the last bit in 18.8% of them and shows
one particle too few in 9.4%, because floor(0.5 * 20) comes out 9. The check
had been flaky since it was written. The panel hands the field the age itself
now (ParticleField.hold), and the faked clock is gone.

833 checks (824 + 9), 931 unit tests (921 + 10).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 09:31:53 +02:00
rootandClaude Opus 5 4feb4c2cf6 A step opens a tab the way a person has to, through the menu when it is hidden
CI run 35 was red: the suite died after step 15 with `widget with text
"Container" not found`. Nothing in the editor broke — the tenth tab (#73) pushed
Container behind the `»` button at the test window's 1280 px, exactly the case
#84 was built for, and three steps still clicked the tab name directly.

openTab(t, tab) in tools/e2e/lib/scaffold.mjs asks tabRow() whether the tab is
hidden and goes through the menu when it is, so the fix lives in one place rather
than in every step that opens a tab. The LABELS record moves there as TAB_LABELS,
since both the helper and 14-overflow need it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 07:03:40 +02:00
rootandClaude Opus 5 05f7a2745e Frames: cut each frame into its own GPU texture, no more fill matrix (#47)
game-maker / suite (push) Successful in 8m18s
A frame drawn straight from the sheet showed a hairline of the neighbouring
cell along any edge that fell inside a screen pixel: with antialias on, a
partly covered pixel is still evaluated at its centre, past the edge. On top
of that Pixi rewrites a Graphics fill texture from clamp-to-edge to repeat at
draw time (generateTextureFillMatrix.mjs), so the top row of a sheet read its
bottom row.

TextureLibrary.get(ref) now cuts the rectangle out of the loaded sheet with an
OffscreenCanvas on first use and uploads the cut (key file|x,y,w,h, kept for
the session). Address mode is mirror-repeat: Pixi only rewrites clamp-to-edge,
and within a texel of the border mirror behaves like clamp, for linear as well
as nearest. Loaded sheets get it too, since whole pictures wrapped at their own
borders the same way. viewport/textureFill.ts and its test are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:25:49 +02:00
rootandClaude Fable 5.1 8a7185948d Make texture and colour full kinds and drop the load-time migrations
- `Texture` and `Color` can be picked as a variable kind, a struct field
  kind and a declared port kind; a new `Texture` node hands a picture id
  into the graph. Declared, a texture is its bare id – the `$texture` tag
  stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
  `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
  `migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 14:06:14 +02:00
rootandClaude Opus 5 1135bf4562 Play sprite animations from an animation asset worn through texture
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.

A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.

The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.

Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:52:34 +02:00