4194f3e150381c4c69bd0ae865e090bdbe97b34d
26
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4194f3e150 |
Share a link that plays the game without the editor (#266)
A project could only be played inside the editor, behind the login.
To show a game to someone else there was no way short of an account.
A release freezes the saved project under a random token and serves
it on a page that holds nothing but the game view.
- Menu -> Release... opens a dialog with the state of the release,
the link with Copy link and Open, Publish or Update release, and
Take down behind a confirmation.
- Publish saves first and refuses while anything is unsaved, like
snapshots. Updating keeps the link. Take down kills it, and a new
Publish hands out a new token.
- The link is play.html?game=<32 hex>. The page needs no session,
paints Theme.colors.background, zooms with Auto and waits for
"Click to play" so that sound and keyboard focus are allowed.
- The game always starts in the start scene. Persistent variables
are stored per link in the browser. Plugins are not loaded, and the
dialog warns about them and about a project without a scene.
- GET ?r=releases/{token} is the only route besides auth/* that works
without a login. The public answer carries the token as project.id,
so the project id stays hidden. Release files are written with 0640
and nginx does not serve them directly (measured 403).
- Starting, the frame and Load scene moved into runtime/GameRun.ts.
Play in the editor and the play page run through the same code.
- WorldViewport moved to src/viewport/ and no longer needs the
EditorState. Pointer forwarding (gamePointer.ts) and setting up
textures, fonts and sounds (projectFiles.ts) exist only once.
- Vite builds two pages. The play page loads 4 kB of its own code
plus the shared chunk (Pixi, runtime, drawing, 653 kB) and Rapier,
without the editor.
Found on the way, not fixed here: data/projects/<id>.json answers 200
without a login because the files are 0644.
Tests:
- ./check.sh: 194 files, 2808 tests, exit 0.
- tests/php/releases.test.php: 20 checks, also added to CI.
- New e2e module 60 with 10 checks, played in a browser without a
session. Module 46 counts the menu entries and was updated.
- Full e2e run: 1572 checks passed.
- Counter-checks: 3 unit, 2 PHP and 3 browser mutations all fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
94c1055895 |
Throttle what the editor shows while a game runs (#265)
With the behaviors fixed, the scene Performance test still ran at about 23 fps with 500 entities. The rest of the frame went to the editor: every spawn or destroy rebuilt the hierarchy with all its rows, redrew the scene view behind the game tab and refreshed every panel. The game itself is cheap. What the panels show of it now follows a preset in the project settings. - New section While playing in the project settings with three presets: Live, Balanced and Minimal. Every project reads as Balanced, including those saved before the setting existed. - Live keeps today's behavior. Balanced brings hierarchy and panel values up to date 4 times a second and stops redrawing the hidden scene view. Minimal updates hierarchy and values only on pause and throttles the graph tabs as well. - The graph and state machine tabs stay live under Balanced. At 4 updates a second the beads on exec edges stutter, and the tabs draw only while on screen. This differs from the issue proposal. - The game itself is never throttled. Pause, step and stop bring every panel up to date at once, and a change of structure that falls between two updates is kept for the next one. - The rule lives in editor/playThrottle.ts as panelPolicy and PanelClock. PlaySession asks it after every frame. - Objects spawned at the top level go into a hierarchy group named after their asset, so hundreds of gold pieces fold into one row. Groups start folded. Measured on a throwaway copy of Performance test, 3 s each, fps: - About 500 entities: Live 38.6, Balanced 56, Minimal 60.6. - About 315 entities: Live 56.9, Balanced 60.5, Minimal 60.6. - p95 frame time at 500 entities: 60 ms, 41 ms and 23 ms. Tests: - ./check.sh: 193 files, 2799 tests, CHECK EXIT 0. - GM_STEPS=1,18,59: ALL PASSED, 54 checks. - Full e2e run (local): ALL PASSED, 1562 checks. - tests/editor/play-throttle.test.ts: presets, update rates, kept structure changes, flush, spawn group and loading old projects. - New e2e module 59: preset chosen with the mouse, measured update rates, scene view behind the game tab, Minimal with pause. - Module 03 now expects spawned objects in their group row after the next update. - Counter-checks: 4 in unit tests and 2 in the browser, each failing as expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5fbc6eb088 |
Set the order in which event nodes run across behaviors (#264)
Which of two events ran first depended on hidden lists: the order of the behavior instances and the order in which nodes were created. A graph had no way to say that one On update must run before another, and an On update of one behavior could run before a key event of another. - A frame now runs event kind by event kind over all behaviors: On disable and On enable, key down, key up, key held, timer, collision enter, stay and exit, animation finished, click, mouse wheel and at last On update. - All 17 event nodes have a field Order, a whole number, default 0, negative allowed. Within one kind a lower Order runs earlier, no matter which behavior holds the node. - On a tie the current hierarchy decides from top to bottom, then the order in which the nodes were created. The old rule that a behavior enabled later goes to the end is gone. - The node shows Order: 5 only when the value is not 0, so existing graphs look the same. - The rule lives as pure functions in core/graph/eventOrder.ts. The interpreter can now fire a single event node. Changes in existing projects: - An object destroyed in a frame gets no further event in that frame. On disable and On destroy still come at the end of the frame. - New instances get all their On start first, then all On enable. Teardown runs all On disable first, then all On destroy. - State events follow right after the event kind that caused them. - Four unit tests had fixed the old order and are adjusted: two in behavior-events, one in physics-system, one in key-repeat. Tests: - ./check.sh: 191 files, 2784 tests, CHECK EXIT 0. - GM_STEPS=1-6: ALL PASSED, 280 checks. - Full e2e run (local): ALL PASSED, 1551 checks, measured. - tests/core/event-order.test.ts: 11 tests for the rule, the field on all 17 nodes, order across behaviors, key before update, two nodes in one graph, On start, hierarchy after late enable and reorder at runtime, state events, save and load. - e2e 6z-viii, 2 checks with real mouse and keyboard: Order -1 set in the inspector shows on the node, and a run logs B, A although B is lower in the hierarchy. - Counter-checks: 5 in unit tests and 1 in the browser, all fail as expected. The originals were restored and checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7979b8ff19 |
Let structs and lists hold objects across behavior runs (#238)
An object could only live in a dedicated object variable, kept in slots
beside the variable bag. Struct fields and list items could not hold
one, so a spawned object packed into a struct or list was lost. This
replaces the slots with one mechanism for all three places.
- "No object" has one resting form everywhere: {"$object": ""}. A
running game can tell an object slot from an empty text that way.
- During play the object handle lives directly in the value, also
nested in structs and lists. objectSlots.ts and the special paths for
Keep and Properties are gone.
- liveObjects.ts strips objects to (null) at every exit: overrides the
editor run writes back to the scene, saved values of persistent
Variables assets, live values in the inspector, and the world
snapshot of the test hooks (objects as names there).
- A destroyed object reads as (null) through isAlive, alone, in a
struct field or as a list item.
- "+ Field" and "+ Item" offer Object. The inspector and the list tab
show a fixed (null) row without input.
- Lists from Raycast all stay objects when written to a variable.
Before they turned into names.
- A struct that only changes by an object writes no override. A list
that grows by an object writes [(null)], its length really changed.
Found on the way: CI at
|
||
|
|
d257298bc7 |
Keep a dragged rigidbody on the mouse while the game plays
Dragging a sprite with a dynamic rigidbody in the scene view during play barely moved it. Only a quick drag and release got through. A dragged object should follow the mouse as if it had no rigidbody and start from rest when it is let go. - The scene view reports the dragged objects as held through EditorState. The game tick no longer writes its own position over a drag position that has not been read in yet. This affected every object in play, not only rigidbodies. - A held dynamic body turns kinematic and is set straight to the drag target. Gravity and velocity no longer pull it away between two mouse moves, and the "moved directly" warning stays quiet. - A body counts as held when the object itself, an object above it or its rigidbody is dragged. - On release the body turns dynamic again with linear and angular velocity set to 0. Not changed: a press on an unselected crate still lands on its same-sized collider (rule from #203). Dragging while play is paused and in the editor run is untouched. Tests: - ./check.sh: 155 files, 2273 tests, CHECK EXIT 0 (before 2271). - e2e 41 alone: 21 checks. e2e 1-16,39-43: 641 checks, all passed. - 2 unit tests: a held body stays where the drag puts it and starts from rest, also for a grip shorter than one physics step. The tick does not overwrite a drag position that is not read in yet. - 1 e2e check in 41b: the crate is lifted 150 units with the real mouse, holds exactly at -3490 for a second, then falls from rest (1.3 px, then 5.1 px) with an empty log. - Counter-checks: without holding in physics the crate falls back to the ground and the old warning shows. With the tick writing back the unit test fails. Without zeroing, a short grip keeps 490 px/s and the test fails. - Check total 1356 + 1 = 1357, calculated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
41772e53f7 |
Variables assets can keep their values from one run to the next (#166)
A game needs a high score or an unlocked level to survive the end of
a run. As agreed in the issue there are no save or load nodes. A
Variables asset gets a switch instead, and the existing Variables
nodes read and write the values as before.
- A "Persistent" checkbox in the Variables inspector. Toggling it is
one undo step, and absent means not persistent.
- The behavior system loads the saved values at start and writes them
after every frame that changed one, so a closed tab loses nothing.
- Values live in localStorage per project, not in the project file.
Another browser or person starts fresh.
- Play and "Run in editor" save apart, so trying a behavior in the
editor leaves the Play save alone.
- Saved values are read through the current declaration. A removed
variable drops out, a new one starts from its default, and a changed
kind is converted.
- "Clear saved values" in the inspector clears this asset in both
areas. The main menu gets "Clear saved data" for the whole project.
Clearing takes effect from the next run on.
Decisions:
- The switch covers the whole asset, not single variables. Mixing
both means two assets.
- A changed default in the inspector shows only after the save is
cleared. That is what persistent means.
Findings:
- The first e2e run died at "debug hook present" right after a Vulkan
error from Chrome. The second run passed, so it was a GPU flake at
start-up.
Tests:
- ./check.sh: 140 files, 2070 tests, exit 0 (before 138 / 2055).
- 15 new unit tests in tests/core/saved-values.test.ts and
tests/editor/saves.test.ts.
- GM_STEPS=32,46 fresh: ALL PASSED, 24 checks. Six new checks in
module 32 go through the UI. Module 46 knows the new menu entry.
- Counter-checks both fail as expected. Loading switched off: 2 unit
tests and 2 e2e checks fail. Editor run writing into the Play area:
1 e2e check fails.
- Check total 1278 (1272 measured by CI at
|
||
|
|
3427f84a55 |
A game can pause itself and set its own time scale (#167)
Until now only the editor toolbar could pause a running game. A pause menu inside the game had no way to stop the world while its own graph kept running, and there was no slow motion. - Five new nodes: Pause game, Resume game, Is game paused, Set time scale and Time scale. Pause and resume take effect on the next frame. - A game pause stops world time, physics, animations, particles and shader time. Only behaviors with the new "Run while paused" switch keep running. - Set time scale 0 freezes the world but graphs keep running, as agreed in the issue. - Play and Stop reset the time scale to 1, Load scene keeps it. The pause belongs to the scene, so Load scene and Restart unpause. - The toolbar pause stays separate and also freezes the menu. When the game pauses itself the status line reads "The game paused itself". - While paused, collision events are not reported, sounds keep playing, and the editor run ignores both nodes with a log line. Findings: - The first status line was longer than any before and got cut with an ellipsis. It is shorter now. - The e2e harness did not know the key "p". It does now. Tests: - ./check.sh: 133 files, 1968 tests, exit 0 (before 132 / 1952) - npm run build: exit 0 - GM_STEPS=18 fresh: ALL PASSED, 33 checks (before 26) - 16 new unit tests: 8 catalog cases, game-pause.test.ts, editor run - 7 new e2e checks in block 18i, driven by real keys in the game - Six counter-checks all bite: behavior filter, collision lock, world time, editor run, clock reset and the game clock in the browser - Check total 1217 in README is computed (1210 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78899b1ace |
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
193378e6a9 |
An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes. - AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one property of the root or of an object inside it, keys sit on whole frames. - The last key decides the length (its frame counts as a frame of its own). The playhead runs between frames, so numbers and colors blend every tick even at 10 fps; everything else holds. - Playback gains `once-reset`, which puts back what was there before the start. A row can be relative, adding to where its target stood. - Play / Pause / Stop animation act on an object; each instance plays on its own, the later start wins on a shared property, and a behavior writing the same property still writes last. - A sprite no longer wears an animation through `texture`. "Create animation from texture" builds a sprite plus a texture row keyed frame by frame instead. - The Animation tab shows a placeholder until the timeline editor (part b) lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e9d21cd5d |
A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays. |
||
|
|
5671a38044 |
A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter. |
||
|
|
7babf88636 |
A state change is something the graph can react to, so the machine announces it (#53, #54, #55, #57)
game-maker / suite (push) Successful in 10m58s
Four issues that all reach for the same line: how the current state is read and written. Splitting them into four commits would mean four states of the tree that do not compile, so they arrive together. #55 start state — the machine asset carries an optional `start` holding a State node's id. Right-click a state to set or remove it, a ring in the node's title says which one it is. A dead id (the state was deleted) reads as "no start", so nothing has to clean up after a deletion. #53 entered/exited — `On state entered` and `On state exited`, one exec output per state. Every change now goes through a single `setState` that reports it; BehaviorSystem delivers it after the exec flow that caused it, `exited` of the old before `entered` of the new, round after round until the queue is empty, with a cap that stops a ping-pong graph with an endless-loop error. A local machine tells the instance that switched, a global one tells every instance holding a matching event node. The start state fires `entered` too: per instance for a local machine, once per run for a global one. #57 reading — `Current state` with `name` for showing and `id` for comparing, `Is in state` with one boolean per state. Neither has a `run`, so `read` learned `def` and `lookup`, and the catalog test knows this third kind of node. #54 seeing it — during a run the state machine tab marks every state some instance is holding faintly, and the one held by the selected scene object's instance strongly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8143130ed2 |
A shared value belongs to no behavior, so it becomes an asset of its own (#91)
game-maker / suite (push) Failing after 8m59s
This reverses the design of the previous commit. `PropertyDef.scope` is gone without replacement: a behavior variable is an object property again, without exception, and sharing is something you point at rather than something you declare twice. A **Variables asset** is declarations and nothing else -- no graph, no position, never placed, no tab of its own (`TAB_OF_ASSET` is already `Partial<>`, so that costs no line). A `Variables` node names one through its new `variablesAsset` setting, empty meaning the behavior's own variables as before, and then reads and writes one value per key for the whole run. Its property list follows the chosen source, which was the actual request: a value another graph declared is now offerable, where before every participant had to declare the name itself. That deletes more than it adds (-72 lines net): `globalConflicts` and its run-stopping kind clash, `SetVariableScopeCommand`, the scope picker, both object-inspector filters and the "shared by the whole run" note on the placed object. A typo is no longer a silent second variable -- naming an asset is picking from a list. And two Variables assets may carry the same key without meeting, because the asset is the key, not the name. The one shape it copies is the one that already existed: `Set state` picks its machine the same way. Lifetime is unchanged. The bag is built at `BehaviorSystem.start()` from the declarations and never written back, so playing in the editor cannot change the project -- which is the whole reason a counter must not be an object property (`World.syncToModel` would `commit()` it, and the coin counter would stand at 7 for good). Full symmetry across that gap, without mirroring anything. Two named wires: `Inspector.liveVariables` shows the values as they stand while a run is on and falls back to the declarations when it ends, and `adoptDeclarations()` takes a key into the run *only where the document changed it since the last sync*. Without that condition any unrelated project edit would snap a running counter back to its starting value; a counter-test bends exactly that and falls over. Nothing was ever cached, incidentally: `GraphInterpreter` only pulse-caches nodes without a `read`, and the variable node has one. The wire was missing. The state machine `Scope` row is now an ordinary `enum` row through `createFieldRow`. It was hand-built from an `HStack` with a ghost dropdown and began ~34px left of every value above it; the explanations beside `Local` and `Global` are gone from the menu, the sentence below the row keeps saying it. Machine scope is otherwise untouched -- `StateMachineAsset.scope`, `SetMachineScopeCommand`, `stateBag()` and its persistence all stand, so #59 stays closed as built and `Scope` as a type now carries machines alone. Six counter-proofs, each biting at its own guard. One of them did not bite at first: the suggestion list was computed twice, in `nodeSection` and `portSection`, so only the browser test covered the request itself. It is one named function now, `variableChoices`, with a unit test of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d1fcfe8b81 |
A shared value belongs to the run, so a declaration carries its scope (#91)
A variable or a state machine declared `global` keeps one value for the whole run instead of one per behavior instance. The scope sits on the declaration -- `PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence meaning `local` -- and `local` is never written, so every project made before this saves back unchanged. **The name is the key.** Two behaviors declaring a global `score` mean one variable; a behavior declaring `score` locally keeps its own. Kinds that disagree about one global name stop the run before it starts, with the two behaviors named; starting values that disagree are a warning on the behavior that declares it, and the first declaration wins (`globalConflicts`). The runtime needs no redirection layer. A global variable falls out of `schemaForObject`, so it never reaches `entity.props` -- and then the whole rule is "whoever declares the name takes their own value" (`graph/sharedState.ts`). The `Variables` node is untouched and stays transparent: switching a variable to global breaks no existing graph, because `GraphInterpreter.readAll` builds a fresh object per read anyway and only asks `port.key in all`. That same cut answers the inspector. A shared value has no owner, so the placed object shows no row for it and can hold no override -- `SetVariableScopeCommand` takes the deviations that were there and undo gives them back. Both places say it in words rather than leaving a glyph to be guessed: the declaration explains the mark below the list, and the placed object names the variable it no longer holds instead of claiming the behavior declares none -- which is what it said before, untruthfully. Screenshots of the e2e run turned that up; no check had been looking at it. `SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from the *declarations* rather than from any instance (a global variable exists before its behavior is placed), and is thrown away with the run: nothing is written back, not even in the editor run. `GraphInterpreter.call` passes it on through `...ctx` while nested calls keep getting fresh local variables, which is why `program.defaults` is stripped of global keys. Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or the inspector does not rebuild after the switch -- and a state machine has no rows at all, so its scope is appended to the signature by hand. And the object inspector does not go through `schemaForObject` for behaviors, it reads `asset.variables` directly; filtering in one place was not enough, and no unit test covered that path. The e2e caught it (`global: true` where false belonged), and the rule now lives in one named function, `objectRows`. Scopes `object` and `scene` are deliberately not built. The object case already has a route (`Object by name` -> `Properties`), and a run knows exactly one scene, so `scene` would be indistinguishable from `global` until #39 exists. Closes #59: a machine's scope is a word on the machine asset, not a setting per `Set state` node, so the same machine cannot live in two places at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c47fd5e4dc |
Console panel: capture console.*, timestamped lines, resizable (#50)
A log layer (src/editor/console/log.ts) keeps a 500-line ring buffer and
wraps console.log/info/debug/warn/error, still calling the original. Notices
and behaviour runtime errors go straight to the layer instead of through
console, so editor tests stay quiet and errors can render red. world.log is
untouched: it is per-run structured state that runtime tests and e2e read
directly, not a text stream.
The panel sits under the visible tab, resizable with Splitter('y'), with a
CONSOLE header and a clear button. Each line carries its wall-clock time in
its own dimmed column, so locate() still finds the message text.
The taller centre area drops the graph fit zoom to 0.60, which broke six e2e
checks that assumed one screen pixel is one graph unit and that a fixed
offset from the canvas centre is empty. They now ask __gm.emptySpot(),
__gm.graph.emptySpot() and __gm.graph.zoom() instead of guessing.
locate/locateAll added the unscaled Pixi text width to the global position,
so click points in the scaled graph canvas landed too far right; they use
getBounds() now.
check.sh: 831 tests. Full e2e: ALL PASSED - 671 checks. Smokes 24/19/20.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
8f92a06c84 |
Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus; the port and marker menus were the same list and are now one function. Value specs of the inspector and the list tab share valueSpecs.ts, and the remaining copies (compound commands, type glyphs, typed asset lookup, name rule, play/editor world setup, marquee) are merged. Four copies had already drifted apart; merging them fixes: - "Disconnect N links" in the port menu is one undo step, not N - "+ Add collider" is hidden on a placed container instance, as in the hierarchy menu, instead of always failing - a bool property coerced from the text "false" becomes false - a port command on an unknown node type reports instead of crashing Comment-only edits leave the bundle byte-identical. README corrected in 35 places (step limit, jump-run clock, suite duration, 409 body, ...). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
11981b40d0 |
Give the three asset tabs one base, and move the viewports into the editor
src/viewport/ held three classes only src/editor/ ever used, and framing.ts sat under graph/ although the scene view wants the same bounds sum. The graph, the texture and the container tab repeated the same header, subscription and disposal code with two silent divergences: the graph tab said "nothing shown" with null instead of '', and the texture tab refell back to the first asset on a selection change but not on a project event. These arrive together because GraphCanvas ties them: it reads framing from its new home, hands its fold marks to folding.ts, and its '' change is what GraphEditor must agree with. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9c4148dc1b |
Hold a running game, and let the editor run reach into containers
Pause is a sub-state of play, not a third mode: the toolbar button and Ctrl+Enter stop ticking the world, the clock included. A held frame still drops the input edges, so resuming does not fire a pause worth of keys in one go. The editor run missed the case it is most wanted for. hasEditorRun only looked at the stored scene objects, so a behavior inside a placed container never started one; the world now builds from resolveObjects and writes back through objectRef, keeping the move as an instance edit instead of baking the instance out. Keys and clicks go to the run while the Game tab is on screen, where no key means anything to the editor, and are handed back on leaving it - held keys released, so nothing keeps walking unpressed. |
||
|
|
8f31afac37 |
Let a behavior run while the editor is open
A behavior object carries a "Run in editor" switch next to Enabled. When it is on, the behavior ticks outside of Play and works on the saved model directly - Unity semantics: what it moves is a real change, autosaved and not undoable. Input stays with the editor, so On key never fires while typing. The editor run never bakes placed containers into the model, since that would silently drop their instanceEdits. syncToModel reports whether it wrote anything, and the dirty flag is only raised on change - otherwise a behavior moving something every frame would trigger ~60 saves a second. |
||
|
|
37fff5dabf |
Play through the first camera that is switched on
The game view took whatever camera came first in the scene, ignoring the switch, so `enabled = false` on a camera did nothing and a second camera never got its turn. `World.activeCamera()` now decides: of the cameras switched on – the whole parent chain counted, as for any object – the first in hierarchy order, and none on renders nothing at all. It is asked per frame, so a graph switching cameras hands the view over immediately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d84231b9f |
Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners and the notes that carry a reason, a measured fact or an external contract. ButtonOptions.width claimed to be a fixed width but only set minWidth, so the Graph and Texture pickers grew with the name they showed. It is now what it promises, with a separate minWidth for the buttons that have two forms. Narrow buttons shrink their padding, otherwise a 24px icon button had 8px left for its glyph and truncate dropped it entirely. __gm.labelsDropped() guards that class: the overflow guard only saw text that was too wide, never a label truncated to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f16801aef3 |
Add a decimals option, fix dropdown overflow and the texture filter
- NumberField takes a decimals option (default 3); frame rects and the slice dialog use 0, matching their integer model. - Button truncates its label to the drawn width; Dropdown reserves the chevron slot. NodeView reuses the shared truncate(). - Setting scaleMode leaves the style's shared resource id in place, so the old GPU sampler stayed in use until a reload. Call style.update(). - Graph gets a header bar like the Texture tab: GraphEditor is now the frame, the former editor is GraphCanvas. A selection the tab does not concern no longer unloads the behavior. - New e2e step 14 guards text overflow across all widgets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d54a620ebd |
Add ESLint and Prettier, and close the node test gaps
Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f140c2fd77 |
Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |