14972d057f877f355048fc8389a4f60fc0d0f7fb
228
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 (
|
||
|
|
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> |
||
|
|
a7e793f6de |
Keep many spawned behaviors from eating the frame
A scene with 200 spawned gold pickups, each with a Pickup behavior of 97 nodes, dropped the game to about 14 fps. The graphs themselves ran only a dozen nodes per pickup and frame. The cost was the lookup: for every data input the interpreter searched all edges and all nodes of the graph linearly and computed the port shape of the node again. - New module core/graph/graphIndex.ts builds tables once per graph: nodes by id, edges by node and port, node types. Port shapes are cached as well. - BehaviorSystem shares one cache between all its instances and clears it in syncFromModel. A graph changed during a run applies from the next frame on, as before. - An interpreter without a cache, as in most tests, searches as before. Measured on the scene Performance test (headless per frame, browser fps on a throwaway copy of the project): - Headless: about 21 ms down to about 3.6 ms per frame. - Browser, about 505 entities: 13.6 fps up to 23 fps. - Browser, about 320 entities: 32.3 fps up to 48 fps. - After the fix behaviors take about 15 % of the browser profile. The rest is the editor mirroring the game into the model and rebuilding its panels, which is a separate change. Tests: - ./check.sh: 192 files, 2786 tests, CHECK EXIT 0. - Full e2e run (local): ALL PASSED, 1551 checks. - tests/core/graph-index.test.ts: 2 tests. The tables return the same as the search, in the same edge order. A graph changed during a run applies from the next frame. - Counter-check: without clearing the cache in syncFromModel the second test fails with ['before']. 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> |
||
|
|
ef2600493f |
Read the pixel size of a texture in a graph (#262)
A graph had no way to know how big a picture is. An icon sprite with a fixed size therefore stretched every icon of another size, an 8x8 pixel-art icon in a 16x16 slot was drawn twice as big. - New data node Texture size. It takes a texture, a frame or a pixel art and hands out width and height in pixels, 0 x 0 for none. Wired into a sprite's width and height, the picture shows pixel for pixel. - GraphLookup gets textureSize. The project answers it with the same pictureSize rule a sprite uses to size itself. Tests: - ./check.sh: 2749 tests, CHECK EXIT 0. - 2 catalog cases (a picture, no picture) and one test against a real project: pixel-art sheet, single frame, missing texture. - No e2e check and no counter-check for this node. 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> |
||
|
|
cf6565d108 |
Start new behavior variables with Keep switched off
Keep is a choice the author makes per variable. Switching it on for every new variable meant most behaviors carried state across runs that nobody asked for, and the toggle had to be turned off by hand. - AddVariableCommand no longer adds a new variable to keptVariables. Keep is only switched on by hand. - Existing projects are unchanged. Their keptVariables stay as saved. - The comment in Asset.ts describes the new default. - The counter fixture in keep-state.test.ts now keeps count explicitly instead of relying on the old default. Tests: - ./check.sh: 2706 tests, CHECK EXIT 0 - e2e 1, 6, 19: ALL PASSED, 172 checks - 19c-ii expects [] after adding, then ["var1"], [] and ["var1"] after undo, all clicked with the real mouse - Before the fixtures were adjusted, 8 unit tests failed on the new default 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> |
||
|
|
b90b62886b |
Put one item into a list, a struct staying whole, with Add item (#250)
A list could only grow through Join lists, and that takes its b apart: a struct becomes one item per field and a list is joined. A list of structs such as loot could not grow at all. - Add item returns a new list with value as exactly one more item. A struct or a list goes in whole. The list that came in is left alone, like with Set item. - index -1 or unset appends. Otherwise the item goes in before that index. An index outside the list appends, so the value is not lost. - The rule that an item takes the kind of its slot moved out of Set item into fitToSlot. Both nodes use it, Set item behaves as before. - No e2e check and no browser look. The node is covered by unit tests. Tests: - ./check.sh: 180 files, 2634 tests, CHECK EXIT 0 - 7 catalog cases, 4 cases in list-of-structs.test.ts, and index in the whole-number inputs of texture-kind.test.ts - Counter-check: taking the value apart like Join lists fails 3 tests - This commit alone: tsc and the three touched unit files pass (337 tests) 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
483c34bb5c |
Let Destroy keep the children and hand them back as a list
Destroying a container used to take everything below it along. Now Destroy can remove just the object itself and pass its direct children on, so a behavior can keep working with what was inside. - New input keep children (bool, default off). When on, only the object goes. Its direct children move up to the object's parent, or to the top level if there is none. Grandchildren stay where they are. - New output children (object list) with the direct children that were kept. It is empty when keep children is off. - The children keep their place on screen. carriedTransform folds the destroyed object's position, rotation and scale into their own, the same as reparenting in the editor. - Behaviors and the rigidbody of the object are always destroyed with it and are not in the list. Moved up they would steer or simulate the parent. - World remembers which children it reparented, and syncToModel writes their new parent into the scene. A reload of the model keeps it. - Limit, read from the code only: in the editor run the contents of a placed container are not baked. Moved children there last for the run only. Tests: - ./check.sh: 170 files, 2478 tests, CHECK EXIT 0 - e2e 34 (the module with a Destroy node): ALL PASSED, 21 checks - world-sync: world matrix of child and grandchild unchanged under a rotated and scaled parent, parent survives syncFromModel, top level case, already destroyed object gives [] - destroy-keep-children: real graph Destroy(Owner) -> Log(children) logs Lid, Label without the grandchild Nail - 3 counter-probes, all caught: no world transform (1 test fails), no parent in the scene (1), behaviors and rigidbody moved up (2) 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> |
||
|
|
74b9c60661 |
Stop marking older sprites as changed after loading (#232)
Loading filled every missing asset property with its `default`. For
Sort with parent that is `false`, while the gold mark and Reset compare
against `initial`, which is `true`. So every sprite, text and particle
source saved before #232 counted as changed once loaded, and each save
wrote the `false` into the project file.
- normalizeBag leaves a missing key missing when it has its own
`initial`. It still reads as `false`, so older projects draw as
before, but they no longer count as changed.
- e2e module 4 failed in CI on three checks since
|
||
|
|
1d18862ed6 |
Give unconnected node inputs the default the node shows (#234)
A new node handed null to every unconnected input that had no port default, even a Bool. The inspector showed false, 0, "" or an empty list at the same time, so what the node read was not what the editor said. 97 of 135 inputs of the built-in nodes were affected. - inlineValue() falls back to the default the inspector shows. One place covers every node, also future ones and saved projects. - A default list is handed out as a fresh copy per read, so a node that edits its input in place cannot change the shared default. - The node shows the value it reads: an empty text or list as (empty), an object or struct input without a link as (null). Live values while playing use the same words. - A key input without a key keeps showing a dash, like the key field in the inspector. - Visible changes in old projects: To text without input gives "" instead of "0", Select and First not empty give "" instead of null. Tests: - 8 new unit tests in tests/core/inline-defaults.test.ts, among them one over all built-in nodes that no typed-in input reads null. - The e2e check in module 6 reads the rows of an unlinked Name of node. The check sum stays the same. - ./check.sh: 167 files, 2434 tests, exit 0. e2e 6: 117 checks. 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> |
||
|
|
21d7149756 |
Let a nested behavior without a start node run from On start (#230)
A behavior built as a plain behavior, with On start and no Behavior start, did nothing when another behavior called it through Run behavior. The node only showed a warning. Such a behavior should run as a call too, without the author having to rebuild its entry. - New callEntries() in nested.ts returns the Behavior start node or, when there is none, every On start node. callProblem uses it too. - GraphInterpreter.call() follows all entries one after the other, in graph order, in the same frame and with the same variables. - Behavior end still returns what reaches it. A call through On start has no inputs, because only the start node declares them. - It is a fallback only. With a Behavior start present, On start nodes stay out of the call. - The node warning now reads "No start or On start node - does nothing" and only shows when neither exists. - Keep and local state machines behave as before. Tests: - ./check.sh: 163 files, 2396 tests, CHECK EXIT 0 (before 2392). - 4 new unit tests in tests/core/nested-behavior.test.ts: the flow after On start runs, the return value arrives, several On start nodes run in graph order, a start node keeps On start out. - e2e 19: ALL PASSED, 35 checks. Only the expected warning text changed, the check sum stays at 1385. - Counter-checks: without the fallback 3 tests fail. With only the first On start exactly the order test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
454d202109 |
Let each variable of a behavior choose whether calls keep it (#229)
The first version of #229 put a Keep state switch on the Run behavior node. The user wanted it where the variable is declared instead. Whether a value lives on belongs to the variable, not to every call site. - The node switch keepState is gone again. RunBehavior, CallResult and nested.ts are back to what they were before #229. - BehaviorAsset.keptVariables lists the kept names. It lives on the asset and not in PropertyDef, because that is the schema of every inspector row. - A variable added from now on starts with Keep on. Variables in existing projects start with Keep off, so old projects run unchanged. - SetVariableKeptCommand flips the switch as one undo step. Add, remove and rename carry the list along. Loading drops names that are not declared. - The interpreter keeps only the listed variables, per Run behavior node and per object as before. The others start at their defaults. - Local state machines in a called behavior start empty on every call again, as before #229. Global and scene machines are unchanged. - The inspector shows a Keep button in each variable row of a behavior. Its state is part of the asset signature, so a click redraws the row. Without that the button kept its old look until the inspector was reopened. Found: the project Garden of Sacrifices still has keepState = true on the call Player -> Character Movement. It is ignored now. Direction there predates the switch, so Keep is off until set by hand. Tests: - ./check.sh: 163 files, 2392 tests, CHECK EXIT 0 (before 2388) - tests/core/keep-state.test.ts rewritten, 15 tests - e2e 19: ALL PASSED, 35 checks. 19c-ii now clicks the Keep button of a new variable: on when added, off by a click, on again, off after undo. - Counter-checks, each fails its own test: keep list ignored (2), states kept (1), new variable not kept (8), rename drops the list (1), no Keep button (e2e 19c-ii). 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> |
||
|
|
03e7a78c3d |
Keep a tilemap with outlined solids at 60 fps in scene and play
A project with three tilemap layers, two of them with collision on water and hill tiles, ran at 33 to 52 fps. A CPU profile showed that almost all of the time went into the collision geometry of the map. It was rebuilt from scratch every frame although nothing had changed. The animated water makes the views redraw every frame, so the cost was never skipped. - New module tilemapMemo keeps what is derived from the cells: the tiles of a layer, the blocking cells, the rectangles and polygons. It is reused as long as the content stays the same. - A brush stroke paints into the existing cells object. The memo therefore compares the content cell by cell, not the identity. The comparison walks two arrays in insertion order. - Clipped outlines are kept per outline list. The model always replaces that list instead of changing it. - PhysicsSystem no longer rebuilds the shapes of a tilemap each tick while its cells, placement and settings are unchanged. Before, it also built a JSON key from all world shapes every tick. - The overlaid layers after Set tile in a run stay stable. Otherwise the memo would miss on every call after the first edit. The #184 work only helped Move and raycasts, not this path. Measured on stein2 (i5-7500T, HD 630), script time per frame: - Scene, nothing selected: 17.7 ms, 52 fps -> 5.8 ms, 60 fps - Tilemap selected: 26.2 ms, 38 fps -> 8.6 ms, 60 fps - Play: 29.9 ms, 33 fps -> 8.7 ms, 60 fps About 3 ms per frame remain. The content comparison costs roughly 60 us per 700-cell layer and runs about 40 times a frame. The mesh geometries in TilemapField are still rebuilt every frame. Tests: - ./check.sh: 162 files, 2377 tests, CHECK EXIT 0 (before 2370) - e2e 1-5 and 39-43: ALL PASSED, 289 checks - 7 new unit tests in tests/core/tilemap-memo.test.ts - 5 counter checks (content ignored, no memo, outline by kind only, physics always rebuilds, layer settings ignored) each fail their test. All originals are back, checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af6ac12ab5 |
Show sharp tile preview labels and paths with tunnels and crossings
The mask labels under the tile preview sat inside the zoomed board. Pixi rendered them at 10 px and stretched them with the zoom, and each label was shrunk to the width of its tile on its own. They came out blurry and in different sizes. The example map also only showed a ring, so narrow paths and crossings were never previewed. - The labels live in their own layer in screen space and are re-rendered at the fitting font size on every zoom. - All labels share one size, at most the 10 px of the theme. Below 6 px they are hidden. - A label has two lines: the sides on top, the corners dimmed below. - The example map keeps the ring cell for cell and adds a network of 1 tile wide paths below it. It has straight 1xN and Nx1 pieces, curves, two T junctions, a crossing and a dead end. - The map stands next to the grid in a wide panel and below it in a tall one. Fit picks the layout with the larger tiles, and only Fit changes it, so zooming does not make the board jump. - The new hook __gm.tile.captions() reports size, scale and lines. Found on the way: the taller map made Fit zoom out further, and 3 pixel checks on the 16 px terrain tiles failed. The side by side layout fixed that. Tests: - ./check.sh: 160 files, 2364 tests, CHECK EXIT 0 (was 2363). - e2e 39: ALL PASSED, 29 checks (was 28). - New unit test: the example map has straights, a curve, a crossing with four notched corners, a T junction and a dead end. - Counter check: with the labels back in the zoomed board the new caption check fails (scales [0.648, 1]). - The e2e sum in the README is now 1381. It is calculated only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5dbac3a8df |
Add a Select node that picks one of two values by a condition
Choosing between two values meant a Branch with the whole action twice behind it, for example two Play animation nodes that differ only in the animation. Every later change had to be made in both. Select picks the value, so the action stays single. - Select is a data node with the inputs condition, false and true and the output value. It passes on "true" if the condition holds, else "false". An unconnected condition counts as false, and an empty picked input gives null. - The ports are named false and true, not a and b, so nobody has to guess which input is which. The order false, true follows Unreal's Select and Blender's Switch. - false, true and value share one type through `sharedType`, like First not empty. With two Animation literals plugged in, value takes their type and fits Play animation directly. - Both inputs are always evaluated. Data nodes have no side effects, so that does not matter. Tests: - ./check.sh: 160 files, 2363 tests, CHECK EXIT 0 (before 2356). - 5 cases in tests/core/node-catalog.test.ts and 2 in tests/core/shared-port-type.test.ts. - Counter checks: with the inputs swapped all 5 catalog cases fail, without sharedType exactly the type test fails. - No e2e change, the sum stays at 1380. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ffb5bbe039 |
Stop a key getting stuck when Shift changes it while held (#227)
Input remembered keys under KeyboardEvent.key. Pressing a, adding Shift and letting go sent "A" on release, which matched nothing, so "a" stayed down until the next release without Shift. - New rule keyName in src/core/keys.ts lowercases a single character. So a and A are the same key, also under Caps Lock. - Input remembers per physical key (e.code) under which name it went down and releases exactly that name. So 1 released as ! is freed too, which lowercasing alone does not cover. - On key down, up and held, Key is down and Axis input compare with the same rule. A node that already stores A reacts to a. - Recording a key in the inspector stores Shift+A as a. - Limit: holding Shift before pressing 1 still sends !, and a node on 1 does not react. Nothing gets stuck by it. Tests: - 3 new unit tests in tests/core/behavior-events.test.ts: the flow from the issue, 1 released as !, and a node on A hearing a. - Without lowercasing exactly the issue test and the A test fail. - Without the code memory exactly the 1/! test fails. - ./check.sh: 159 files, 2340 tests, CHECK EXIT 0 (before 2337). - No e2e change, the check sum stays at 1377. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4b7e8614a8 |
Keep play start and game view scale in the project (#226)
Both settings lived in the browser's localStorage, so they did not travel with the project and differed per machine. They now belong to the project like the symbol scale. There is no migration, old projects load with the defaults. - New module src/core/model/playSettings.ts reads playFrom and gameScale on load. Unknown or missing values fall back to the start scene and 100%. - src/editor/settings.ts is gone. - Play starts in the project settings dialog is an undo step (SetPlayFromCommand). It is only recorded on a real change. - The game view scale is saved with the project but is no undo step, so Ctrl+Z never undoes a zoom. A scale picked during play is written to the stored project and survives Stop. - Save states of persistent Variables assets (saves.ts) stay in the browser on purpose. They are runtime data of the game, not settings. Tests: - 6 new unit tests in tests/editor/play-settings.test.ts: defaults, save and load, broken JSON, undo of Play starts, scale without undo step, scale picked during play survives Stop. - e2e 03g checks the value in the project, nothing in localStorage and no new undo step. 35f also checks project.playFrom. No new checks, the sum stays at 1375. - Counter checks: no write to the stored project in play, no read on load, and an undo step for the scale each fail exactly their test. - ./check.sh: 158 files, 2332 tests, CHECK EXIT 0 - e2e 1-17,28,30,35,38-40,46: ALL PASSED, 752 checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5afb70a92e |
Let the project set the size of scene view symbols
Pixel art projects draw at very different sizes, so the fixed 18 px icons of behaviors, containers and rigidbodies were too big in some projects and too small in others. The size now lives in the project settings and travels with the project. - New field Symbol scale in the project settings dialog, below Play starts. It takes 0.1 to 4 with two decimals and defaults to 1. - The value is stored as project.symbolScale. Older projects load with 1, and values out of range are clamped on load. - Every change is its own undo step through a project command. - The factor scales the icon size, the spacing of icons that share a spot, the label offset and the selection frame of icon-only symbols. - The footprints in bounds.ts that Fit uses stay as they were. The hit area follows the icon, so clicks match what is drawn. - uiHooks reports the icon size so e2e can measure it. The e2e sum in the README is 1375. That is 1374 plus one new check, and 1374 itself has not been confirmed by CI yet. Tests: - ./check.sh: 157 files, 2326 tests, CHECK EXIT 0 (before 2320). - New tests/core/symbol-scale.test.ts with 5 tests (default, save and load, clamping, undo, icon frame), one more in project-settings.test.ts. - e2e 46f: dialog opens with 1, 0.5 plus Apply gives a 9 px icon, undo gives 18 again. Module 46: ALL PASSED, 13 checks. - Counter checks: without loading the value the save and load test fails. With the icon ignoring the factor exactly 46f fails. Both originals restored per cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2592873d76 |
Pull nearest tilemap uvs a tenth of a texel in, 0.02 still showed lines
At 0.02 texel the lines between tiles of a nearest tilemap were gone at the zooms tried first, but still showed at others. The user raised the inset to 0.1 by hand and checked it in the browser. - A tenth of a texel is still far below half a texel, so the texel picked at every pixel centre stays the same. - The comment names the new value. Tests: - tilemap.test.ts follows the value: the inset test expects 0.1/96, the sliver test allows up to 0.25 texel from the edge, and the quad test checks its uvs to 2 digits instead of 3. - ./check.sh: 156 files, 2314 tests, CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5bf1ee7a4e |
Let Equals match a number against a typed number like 130 (#224)
The float tolerance from the last commit never reached the case it was built for. The inputs of Equals are of type any, and their inline field is a text field. A typed 130 therefore arrives as the string "130", and the tolerance only applied when both sides were real numbers. The node fell back to comparing "129.99999999999997" with "130" as text. - A number against a text that parses as a number now uses the same 1e-9 rule as two numbers. - An empty text does not count as 0. - Two texts are still compared as text, so "1.0" and "1" stay different. - Compare was not affected, its inputs are real number fields. Tests: - 3 new cases in node-catalog.test.ts: 129.99999999999997 vs "130" is equal, 0 vs "" is not, "1.0" vs "1" is not. - Counter-check: with the old rule (two real numbers only) exactly the "130" case fails. Original restored and compared with cmp. - ./check.sh: 156 files, 2314 tests, CHECK EXIT 0 (before 2311). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9d6a0aebd3 |
Let Compare and Equals treat float noise as equal numbers
A graph that checks "time - late == 6" after On timer (#224) failed, because the difference comes out as 5.999999999999972. Both values are sums of measured frame times, and they round along different paths. The only workaround was a Round node in front of every comparison. - New helper nearlyEqual in values.ts. Two numbers count as equal when |a - b| <= 1e-9 * max(1, |a|, |b|). The tolerance grows with the size of the values, so it holds for hours of summed dt. - Compare uses the rule for all six operators. "<" and ">" only hold when the values are not nearly equal, "<=" and ">=" also hold when they are. So "==" and ">" are never true at the same time. - Equals uses the same rule when both inputs are real numbers. Objects still compare by identity, everything else as text. The node description says so. - The tolerance is fixed, there is no parameter on the node. Physics values come from float32 in Rapier and are not covered on purpose. - The log display is unchanged and still prints the raw number. Tests: - ./check.sh: 156 files, 2311 tests, CHECK EXIT 0 (before 2300). - 11 new cases in node-catalog.test.ts, including the value from the log against 6, growth with magnitude, 6.001 != 6 and 1e-6 > 0. - Counter-check exact compare: exactly the 8 noise cases fail. - Counter-check "<"/">" without the exclusion: exactly 2 cases fail. - No e2e change, the check sum stays at 1374. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6fecb2e841 |
Tell On timer how late its frame is, so time minus late is exact (#224)
A timer can only fire on a frame. It fires on the first frame past each due time, so Time.time at that moment is the due time plus up to one frame. On a display that is not exactly 60 Hz this delay creeps up by a millisecond every few seconds and falls back to zero once it reaches a whole frame. The issue saw 82.009 after 82 ticks. The timer itself does not drift, it keeps the remainder when it fires. - On timer gets a number output late. It is what is left of the timer's clock after taking one interval, so time - late is exactly the due time. - When one long frame catches up several ticks, each gets its own late. The first one was due longest ago and is the latest. Tests: - New unit test in behavior-events: late 0.125 after a frame past 1.0 s, 0 on the dot, 1.5 and 0.5 for two ticks caught up in one frame. - Counter-check: late taken before the interval is subtracted fails exactly that test. Original restored with cmp. - ./check.sh: 156 files, 2300 tests, CHECK EXIT 0 (was 2299). - No e2e change. The e2e modules only wire the exec of On timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aef1eba917 |
Stop thin lines between tiles of a nearest tilemap at odd zooms
At zoom levels that do not land quarter edges on whole screen pixels, a tilemap whose texture uses Nearest showed one-pixel lines in the wrong colour. The float uv of a pixel centre right at a quad edge could round onto the first texel of the neighbouring quarter in the sheet, which is usually a different tile. Terrain sheets showed it most, because every tile is four quads there. - Nearest now pulls the uvs 0.02 texel inside each quarter. That is far below half a texel, so every picked texel stays the same and pixel art keeps its size. - Linear keeps its half-texel inset. - The first draft used 0.01. The user raised it to 0.02 after checking the result in the browser. Tests: - ./check.sh: 156 files, 2299 tests, CHECK EXIT 0 (before 2298). - New unit test: every nearest uv sits strictly inside its quarter, by less than 0.05 texel. Two older tests now expect the inset. - Counter-check: with no inset for nearest the new test and the inset test fail. - No e2e check. The user checked the result in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c1a702bc39 |
Add a terrain autotile kind for 6x4 sheets like TD Plains
Sheets such as "TD - Tileset - Plains" draw an autotile as a 6 x 4 block: a 3 x 3 nine-slice, a vertical and a horizontal strip and a single tile on the left, the inner corners on the right. The floor kind only knew the RPG Maker 2 x 3 layout, so these sheets could not be used as autotiles at all. - New TileKind "terrain" with 16 px tiles in a 6 x 4 block. All 47 neighbour combinations are resolved per quarter, so the renderer, the collision and the palette stay untouched. - Whole tiles are used where the sheet draws them: the 16 side cases, the four single notches and the two diagonal pairs. Hand-drawn strips and single blocks therefore look as drawn. - Only the missing cases are composed, for example an edge with a notch or two notches side by side. They take the side tile and put the notch quarters in. - A notch also takes the quarter above or below from its notch tile when fill would stand there. Tall cliff notches keep their wall. On ground and stones fill meets fill, so nothing changes there. - The label rule for floor and terrain now runs through one notch function. Floor still passes the MZ table test. Not touched: the tile board only refits on open and on Fit. Switching an open tile to a kind with another tile size leaves it small until Fit is pressed. That was the case before. Tests: - ./check.sh: 156 files, 2290 tests, CHECK EXIT 0 (before 2283). - e2e 39,40: ALL PASSED, 90 checks (before 86). 39j checks 47 tiles, pixels of single, notch, pair and composed tiles, and undo to floor. - Counter-checks: without whole notch and pair tiles one unit test and one e2e check fail. Without the taken quarter one of each fails. Originals restored by cmp and rebuilt. - Check sum: 1363 measured by CI plus 4 is 1367, computed only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |