f14c4ecb77d08dab3cdef403a61afbdd37814334
267
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f14c4ecb77 |
Show the frame rate the measured times allow in the overlay (#275)
The fps line of the performance overlay stops at the display's rate, 60 on most screens. It does not tell how much room a game has left. The new top line answers that from the times the overlay measures. - possibleFps in src/editor/frameMeter.ts is 1000 ms over the sum of the five areas, rounded down. It has no number while nothing took any time, and the overlay shows a dash then. - The editor's share is in the sum, because it costs time while playing in the editor. A released game can do a little better. - It is an upper bound. The GPU's time is not measured and not in it. - e2e 59e reads the lines one further down and checks the new line against the times it measured itself. Tests: - Unit frame-meter: 11 passed (+2 for possibleFps) - Gate: 197 files, 2855 tests, CHECK EXIT 0 (2853 + 2) - e2e 59: ALL PASSED, 19 checks (+1), on melektaus-atx - Counter-check: possibleFps without the editor's share fails the unit test - The e2e total of 1602 is counted (1601 + 1), not measured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
77f4c2c1a3 |
Show entities and time per area while playing in the Game View (#275)
A game that stutters gave no hint where the frame goes. The Game tab now has a Performance switch. It shows a small overlay in the top left corner of the Game View while a game runs: fps, the number of entities and the mean milliseconds per frame for each area, averaged over half a second like the fps counter. - src/editor/frameMeter.ts holds the pure rule. FrameMeter sums the areas per frame, closes a window every 0.5 s and formats the lines. - World.timeSystem times every system the world ticks. PlaySession maps the systems to Behaviors, Physics and Animation and sets the hook again after Load scene. - Animation has its own line. Counting the AnimationSystem under Behaviors would have skewed that number. - Drawing is the build of the game picture plus app.render(). It is main thread time only. The GPU work after it is not in the number, and app.render() draws the whole canvas including the panels. - Editor is the rest of the editor tick after the systems and the game picture: mirroring into the model, panels and console. - The overlay lets clicks through to the game. It shows dashes until the first window closes. The switch is off by default and is not saved. The released game (#266) runs without a meter. Tests: - Unit frame-meter: 9 passed. - Gate on melektaus-atx: 197 files, 2853 tests, CHECK EXIT 0. - e2e 59 on melektaus-atx: ALL PASSED, 18 checks (13 before, +5 in 59e with a real mouse on the switch). Run twice. - Three mutations were caught: the editor share not subtracting the systems (2 unit tests), systems running in pause not reported (1 unit test), the overlay getting no times (59e). - The e2e total of 1601 is computed (1596 + 5), not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ded71a92c9 |
Pick the target of an object variable in the Scene View (#276)
Choosing the target of an object variable meant finding its name in a dropdown. With many objects, or several with the same sprite, the name alone does not say which one it is. The eyedropper lets you click the object where you see it instead. - Every object dropdown gets an eyedropper button next to it. This covers variables, struct fields and list entries, in the Inspector and in the Variables tab. - The button opens the Scene View or the container tab the object lives in. A strip above the view names the field and the current candidate and offers Take and Cancel. The cursor is a crosshair. - A click picks a candidate and writes nothing yet. The candidate gets its own cyan frame, the selection stays on the object with the variable. - Clicking follows the selection rule. `pickOn` in `src/viewport/pick.ts` uses `pressOn` and only counts objects the dropdown offers. A click on empty space keeps the candidate. - Enter or Take writes through the same handlers as the dropdown. That is one undo step, or none if nothing changes. - Esc, Cancel, a right click in the view or a second click on the button cancel without writing. So do a tab switch, another selection, undo, redo, play and loading a project. - After Take or Cancel the editor goes back to the tab the pick came from. - While the eyedropper runs, the view does not drag, marquee, paint, show handles or open its context menu. Panning and zooming stay. Found on the way: - Started from the Variables tab, the click missed the object. The Scene View is hidden while that tab is open, and the layout skips hidden children. The strip appeared then, the view moved down by 32 px but kept its old height. `Editor.display()` now lays the row out again after showing it. Tests (on melektaus-atx): - unit pick and object-pick: 27 passed, 13 new - gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from svg-icons, which checks every icon twice. - e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g) - Counter-checks work. Without the layout fix 57g fails. Picking objects the dropdown does not offer fails 2 tests. Writing on cancel fails 2 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6d4247ebf8 |
Paste where the view shows it, editor offsets included (#263)
Paste mixed two frames. The pointer comes from the Scene View, which draws every object moved by the editor offsets of it and its parents. Paste turned that point into a local position through the parents' plain world transform, so the copy landed beside the pointer by the offset of the parent it went into. Without a pointer, an original with an offset of its own had its copy placed beside its game position, which the view does not show. - A copied root now remembers where the view showed it (`ClipRoot.shown`, from `editorTransform`) instead of its world position. - New `editorLocalPositionFor` in `properties/transform.ts` turns a shown point into a local position through the parent chain with the editor offsets, as the view draws it. Paste uses it. - The copy itself still gets no editor offset of its own, as since #240. In the game it stands where it was drawn, minus the offsets of its parents. - Re-parenting under a parent with an offset still keeps the game position, so the object jumps in the view by that offset. That is a different rule from paste and is left as it is. Tests: - Unit (clipboard, groups, references, transform): 50 passed, 2 new. - e2e 51 on melektaus-atx: ALL PASSED, 12 checks, 1 new (51f). - Gate on melektaus-atx: 195 files, 2829 tests, CHECK EXIT 0. - Counter-checks: without the paste fix both new unit tests fail (copy at 540/470 instead of 500/500) and 51f fails by exactly the 60/-40 offset. With the root remembering its world position again, the own-offset unit test fails. - No full run. The e2e total of 1591 is 1590 plus 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9b5eeaab57 |
Empty a deleted tile's cells on every tilemap (#268)
Deleting a tile left the cells painted with it in the tilemaps. They drew nothing, blocked nothing and did not show in the delete dialog, but still read as a tile for "Tile at" and stopped a fill. - DeleteAssetCommand empties the cells of a deleted tile on every layer of every tilemap in the same step. Undo puts the tile and its cells back together. - The delete dialog counts them, for example "3 cell(s) on 1 tilemap(s) are painted with it and become empty." (cellsPainted). - presentTile treats a cell naming a missing tile as empty. "Tile at" (World.tileAt) and floodFill use it, so cells already in existing files read and fill as empty. There is no migration. - The brush, the rectangle and the fill check the tile in hand. A tile deleted after it was picked no longer paints new invisible cells, the editor says "Choose a tile to paint with." instead. - Unchanged on purpose: the picker still reports such a cell as naming a tile that no longer exists, and erasing removes it. Tests: - ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6). - GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t). - Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4). - Counter-checks, all fail as expected: 5 in unit tests (delete without emptying, undo without cells, fill and "Tile at" without presentTile, wrong map count) and 3 in the browser (dialog without count, delete without emptying, brush without the check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
14972d057f |
Refactoring round 8: hide data files, safe saves, Esc as Cancel (#85)
Recurring cleanup round over the code since round 7 (
|
||
|
|
bc688f3efc |
Refactoring round 7: split BehaviorSystem, fewer copies (#85)
Recurring cleanup round over the code since round 6 (
|
||
|
|
cab1c0177f |
Show only the game view in fullscreen, in the editor and the link (#267)
A game is easier to judge and to play without the editor around it. The Game tab and the game link now both have a button that puts the game view itself into fullscreen, not just the browser window. - The Game tab header has a fullscreen button on the right. It hides toolbar, panels, tab headers and console and keeps the zoom from the picker. A running game keeps running. - A corner button at the top right, or Esc, leaves fullscreen and puts the view back into the tab where it was. - The game link shows the same corner button. Over a running game it hides after 2 s without mouse movement and returns on the next move. - A click on the corner button never reaches the game. - Stop ends fullscreen. Stop switches back to the Scene tab, and any switch away from the Game tab ends fullscreen, so no empty screen is left behind. - src/viewport/fullscreen.ts wraps the Fullscreen API and holds the rule for when the button shows. FullscreenCorner.ts is the button used by both pages. Two SVG icons are new. - Editor.showGameFullscreen moves the GameViewport into the root while fullscreen is on. Tests: - ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests). - e2e 60: 2 new checks for the corner button on the game link. - e2e 61 (new): 4 checks for fullscreen in the editor. - Full e2e run (local): ALL PASSED, 1580 checks. - Counter-checks fail as expected: rule without "no game running", no stopPropagation (60h), no end on tab switch (61c), no reparenting of the view (61a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
24d360aeec |
Add zoom steps and a larger start caption to the game link (#266)
The released game only ever zoomed in whole steps up to 400%, so a small camera on a large screen stayed far smaller than it could be. The "Click to play" caption was drawn in the 11 px panel title size and was easy to miss. - GAME_SCALE_STEPS gains 300%, 500% and 600%. The game tab picker, Auto in the editor and the game link all read the same list. - The start caption takes its size from the new Theme.captionSize (16) instead of a constant in GameScreen. - The player page keeps the default arrow cursor at all times. It no longer switches to the hand over the caption. Tests: - view.test: Auto for 320x180 in 2000x2000 is now 6, with cases for 3 and 5. play-settings.test takes 2.5 and 8 as invalid steps. - e2e 60 checks the caption against the panel title size and the arrow cursor before and after the start. Module 03 follows the new step list. - ./check.sh 2808 tests, CHECK EXIT 0 (before the last Theme tweak, which changes no test). Full e2e run ALL PASSED, 1574 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
0c41cb97b1 |
Clip the children of an object to a clip area (#260)
An inventory grid could not scroll, because nothing could cut a child off at an edge. A game also had no way to hear the mouse wheel. Both are needed for a scrolling list, a dialog text or a window with more content than room. - A new object type Clip area has width and height and turns, scales and mirrors with its parent. The size gizmo drags width and height. - Everything below a clip area, on every level, is drawn only inside its rectangle, in the Scene view as in the game. Nested areas cut to their intersection. The rule works on a convex polygon, so a turned area cuts right as well. - On click, Is hovered and On mouse wheel do not hit a cut-off part. The Scene view does not pick it either. - In the Scene view the area is a dashed frame that is grabbed only at its edge, so a click inside lands on the content. The eye under Shape hides the frame. In the game the area is invisible. - While an object below an area or one of its parents is selected, the Scene view shows the cut-off part at 30 % opacity and it can be grabbed there. The rule lives in core/properties/clip.ts. - The Properties node knows the type Clip area. - The new event node On mouse wheel fires by the rule of On click. Its delta counts in notches, positive is down, and a frame's turns arrive as one value. It also gives x and y. Limits: - Only sprites and texts show the faded rest. Tilemaps and particles are cut and can be grabbed when open, but are not shown faded. - Masks by texture shape are out, as noted in the issue. Tests: - ./check.sh: 2773 tests, CHECK EXIT 0. - Full e2e run (local): ALL PASSED, 1549 checks, measured. - tests/core/clip-areas.test.ts: 12 tests for the cut rule, scene and game, save and load, hits, the wheel with and without owner, the notch conversion and the open rule. - e2e 58, 5 checks with real mouse and wheel: the scene cuts (74 of 74 red inside, 0 outside), a click on the cut part picks nothing, the selected rest is faded and grabbable, the game cuts, one notch logs 1. - e2e 04 and 46 count the object types and the eye rows. They now include Clip area. - Counter-checks: 6 in unit tests and 3 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> |
||
|
|
89aa92d2e3 |
Put a shader on a text and draw pixel fonts sharp (#169)
A text could not wear a shader, and a pixel font was always smoothed. Pixi's Text takes no custom shader. It also shares one texture between texts of the same content and style, and the filter is not part of that key, so two equal texts with different filters would overwrite each other. - TextPicture rasters a text into a texture of its own and draws it like a sprite. Shader, time animation and edge mode work without a special path. A new text, font or size rasters again and keeps the shader. - The old texture is dropped only after the shader holds the new one. Before, Pixi warned about a destroyed texture that was still bound. - A text has a Shader row with the shader's property rows below, as a sprite has. Overrides, saving, animation and the Properties node (type Text) know them. A shader parameter named like an own row of the text loses to that row, as on the particle source. - The font asset has a Filter row, Linear or Nearest. The row existed three times already (texture, vector art, pixel art) and is now one helper filterRow. - Nearest rasters the text 8 times finer and cuts every 8x8 block to one texel: at least half covered is full, else empty. Chrome on Windows smooths canvas text sideways through DirectWrite whatever filter we set later. The cut removes those half pixels on every system, and the texture stays one texel per world unit. Found on the way: - On a font that is no pixel font thin strokes break apart under Nearest. For a pixel font at its own size this does not matter. Not done: - The preview in the Shader tab does not take a selected text as its picture, it shows the test picture then. Tests: - ./check.sh: 189 files, 2759 tests, CHECK EXIT 0. - text-shader.test.ts with 10 tests: rows and name precedence, a clamped override, save and load, animation, the font filter, and sharpCoverage on a smoothed edge and at the texture border. - e2e 30 has 7 new checks: red glyphs through the shader picked with the mouse, the shader survives a new text, red in the game, soft edges under Linear, clean 4x4 blocks under Nearest, and half covered texels (Linear 238, Nearest 0). - Full local run before the Nearest cut: all passed, 1543 checks. GM_STEPS=1,30 on the final bundle: all passed, 32 checks. The sum 1544 in the README is counted from these two runs, not measured. - Counter-checks fail as expected: in the browser no shader on a text fails 30h-30j, an always linear sampler fails the Nearest check, the old 1:1 raster leaves 107 half covered texels and fails 30k-ii. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4e6aeb5f72 |
Query objects in bulk and filter, count or test a list (#259)
The object queries only ever returned one object: Object by name, Child by name and Find all stop at the first hit. Getting every coin, every child or every item that passes a test needed a For each with its own bookkeeping. These nodes return the whole set as a list of Object, so For each and Find can use item straight away. - Objects by name returns every scene object with that name in scene order. - Children by name returns every match below object, nearer levels first, the same order as the singular node. - Children returns the direct children, with recursive all of them level by level. - Objects of asset returns every instance of an object asset or container by the same rule as Is asset, spawned ones included. - All four take a snapshot. Destroyed objects are never in it, disabled ones are, as with the singular nodes. - Find has a Mode setting: First, All, Any and Count. First is the default and hides the row, so existing graphs look the same. - Match follows the mode of its paired Find: done with items and indices, done with any, or done with count. An unpaired Match shows First, since the pairing only comes from the wiring. - First and Any stop at the first hit, All and Count walk the list. - Changing the mode drops edges at outputs the Match no longer has, in one undo step. SetNodeParamCommand now cleans the whole graph, but only edges that were valid before the change. - The palette entry "filter" adds a Find set to All with its Match. Found on the way: - After adding the filter entry, "find" + Enter picked Filter. The entry has node type Find, counted as an exact hit, and the tie went to the alphabet. The search now ranks by the entry id. e2e 6z-v caught it. Tests: - ./check.sh: 188 files, 2746 tests, CHECK EXIT 0. - find-modes.test.ts with 11 tests, 11 catalog cases for the four nodes, the asset input guard now expects 3. - e2e 6z-vii with 3 checks: filter from the palette, Count picked in the inspector drops the items edge, a run logs 2, 2, 2, 3, 4, "0, 2". - GM_STEPS=1-6: all passed, 278 checks. - Counter-checks fail as expected: every mode stopping early (2), Match ignoring the mode (6), cleanup on the own node only (1), no recursion (2), no inOrder (2), old ranking (1). In the browser a Match ignoring the mode fails both editor checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f84defb9b6 |
Pick an object of the same scene for an Object variable (#258)
An Object variable could only be set by a graph while the game runs.
A placed object always showed "(null)", although the other objects of
its scene are there to choose from and the runtime can already find
any object by its id. Now the placed object stores a reference to one
of them, and the running game turns it into the real object.
- Stored as {"$object": "<id>"}. The empty placeholder stays as it
was, so old projects load unchanged and empty still means "(null)".
- The row of an Object variable on a placed object is a dropdown with
None and every object of the scene, indented like the Hierarchy.
The same goes for Object fields in structs and Object items in
lists, also in the Variables and List tabs.
- The behavior asset keeps "(null)". An asset has no scene, so "Apply
to asset" is gone from Object rows.
- The runtime resolves the reference when the Variables or Properties
node reads it. A destroyed target reads as null, a missing one shows
"(missing)" in the row.
- Inside a container only objects of the same container are offered.
The reference keeps the template id, so every placement finds its
own copy.
- Deleting the target clears the reference in the same undo step.
Paste points references inside the pasted group at the copies and
leaves outside references alone. Create container and Unpack carry
references along with the new ids.
- A value a graph writes while running stays in the run. The scene
keeps the picked object, also in "Run in editor".
Found on the way:
- Setting an override coerced every object slot to empty, in plain
values, struct fields and list items. The shared objectPlace() now
keeps the reference.
Not done:
- No eyedropper to pick in the scene, the dropdown is enough for now.
- Deleting a template object in the container editor does not clean up
references in other scenes. They read as null there.
Tests:
- ./check.sh: 187 files, 2724 tests, CHECK EXIT 0
- e2e 1-9, 51, 57: ALL PASSED, 389 checks
- e2e 1, 57 on the final bundle: ALL PASSED, 17 checks
- 13 unit tests in object-references.test.ts, 5 checks in e2e 57
- Counter-checks: 8 unit breaks fail their tests, the browser break
without a pick list fails 57b-57d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
e8b3ac0a3b |
Keep behavior variables private to their own graph (#257)
A behavior often needs working state such as a timer or a counter. Until now every variable showed up on each placed object and in the Properties menu, so the inspector filled up with values nobody should touch. A private variable works like any other inside its graph but is not offered anywhere outside. - Each behavior variable gets a lock switch next to Keep. It starts off, so existing projects stay as they are (`privateVariables`). - A placed object shows no private variable, neither in the inspector nor while the game runs. If all are private, it says so. - Switching a variable to private drops its overrides on every placed object in one undo step. Switching back does not restore them. - A running game does not write a private value back as an override, and a project change during the run does not reset it. - `+ Property` no longer offers private variables. A port that names one by hand still reads and writes it without a warning. - The default value of a private variable is read-only in the asset inspector. Its shape (name, fields, type, delete) stays editable. - Keep is now a pin icon instead of a word. With the lock added, the header of a list variable had no room left and the lock covered `+ Item`. Not done: the animation track picker still offers private variables. Tests: ./check.sh -> 186 files, 2711 tests, CHECK EXIT 0. e2e 1,19,20,21,56 -> ALL PASSED, 119 checks. New module 56 has 7 checks with a real mouse. Counter-checks fail as expected: unit without the write-back filter, the sync guard, dropping overrides or loading the flag. Browser with all variables on the object, an unfiltered menu, or no value lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
38e0f656be |
Split a struct output into one pin per field (#256)
Reading one field of a struct needed its own Expand node, even where the graph already knows the struct type. A split output shows the fields directly on the node, so a Find … Match test becomes a single Equals and the wires keep running left to right. - Right-clicking an output pin offers "Split into <Type> fields" when the type is known and "Split as …" for every other struct asset. The output is replaced by one pin per field with the field's kind. - The type is known for typed struct variables, typed Pack/Expand, fields of another split and item of For each, Find, Match and Item at over a typed list (portSplits.ts). - Right-clicking a field pin offers "Recombine". Splitting drops the edges of the whole output, recombining drops the field edges. One undo step brings them back. - The split is stored as GraphNode.splits and saved with the graph. Copy and paste carry it along. - Field pins follow the struct asset like a typed Expand. A new field gets a pin, a removed field loses pin and edge, a renamed field keeps its edge. Deleting the asset keeps the last fields. The delete dialog counts splits, so it now says "node(s)" instead of "Pack/Expand node(s)". - Renaming a variable carries its split and the field edges along. - At run time the node still produces the whole value. A field pin reads its field like Expand, a missing field reads as empty. - Limits: outputs only, one level, behavior graphs only. Tests: - ./check.sh: 185 files, 2706 tests, CHECK EXIT 0 - e2e GM_STEPS=1-6,54,55: ALL PASSED, 303 checks - New port-splits.test.ts with 12 tests. e2e 6z-vi with 3 checks using a real mouse: split Find.item, wire item.Name into the test and Match.item.Amount into the log (logs 7), recombine. - Counter-checks: without field reads in the interpreter, without rename in the split, without moving edges on rename and without cleanup on field removal one unit test fails each. In the browser, without field reads nothing is logged and without type inference the split entry is missing. - Check total in README: 1520. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2aaca01929 |
Find the first matching item with a Find … Match block (#255)
Searching a list with For each needs the test result wired back into break on the left of the loop node. The wires then run across the node and the graph reads badly. Find … Match keeps the whole search flowing left to right, while the test in between stays a free graph. - Find takes exec and list and offers item and index. It sets them to each element in turn and has no exec output of its own. - Match reads its bool input match once per element. The first true value continues at found with item and index of the hit. No hit or an empty list continues at not found with an empty item and index -1. - The pair is derived from the wiring and stored nowhere. A Match belongs to the Find whose item or index its test is built from. Copy, paste and undo therefore need no extra code (findBlock.ts). - An unpaired Find or Match, two Matches on one Find and a Match fed by two Finds show a warning on the node. At run time the flow ends at an unpaired Find and the console says so. - Adding Find from the palette places its Match 480 px to the right in the same undo step. - The graph draws a quiet frame behind Find, Match and the nodes of the test. Literals feeding the test from outside stay outside the frame. - The test consists of data nodes only, like the break of For each. Tests: - ./check.sh: 184 files, 2694 tests, CHECK EXIT 0 - e2e GM_STEPS=1-6: ALL PASSED, 272 checks - New find-match.test.ts with 12 tests, 2 catalog cases, e2e 6z-v with 3 checks using a real mouse. - Counter-checks: ignoring the test fails 4 unit tests, no ambiguity check fails 2, framing everything fails 1. In the browser, no frame and no Match from the palette each fail their check. - Check total in README: 1517. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e1d3c0cadb |
Keep items a running game puts into a typed list
A typed global list lost the items the game had added to it. The item showed up in the inspector for a moment and then was gone again. - Since #254 the project hands out a typed list as a fresh copy each time, with the `$struct` marks stripped. - While a game runs, every change to the project runs `adoptDeclarations`. It took a declared value over when it differed from the last one it had seen, compared with `!==`. - The fresh copy never equals the last one by identity. So the empty list from the project overwrote the live list on every project change, for example when the game writes something back to the scene. - The comparison now uses `sameValue`. A real edit to the variable during a run still reaches the game. Tests: - ./check.sh: CHECK EXIT 0 - New unit test in scope.test.ts, red without the fix. - e2e 55i: a graph appends to a typed global list, then another asset changes during the run. With the fix {"filled":2,"afterEdit":2}, without it afterEdit is 1 and the check fails. - GM_STEPS=1,55: ALL PASSED, 26 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b0d0868f55 |
Give a list one struct type so every item keeps its shape (#254)
A list of structs used to let every item carry its own fields. Adding an item meant typing the same fields again, and nothing kept the items in step with the struct asset from #253. A list can now name a struct type for its items, and the asset keeps them in shape. - A list variable and a list field get the row Items with Mixed, every struct asset and Make struct type. Make struct type collects every field any item has, so no item loses a field. - A typed item carries its type under the key `$struct` (ITEM_TYPE_KEY). listItems does not count it as a field. - A list that holds anything but structs refuses a type, also when the offending item sits in an override. The console names the list and the count, and the list stays as it was. - In a typed list `+ Item` adds an item with the defaults, without the kind menu. Field names are locked, values stay editable. - The rows of a typed item take their kind from the struct asset (declaredKind in InlineList.ts). An asset field shows the asset picker instead of a text field, as reported in the issue. - A struct item in a mixed list gets its own Type row. Switching a list back to Mixed keeps the type on each item. - syncStructTypes brings marked items into shape and carries renames into them, also inside overrides and nested lists. One undo step. - The delete dialog of a struct asset also counts typed lists. - The game never sees the key. World and sharedState strip it with withoutItemTypes and bagWithoutItemTypes, so a struct stays a plain object at runtime. - Add item and Set item get the setting Type. With a type they only accept a struct with exactly its field names (fitsType in SetItem.ts). Anything else is left out and logged. - The item indent from #252 is back to its old width, the narrow one squeezed the value fields. Not done: writing a variable does not check the type, and the List node offers no type. Found on the way: a test helper in list-of-structs.test.ts called nodes without params. It now passes the same arguments as the interpreter. Tests: - ./check.sh: 183 files, 2679 tests, CHECK EXIT 0 - e2e GM_STEPS=1,21,22,32,54,55: ALL PASSED, 150 checks - e2e GM_STEPS=1-6,21,22,32,54,55 before the follow-up: 404 checks - New: tests/core/typed-lists.test.ts, e2e module 55 with 13 checks - Counter-checks bite in unit and browser, originals restored by cmp Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
61bbb96a96 |
Define a struct once as an asset and reuse its shape (#253)
A struct used to declare its fields wherever it stood. The same shape in two variables meant typing it twice, and Pack/Expand copied it once without staying in sync. A Struct asset now holds the shape in one place, and every struct that picks it as its type follows it. - New asset type "Struct" (not placeable, group Structs, own icon). The inspector shows its fields with name, kind and default value, and "Open" shows them in the variables tab. - Struct variables and struct fields get a Type row: own fields, any Struct asset, or "Make struct type". With a type the fields come from the asset and cannot be added, renamed or removed there. The values stay per variable, overrides on placed objects still work. - Pack and Expand get the setting Type. With a type their ports are the fields of the asset and read-only in the inspector. - A change to the asset reaches every typed place in one undo step (syncStructTypes). A new field arrives with its default. A removed field goes along with its port, edge and typed value. A renamed field keeps its value in variables, overrides and on ports with edges, also through nested types. - Undo for these edits records plain assignments (Recorder), because the reach of a change is not known in advance. - Cycles such as Loot in Loot, also through another type, are refused and not offered in the picker. - Deleting the asset names how many places use it. Afterwards each place keeps its fields as its own. - Loading reshapes a stale copy in a project file to its type. - "Make struct type" creates an asset from an existing struct and links the place to it. Differs from the plan: - A changed default value applies to new fields and newly typed places only. Existing values stay. - Removing a single field shows no warning. Undo brings it back. Typed lists are left for #254. Lists stay dynamic for now. Found by e2e: after deleting or renaming the type the inspector did not rebuild and kept showing the old name. Fixed, the check was red before. Tests: - ./check.sh: 182 files, 2661 tests, CHECK EXIT 0 - GM_STEPS=1,4,21,22,54: ALL PASSED, 176 checks - 14 unit tests in tests/core/struct-types.test.ts - New e2e module 54 with 14 checks, real mouse. Module 04 counts 23 asset types. - Counter-probes: without the rename pass 4 unit tests fail, without reshaping values 8, without the cycle rule 1. In the browser, without spreading changes and with + Field despite a type, 3 checks fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4857bb75e6 |
Show lists in the inspector and variables in their own tab (#252)
The List tab could show one list at a time and was the only place to see its items. A list now shows its items right where it lives, and a new Variables tab shows all variables of an asset or placed object. - InlineList draws the items of a list under its row in the inspector, in struct fields and in the Variables tab. A list inside a list or a struct field is drawn the same way, one step indented. - The index column is the indent. Items sit under their list without an own margin, and the index column is 16 px wide instead of 28 px. - Above 20 items (INLINE_LIMIT) the rest collapses into "... N more - open in tab". During a run the rows show the values from the game. - ListHead puts "N items" and "+ Item" in one row. "N items" opens the list in a tab. A separate Open button squeezed the counter to zero width in the narrow inspector column. - VariablesEditor shows every variable of a behavior asset, a variables asset or a placed behavior. On an asset it edits the declarations. On a placed object an edit only overrides that object, with the usual reset and push buttons. - Opening a list inside a tab adds a new tab and keeps the open one. valuesKey gives each list its own document, so a list that is already open is shown instead of opened twice. - Titles read "Pickup · Variables" or "Listy · nums · [3]". The tab closes when its asset or object is deleted. - Lists on node inputs stay inline only, a node owns no variables. - The List tab, ListEditor and the __gm.list hook are gone. Tests: - ./check.sh: 181 files, 2645 tests, CHECK EXIT 0 - GM_STEPS=1,21,22,32: ALL PASSED, 123 checks - e2e 21 is rewritten for inline lists and the new tab, 22 and 32 are adapted. Sum in README: 1486 (+6 in 21, -1 in 22), counted by hand. - Counter-probes fail as expected: sub-lists not told apart (1 unit test), sub-list replacing the open tab, all items drawn inline (60 rows instead of 20), no live values (0 rows at 24 items in a run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
92fad6b9e3 |
Show the list a running game fills in the list tab (#251)
While playing, the inspector said "2 items" for a list a graph had filled, but clicking it opened an empty list tab. The inspector reads a Variables asset from the running game, the list tab only read the list stored in the project. It did not follow the run's ticks either. - resolveList takes the live values. For a Variables asset it uses the running game's value while one runs, the project's otherwise. This is the inspector's rule. - ListEditor refreshes on runtime ticks about 10 times a second, and not while a field in the tab is being typed in, like the inspector. - Editor hands the list editor the inspector's liveVariables. - __gm.list.info() also returns rows, the rows the last build drew. - Editing in the tab while playing goes into the run's copy of the project, as in the inspector. A very long, growing list rebuilds all rows up to 10 times a second. Tests: - ./check.sh: 180 files, 2634 tests, CHECK EXIT 0 - e2e GM_STEPS=1,32: ALL PASSED, 30 checks - e2e 32b-ii, 2 checks: a graph adds an item every frame, the test pauses and clicks "N items" with a real mouse. The tab draws 26 rows for 26 items, grows to 50 after resuming and is empty after Stop. - Counter-checks: without live values the tab draws 0 rows for 23 items, and without the refresh it stays at 23 - README: 1481 editor checks, counted by hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb8a2d029a |
Loop over a list and stop at the first match with For each (#249)
Finding the first item that matches took Count, Repeat, Item at and a Branch, and the loop could not stop once it had found something. A graph has no lambdas, so the predicate is an input that is read every round. - For each runs body once per element, with item and index set, and goes on at done. - Its break input is read anew before every round, after item is set. Once it is true the loop ends there and that round's body does not run. - done sees the matching item and index with stopped true. Without a match it sees no item, index -1 and stopped false, the OrDefault. - RepeatResult has optional items, stopWhen, stopped and exhausted. GraphInterpreter.loop reads them, Repeat runs as before. Tests: - ./check.sh: 180 files, 2623 tests, CHECK EXIT 0 - e2e GM_STEPS=1-6: ALL PASSED, 269 checks - 5 interpreter tests in graph.test.ts, 2 catalog cases - e2e 6z-iv: the palette creates the node, and a search for Name == "Gold" in Axe, Gold, Bow, Gold logs "1true" - Counter-checks: without the break check 2 unit tests fail, and with break not read over its edge 6z-iv fails - This commit alone: tsc and the three touched unit files pass (349 tests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a5ae21481b |
Type point values, snap and scrub the preview in the curve tab (#248)
Building a curve by mouse alone was fiddly: there was no way to put a point on an exact value, no grid, and the running preview could not be stopped at a chosen moment. The hand cursor over points also looked like a link instead of a drawing tool. - Number fields t and v in the header show the selected point. Typing or dragging on them goes through the same rules as a mouse drag, so points never overtake and handles stay clamped. One undo step each. t is greyed out on the two end points. - The magnet button from Vector art snaps points and handles to a 0.05 grid in t and v, when placing on free space and when dragging. - Points and handles keep the default arrow in the curve tab and in Vector art. The pixel art palette dropped pointer as well, which is beyond the issue. Collider corners never had it. - The preview now starts off. A vertical time line with a knob under the t axis sets t when dragged sideways. Dragging the dot on the rail jumps to the first t where the curve reaches that value, or to the closest one if it never does. - The rules are pure functions in curveView.ts (withCurvePointSet, snappedToGrid, shareAtValue, previewSecondsAt), the canvas only calls them. Tests: - ./check.sh: 180 files, 2616 tests, CHECK EXIT 0 - GM_STEPS=1,47,53: ALL PASSED, 87 checks - 4 new unit tests in curve-view.test.ts, 4 new e2e checks in 53 (53h-53k) and 1 in 47, all with real mouse and keyboard - Counter-probes bite: without "first t" 1 unit test fails, with pointer, no snap, no field commit, no scrub or preview on 5 browser checks fail, pointer in Vector art fails the new 47 check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
393b8ec7f9 |
Build your own easing curve and use it on keys and Lerp (#247)
The built-in eases cover the usual shapes, but a game often wants its own: a coin that bounces twice, a door that overshoots and settles. A new asset type holds such a curve, and keys and the Lerp node can pick it instead of a built-in ease. - New asset type "curve": points like in Vector art, x is t and y is the value. A new curve is the straight line from (0, 0) to (1, 1). - One pure evaluation in src/core/model/curve.ts serves the timeline preview, the running game and the Lerp node. - The curve always has one value per t. The end points stay at t 0 and 1, points cannot pass each other, and a handle that reaches past its neighbour is shortened along its direction. Loading a hand-edited file applies the same rules. - Curve tab: click on the line to insert a point that keeps the shape, click on free space to put one there, drag points and handles, double-click or Delete removes, Alt-click toggles round and sharp. Every step is one undo step. - Presets: all 30 built-in eases plus Linear, fitted with adaptive subdivision. Each stays within 0.4 % of the built-in ease. - Preview: a dot runs along the curve once a second, a second dot shows only the value. The Preview button stops it. - Animation keys get "Curve" under Easing with a curve picker. Lerp gets "Curve" under Ease, and its result may overshoot b. - A missing or unpicked curve runs linear. The inspector says so, the node shows it as a problem, and deleting a used curve warns with the number of keys and Lerp nodes. Tests: - ./check.sh: 180 files, 2612 tests, CHECK EXIT 0 - GM_STEPS=1-6,25,53: ALL PASSED, 309 checks - New e2e module 53 with 13 checks, all with the real mouse. Expected values come from its own Bezier evaluation. - Counter-checks bite: without Bezier evaluation 4 unit tests fail, without handle clamping 2. In the browser, a key that cannot find its curve, a click that sets no point and a handle drag that moves the point all turn checks red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
320d3e27e1 |
Add a Lerp node that blends from a to b with an ease (#246)
Every animation in a behavior had to build its interpolation from Subtract, Multiply and Add, once per axis. The gold pickup in "Garden of Sacrifices" needed about 30 nodes for that alone. A single Lerp node with an ease makes those chains one node each. - Lerp has the inputs a (default 0), b (1) and t (0) and the output result. It sits in the data category next to Clamp. - The Ease setting offers Linear, In, Out and In-out, all quadratic. The node row shows it as "Ease: Out". - t is held in 0..1. A timer that overshoots in the last frame still lands exactly on b. Tests: - ./check.sh: 178 files, 2583 tests, CHECK EXIT 0 - 7 new cases in the node catalog test. Without the clamp on t, 2 of them fail. - e2e 6z-iii (2 checks): typing "lerp" in the palette adds a linear Lerp, and picking "Out" in the inspector reaches the node and its row. GM_STEPS=1-6: ALL PASSED, 267 checks. - README check sum is 1459, computed, not counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
30de4d35a8 |
Give the scene tab a header with Fit and the eye (#245)
The scene view had no header bar of its own, and its eye sat in the top bar. Switching between Scene and Game also showed two different pictures, since nothing tied the free view to the camera. - Theme.headerPadding (4) replaces Theme.padding in the top bar and in TabHeader, for the padding and for minHeight. Both bars shrink from 40 to 32 px. Nothing else uses the new value. - The new ScenePane wraps the scene view in a header bar with Fit and the visibility eye. The eye is gone from the top bar, which keeps the menu, undo and redo. - Fit with an active camera shows the world exactly as the Game tab does at its chosen step: the same zoom, and the camera centre on the same screen pixel. The rule is the pure freeViewOfCamera in viewport/view.ts, which also mirrors the letterbox snapping. - Fit without a camera frames all content on a whole step, as before. - New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there. - The doc comment above Theme.treeIndent went away in the working tree along with this. It is committed as found. Not checked: the two views only match while they have the same size. With the tilemap panel open the scene view is narrower, and Auto may pick a smaller step there. Tests: - ./check.sh: 178 files, 2575 tests, CHECK EXIT 0 - 2 new unit tests in view.test.ts: three cameras times four steps land on the same screen point as in the Game tab, Auto stays whole - e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate, camera frame and view box match in both tabs - GM_STEPS=1-3,46: ALL PASSED, 94 checks - Counter-probes: without the centre offset the unit test fails, with Fit ignoring the camera both 3h checks fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e0b471c354 |
Open the add menu with a right-click in the asset panel (#244)
New assets could only be added through the + button. A right-click on a category heading or on the empty space below the rows now opens the same menu at the pointer. - ListView gets the callback onListMenu. It fires on a right-click on empty space or on a heading. Only the asset panel has headings, so other lists are not affected. - AssetPanel opens its + menu from there. - A right-click on a heading does not fold or unfold the category. A right-click on empty space clears the selection like a left-click. - A right-click on an asset still opens the menu of that asset. - The empty list now says "No assets - press + or right-click". Tests: - ./check.sh: 178 files, 2573 tests, CHECK EXIT 0 - e2e 1-4: ALL PASSED, 134 checks - 3 new checks in e2e 04 with real right-clicks: menu on the "Scenes" heading (21 types, heading stays folded), menu on empty space, and "Behavior" from that menu creates one, undone again. - Counter-probe: without the menu both right-click checks fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
38f009e035 |
Make e2e 40, 46, 51 and 52 pass in the full run again
The suite job failed on every push since
|
||
|
|
2eeabf7c23 |
Drag a whole selection and join existing groups in the hierarchy (#241)
Three follow-ups to the hierarchy groups. Dragging a row that belonged to a multi-selection moved only that one row. The group dialog could only create a new group. The group row drew its folder in front of the label, unlike every other row in the hierarchy. - Dragging a selected row moves every selected object. This holds for reordering, dropping into a group or between its members, dragging out of a group and dropping onto an object as new parent. - The moved objects land side by side in hierarchy order, whichever row was grabbed. The whole move is one undo step. - A selected child of a selected object travels with its parent, not on its own. Dropping the selection onto its own rows does nothing. - Dragging a row that is not selected still moves only that row. - hierarchyDrop.moveObjects replaces the single-object rule. One object is the case with one element, so the old drop tests hold unchanged. - The group dialog has a "Group" dropdown: New group or an existing group. Picking a group greys out the name and turns the button into "Add". A group that holds all chosen objects already is not offered. - Rename keeps using the dialog without the dropdown. - ListRow draws an end icon at the very right and the hint left of it. Only the hierarchy uses iconAtEnd, so other lists do not change. - New test hook __gm.hierarchyHint() gives the centre of a row's hint. Not clicked in the browser: dragging a selection in the container tab and onto an object as parent. Only the unit tests cover these. Tests: - ./check.sh: 178 files, 2564 tests, CHECK EXIT 0 - e2e 1 + 52: ALL PASSED, 27 checks - 7 new unit tests in hierarchy-drop.test.ts, 4 new e2e checks in 52i and 52j with real clicks and a real drag - 4 counter-probes bite: drop rule ignores the selection (5 unit tests fail), icon column not kept free, dialog ignores the picked group, panel ignores the selection (only o2 lands in the group) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
893b81f798 |
Select a range of rows with shift-click in the hierarchy (#241)
Ctrl- and shift-click only added single rows to the selection. With many objects spread over the hierarchy, picking a run of them meant clicking each one. Shift-click now selects everything between the anchor and the clicked row, as in a file explorer. - The anchor is the row last clicked without shift, plain or with ctrl. A second shift-click redraws the range from the same anchor. - Ctrl+shift-click adds the range to the current selection instead of replacing it. - A collapsed group inside the range brings all its members. Children hidden under a collapsed parent stay out, since they are not shown. - Without an anchor from the hierarchy, the primary object picked in the scene view serves as anchor. With no selection at all, shift-click adds the single row as before. - The range rule is a pure function in hierarchyRange.ts, so it is tested without a browser. HierarchyPanel keeps the anchor. Not checked in the browser: ctrl+shift-click and the anchor taken from the scene view. Only the code path covers them. Tests: - ./check.sh: 178 files, 2557 tests, CHECK EXIT 0 - e2e 1 + 52: ALL PASSED, 23 checks - 5 new unit tests in tests/editor/hierarchy-range.test.ts - New e2e check 52h with real clicks - Counter-check: without the range, 52h fails with ["o2","o5"] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
53fa63441b |
Group scene objects in the hierarchy, for the editor only (#241)
A scene with a hundred crates fills the hierarchy with a hundred rows. Groups bundle top-level objects under one folder row that folds away. They exist for the editor only. Parenting, positions and the game do not see them. - Model: a scene or container template holds a list of groups, an object may carry a groupId. Old projects load unchanged. Loading drops empty groups and members that are no longer top level. - Only top-level objects can join a group. Parenting a member takes it out of the group, and Undo puts it back. There are no groups in groups. - Order: treeOrder lists the members together, where the first one stands. The same order decides drawing at equal z, in the editor and in the game, so the hierarchy and the picture cannot disagree. The stored object list stays untouched when grouping. - Hierarchy: a group row shows a folder icon, the name and the member count. Clicking it selects all members and keeps it folded. Selecting a single member elsewhere unfolds it. Folding lasts for the session. - Create with "Group..." in the object menu. A name dialog asks first, so creating is one Undo step and Cancel creates nothing. - The group menu has Select members, Rename... and Ungroup. The object menu adds "Remove from group". - Drag and drop: onto the group row appends, into a gap between members inserts there, next to a row outside leaves the group. Dragging the group row moves all members as a block. The drop rules live in hierarchyDrop.ts as a pure function. - Copy and paste keeps a top-level copy in the group of its original. A container built from group members takes their place in the group. - Clicking an object that is already selected together with others now keeps only that object selected. ListView narrows the selection on release, so a drag starting on such a row keeps the selection. Not checked in the browser: groups in the container tab, "Remove from group", Ctrl-click on a group row and a drag out of a multi-selection. The code path covers them. Tests: - check.sh: 177 files, 2552 tests, CHECK EXIT 0 - e2e 1, 51, 52: ALL PASSED, 31 checks (new module 52) - 19 new unit tests in object-groups.test.ts and hierarchy-drop.test.ts - 9 counter-probes, all fail as expected (7 unit, 2 in the browser) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2b89292eb4 |
Copy and paste scene objects and graph nodes (#240)
Copy and paste only existed for animation keys. Building a scene or a graph meant placing every object and node again by hand, even when the same arrangement was needed twice. - Ctrl+C copies the selected objects with their whole subtree, or the selected nodes with their links, groups and comments. Ctrl+V pastes into the scene, container or graph of the visible tab. - The copy is a snapshot in the editor's own clipboard, in memory. It holds asset ids, so the system clipboard is not used. It is cleared when another project opens. - A paste lands under the mouse pointer when it is over the view, and keeps its arrangement. Otherwise it lands one grid step beside the original. Each further copy that still exists moves one step on, so an undone copy does not count. - Objects get new ids for the whole subtree. They paste into the one selected object and keep their look under the new parent. If that object is the original or an earlier copy, the paste lands beside it instead of nesting deeper with every Ctrl+V. - A placed container stays a container. Content copied out of a container pastes as plain objects. Pasting a container into its own template is refused. - Nodes paste only where the palette would offer them. A second Start or End, a State in a behavior or a behavior calling itself is left out, and a notice names what was dropped. - Menus: Copy and Paste in the object menu, a new Paste menu on empty scene space, Copy in the node and box menus, Paste in the palette. - Ctrl+V still imports a picture from the system clipboard when the copy does not fit the tab. While objects are copied, the scene and container tabs use Ctrl+V for them, as the timeline does for keys. Not checked in the browser: paste in the container tab, in state machine, shader and particle graphs, and from the hierarchy menu. The shared code path covers them. Tests: - ./check.sh: 175 files, 2531 tests, CHECK EXIT 0 - e2e 51 (new): ALL PASSED, 11 checks with real keys and clicks - 13 unit tests in tests/editor/clipboard.test.ts, one of them pins the grid step after undo found during e2e - 7 counter-checks all fail as expected (6 unit, 1 in the browser) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7e829f737e |
Load every picture of a project ahead so animated tiles don't flicker
A texture was fetched only when it was drawn for the first time. Until the file arrived, the tilemap skipped the cell for that frame. An animated tile whose next step sat in another picture therefore blinked away once on its first cycle, and sprite animations did the same. - FileLibrary gets preload(files). It requests every file that is not held, loading or known to have failed. Each library reports what it holds through holds(). - src/core/model/projectFiles.ts lists the files a project names per library: uploaded textures, fonts and pixel art cels. - App.ts runs the preload on every project event. That covers opening a project, new assets and edits from a collaborator. - A new test hook __gm.heldPictures() reports which textures of the project are in memory. Not done on purpose: - The tilemap keeps no fallback frame while a step is still loading. - Sounds still load on first play. Not checked: the flicker itself was not reproduced in the browser, the e2e check measures "held in memory". Modules 42, 44 and 47 read textures from the project and were not run locally. Tests: - ./check.sh: 174 files, 2518 tests, CHECK EXIT 0 - e2e 39 + 40: ALL PASSED, 96 checks - 3 new unit tests for preload and the project file list - New e2e check 40k-ii: an unplaced picture is held after a reload - Counter-probe: without the preload 40k-ii fails with "Autotiles: held false, unworn: held false" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6015869297 |
Let the scene view hide symbols, names and shapes per asset kind
The scene view gets crowded once a scene holds many behaviors, colliders and tilemaps. A new eye button in the topbar opens a list of all placeable asset kinds, and each row has toggles for what the scene view draws for that kind. - The popup lists all 9 placeable kinds from Behavior to Tilemap, with the columns Symbol, Name and Shape. It stays open while switching. - Sprite, Text and Tilemap draw no symbol, so their symbol toggle is greyed out. Their name toggle shows a new centred name tag, which is off by default. - Shape exists for Collider and Tilemap. A hidden collider outline is neither drawn nor clickable. A hidden tilemap shape removes the collision outlines and the paint tint, the tiles stay visible. - A hidden symbol takes no slot in the ring and draws no dashed link to its parent. A name that stays on moves to the centre of the object. - The setting lives in the project as `sceneVisibility`, not in localStorage. Older projects load with the defaults, which match the view as it was. - Switching has no undo step, like the game scale. It only changes the view. A change made during Play survives Stop. - The rules sit in `src/core/model/sceneVisibility.ts`, the popup in `src/editor/panels/SceneVisibilityPopup.ts`. Four new SVG icons. Not covered by a check: symbol off with name on for a kind with a symbol, Text and Tilemap with names, and the paint tint with shape off. Tests: - ./check.sh: 173 files, 2515 tests, CHECK EXIT 0 - e2e 46: ALL PASSED, 17 checks. 46g clicks the popup, lays a small tilemap over the crate and removes it again. - e2e 8 alone on a fresh seed: ALL PASSED, 23 checks. A red check in the partial run 2,8,46 came from the module selection. - 8 new unit tests in tests/editor/scene-visibility.test.ts. - 4 counter-probes, each fails its test: load ignores the saved value, Play skips the saved model, scene view ignores collider shape, scene view ignores tilemap shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
66868de10a |
Show destructive menu entries in red text, not on a red fill
A red filled row in a context menu shouted louder than the action deserves. The entry now looks like every other entry and only its text is red, so the menu stays calm and the warning is still there. - Menu: danger entries are ghost buttons with the new text style danger. On hover they get the same grey fill as all other entries. - Theme: new text style danger in the normal font size. The existing warning style is small and did not fit a menu row. - Button: two getters expose the drawn text colour and fill, so a test can measure the look instead of reading the danger flag. - Test hook __gm.menuLooks() lists both per menu entry. - Confirm dialogs keep their filled red Delete button. That is a separate widget and was not part of the request. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 25: ALL PASSED, 31 checks. New check in 25g opens the menu with a real right click: Delete row has text #e0553f and no fill, Insert frames keeps the normal text colour. - Counter-check: with the old styling the check fails with text #ffffff on fill #e0553f. Original restored per cmp and rebuilt. - Not checked: other menus (graph, assets) have no own check. They run through the same Menu code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
89b95aa8ac |
Move "Delete row" from the inspector into the timeline menu
The context menu of the timeline is where the other row and key actions live. A separate button in the inspector was easy to miss and sat apart from them. - The timeline menu gets "Delete row" as a danger entry after a separator. It deletes the row under the pointer, whether the click hits the row header or a cell. Below the last row or on the ruler it is disabled. - A right-click on a row header now also selects that row, like a right-click on a key selects the key. A right-click in an empty cell keeps the selection so chosen keys stay available for Copy. - The "Delete row" button in the "Timeline row" inspector is gone. "Delete keys" for a multi-key selection stays. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 25: ALL PASSED, 30 checks. New check in 25g deletes a row via a real right-click and restores it with all three keys via Undo. - No counter-probe. No check covers the right-click into a cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0c5ac32b59 |
Let structs and lists nest to any depth (#239)
Structs and lists were capped at two fixed levels (list of structs, struct with a list field). Anything deeper was dropped silently on load, in node settings and when written to a variable. The cap came from an early design choice, not from a technical need, so it is lifted completely instead of adding a third fixed level. - One kind list NESTED_KINDS replaces ListItemKind and StructFieldKind. The flat special cases in listItems.ts are gone, every value goes through one recursive path. - A struct inside a struct is declared, like a top-level struct. Values coming in over an edge are brought into that shape. Struct items in lists still take their fields from the value. - New valuePath.ts addresses any nested spot by keys and indices. ListSlot.field is now ListSlot.path. - Field commands (add, rename, remove) take the path to the struct. They change declaration, own value and all overrides in one undo step, as before on the top level. - Inspector: "+ Field" offers Struct and List on every level. A nested struct is a header row with its own "+ Field", its fields are indented one step further. - List tab: a list item shows "n items" and opens one level deeper. The title shows the path, e.g. "Listy · nums · [3]". - Pack in Pack now writes a struct into a struct field instead of text. Objects are kept at any depth and stripped at every exit. - A list edit in a struct field without an existing object override now starts from the shown value instead of an empty struct. - Old projects load unchanged. Save, load and save again gives the same result. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 21 + 22: ALL PASSED, 90 checks (new 21d-iii, 22f-ii, 21d and 22f updated) - 20 new unit tests in nested-values.test.ts and nested-list-slot.test.ts, three tests that pinned the old cap turned around - 6 counter-probes, each fails 1 to 8 tests, originals restored and checked with cmp - Not covered: a nested struct inside a struct item in the list tab has no e2e click 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
|
||
|
|
98211a8d54 |
Let a variable hold an object across behavior runs
A spawned object was only reachable in the run that spawned it. The Spawn output could not go into a variable because "object" was a port-only kind, so the next run had no way back to the instance. - "Object" is now a variable kind. The reference lives for the run only. The file stores the declaration and an empty value. - The live reference sits in slots beside the variable bag (graph/objectSlots.ts), not in it. The bag is the behavior's props, and syncToModel would have written an object into the scene. - A destroyed object, or one from a restarted or left scene, reads as nothing. A dead reference is dropped from its slot. - Keep (#229) hands the slots of kept variables on to the next call of a sub-behavior. - Properties of kind Behavior reads and writes another behavior's object variable through the same slots. - The inspector shows "(null)" for it and offers no input. - A persistent Variables asset with an object variable shows a warning under "Persistent". Saved values never keep the object, every run starts it at (null). Objects inside lists and struct fields are left out on purpose. Emptiness is checked with Equals against an empty b. A node "Is valid" does not exist yet. The test helper graphEditor().variables() now also reads the kinds of a Variables asset a node points to. Tests: - ./check.sh: 169 files, 2463 tests, CHECK EXIT 0 - e2e 6 + 23: ALL PASSED, 146 checks (new 23k, 2 checks) - e2e 32: ALL PASSED, 18 checks (new 32f-ii) - 10 unit tests in tests/core/object-variable.test.ts, one of them runs a persistent asset over two runs - 5 counter-probes (value in bag, isAlive always true, no Keep hand-over, Properties without slots, object in saved values) each fail their test - e2e check total 1406, computed only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
923baf2557 |
Let Spawn take an asset over an edge and find a child by name (#237)
An asset could only be picked as a setting on the Spawn node. It could not be wired in, held in a variable or put into a list. Finding an object below the own one needed a scene-wide search by name. - "Asset" is now a kind like Animation or Sound: port, variable, list item and a new "Asset" literal node, with its own cube icon. - Spawn and Is asset have an `asset` input. It uses the key of the old setting, so a saved asset reads as the value of the input and older projects keep spawning without a migration. - New node "Child by name" searches below an object level by level. A child wins over a grandchild of the same name. Nothing found or no object gives null. Tests: - ./check.sh: 168 files, 2453 tests, CHECK EXIT 0. - 9 new unit tests in tests/core/asset-kind.test.ts, catalog cases for Spawn, Is asset, Child by name and the literal. - e2e 6z-ii (2 checks): node rows and a real drag of the literal onto `asset`. e2e 6: 119 checks passed. Spawn checks in 3 and 9 passed. - Counter-checks: depth-first search fails 1, Spawn reading only the old setting fails 3, runtime ignoring the parent fails 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1b3cb625a3 |
Let a text field select from right to left with the mouse (#236)
Dragging from right to left in a canvas text field only moved the cursor. The hidden input behind the field got every selection sorted and without a direction. Its selectionchange echo then handed back the sorted range, and the anchor jumped to the mouse on the first move. - KeyboardBridge sets the selection with 'backward' when the focus lies before the anchor, and reads selectionDirection when the input reports a change. - TextField passes anchor and focus through unchanged. Its own sorting is gone. - Shift+Left runs through the same path. The cursor should now sit at the moving end there too. This is not tested on its own. Tests: - ./check.sh: 167 files, 2435 tests, CHECK EXIT 0 - e2e 2: ALL PASSED, 25 checks. New check 2d-ii drags right to left in the name field and reads start 0, end 13, direction backward. - Counter-check without the direction: 2d-ii fails with end 2, the reported symptom. - e2e check total in README goes to 1401 (computed, not run in full). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
085e6c8cce |
Keep edited list values when an item is deleted (#235)
The list tab read the list once when it built its rows. Editing a value does not rebuild the rows, so the delete button later filtered that old copy and wrote it back. Every edit since the last rebuild was lost. Adding an item only triggered it, because adding rebuilds the rows and that state became the old copy. - ListAccess.value in src/editor/list/slot.ts is now read on every access instead of once when the access is built. - The kind switch of a row and the struct field buttons (field type, rename, remove field, + Field) read the item when clicked, not when the row was built. They had the same stale copy. - New test hook __gm.list.removeAt(i) clicks the delete button of one row, because the tab shows many buttons with the same label. Not covered by a test of their own: the struct field buttons and the kind switch after an edit. The cause is the same, the fix is in code. Tests: - ./check.sh: 2435 tests, CHECK EXIT 0. - Unit test in tests/editor/list-slot.test.ts keeps one access, edits a value, adds and edits an item, deletes another. All edits stay. It fails without the fix. - e2e 21b-ii with real clicks: set row 2 to 66, delete row 0, expect ["11","66"], two undos take both back. e2e 21: 52 checks passed. - Counter probe: with the old snapshot the check fails with ["11","44"], the reported bug. - Check total in README is now 1400 (+2), counted, not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b44f97c35 |
Let a child sort with its parent so its Z stays local (#232)
An animated or set Z of -1 on a weapon put it behind everything on Z 0, not just behind the player holding it. A child can now opt in to being drawn at its parent's place, so its own Z only orders it against the parent and its siblings. - New property "Sort with parent" on sprites, texts and particle emitters, shown in the inspector right under Z. It is a normal property, set on the asset and overridable per object. - With it on, the rest of the scene sees only the parent's Z and, where the layer sorts by Y, the parent's feet. Below 0 the child is drawn behind the parent, from 0 on in front. - It works across levels: a grandchild with the switch hangs on the top object of the chain. - It has no effect under a parent that is not drawn itself (container, behavior, camera, collider) or under a tilemap. - New assets start with it on, saved ones read as off, so old projects draw unchanged. For that a schema row now has "initial" next to "default". The gold "changed" mark and reset follow "initial", so a fresh sprite is not marked as changed. No load migration is needed. - parentGroups() in drawOrder.ts gives a slot only to the top of a chain and inserts the children there. Editor, play mode and picking all use it, so they agree. Tests: - 10 new unit tests in tests/viewport/sort-with-parent.test.ts. - 8 existing tests list the sprite rows or defaults and got the row. - New e2e check 5b-ii clicks the switch and checks order and undo. - Counter-checks, all originals restored by cmp: switch ignored fails 7, children not sorted by Z fails 6, no chain fails 1, "initial" ignored fails 9, row missing in the inspector fails e2e 5. - ./check.sh: 166 files, 2426 tests, CHECK EXIT 0. - e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fae827c087 |
Let an animation key Z so an object can pass behind another
The timeline offered X and Y as position rows but never Z. The list of position rows was simply written without it when animations came in with #97. A swinging weapon could therefore not go behind its character. - Z is the third position row, after X and Y, on every placed root or object. A scene and a behavior still offer no position. - Play and preview already read and write any position row by name, so the one entry in POSITION_KEYS is all the code change. - The draw order is sorted on every render, and play writes z back into the scene each frame. A keyed Z therefore takes effect at once. A blended 4.5 sits above 4 and below 5. Tests: - ./check.sh: 165 files, 2416 tests, CHECK EXIT 0. - 3 new unit tests in tests/core/animation.test.ts: Z is offered after X and Y and not on a behavior or scene, a running game moves the object from behind to in front of another one, and play, scene write-back and preview agree on z. - e2e 25: ALL PASSED, 29 checks. The new check reads X, Y, Z as the first rows of the row dialog. Check sum 1396 -> 1397. - Counter-check: without z in the list all 3 new unit tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
147f8ffbea |
Let animation keys ease with Sine to Bounce in, out or in-out
A key could only ease quadratically, which makes quick moves like a sword swing feel soft. Animators know the Penner curves from CSS, GSAP and Unity, so a key now offers all of them. - Ten curves (Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce), each in, out and in-out, next to Linear and Step. - The inspector shows two fields instead of one long list: Easing, and Direction only when a curve is picked. Switching the curve keeps the direction. Coming from Linear or Step starts at in-out. - Old projects keep working. ease-in/out/in-out now read as Quad and are still saved under that name. New curves save as cubic-out, back-in-out and so on. - Back and Elastic overshoot on purpose. A number property with min or max is clamped in play and preview, so a Back in from scale 0 does not flip the sprite. trackWriteAt now takes the property definition, and animatedKind* is renamed to animatedDef*. - The timeline marker still shows only the direction. - In-out is built for every curve from its in-curve and its mirror. easings.net uses own constants for Back and Elastic in-out, so our Back in-out winds up about 5 % instead of 10 %. Tests: - ./check.sh: 165 files, 2413 tests, CHECK EXIT 0. - 12 new unit tests in tests/core/easing-curves.test.ts: round trip of all 30 values, start/end/midpoint of every curve, reference values, only Back and Elastic leave 0..1, clamping in play and preview, unknown values are rejected. - e2e 25: ALL PASSED, 28 checks. New 25b-ii (3 checks) clicks both fields and undoes the four choices. Check sum 1393 -> 1396. - Counter-checks, each fails its test: no clamping (2), Quad saved as quad-* (2), mirrored in-out wrong (2), curve lookup with `in` instead of Object.hasOwn (1), no Direction field (e2e 25). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94749b4f3e |
Let a project keep named snapshots and restore them (#231)
Autosave only keeps the latest revision. Before a risky change there
was no way to put a known good state aside and go back to it later.
The main menu now has "Snapshots..." to create, restore and delete
named copies of the open project.
- Server: SnapshotStore.php keeps each snapshot as a copy of the
project JSON under data/snapshots/<project>/ (git-ignored). Four
routes under projects/{id}/snapshots use the same owner check as the
project, so an admin also sees foreign snapshots.
- Create writes the autosave first. With unsaved edits (play with open
edits, failed save, conflict) no snapshot is made and a message says
why. The rule is the pure function in src/editor/snapshots.ts.
- An empty name falls back to the local date and time.
- Restore asks first, then keeps the current state as "Before
restoring <name>". It writes the snapshot back as a new revision and
reloads the project, so the undo list is empty. A restore can itself
be undone by restoring that snapshot.
- Deleting a project deletes its snapshots too. Images, sounds, fonts
and pixel art are content-addressed and never deleted, so a snapshot
can always reach them.
- Client: SnapshotStore and HttpSnapshotStore in src/core/store, the
SnapshotsDialog, and the flows in ProjectSession. A stale revision
(409) becomes StaleRevisionError.
- CI runs the new PHP test in the gate job next to accounts.
Not covered by e2e: the lock during play and the conflict case. Both
are unit tests only.
Tests:
- check.sh: 164 files, 2401 tests, CHECK EXIT 0.
- tests/php/snapshots.test.php: 14 checks, all passed.
- tests/editor/snapshots.test.ts: 5 tests for the time format, the
blocker rule, the routes and 409.
- New e2e module 50 on a throwaway project: create, rename, restore,
undo the restore, delete, then delete the project and get 404 on
its snapshots. 46b expects the new menu entry. e2e 46,50: 20 checks
passed. The check sum goes from 1385 to 1393 (computed).
- Counter-checks, originals restored and compared with cmp: blocker
always allowed fails 1 unit test. No sorting fails 2 PHP checks.
Restore without keeping the old state fails 50c and aborts 50d.
- The first sort test with 2 snapshots passed by chance half the time
because of random ids. It now uses 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7c70245c79 |
Let a nested behavior keep its variables between calls (#229)
A Run behavior call started with fresh variables every time, so a nested behavior could not count, remember or keep a local state machine across calls. A new "Keep state" switch on the node lets the callee resume where its last call left off. It is off by default, so existing projects run unchanged. - RunBehavior gets the boolean parameter keepState and its node description explains it. - GraphInterpreter keeps the state in keptCalls, keyed by the call path, the chain of Run behavior nodes. Two nodes calling the same behavior each keep their own state. - One interpreter exists per behavior instance, so every object gets its own state without further code. - Local state machines of the callee are kept like its variables. - resumedVariables() matches kept values against today's declarations. A new variable starts at its default, a removed one drops out, and one whose kind changed starts over. - The kept state lives as long as the object in the run. It is not written to save games. Limit: a callee input named keepState collides with the switch. The same already holds for the name behavior, so it is not caught. Tests: - ./check.sh: 163 files, 2388 tests, CHECK EXIT 0 (before 2377) - 10 new unit tests in tests/core/keep-state.test.ts and 1 new node catalog case. - e2e 19: ALL PASSED, 35 checks, with the new check 19c-ii for the checkbox, its click and undo. - Counter probes: ignoring keepState fails 5 tests, state per behavior instead of per node fails 1, dropping the outer path fails 1, skipping the kind check fails 1, and without the node parameter the e2e check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |