b0d0868f558b90b4dc6bafb75520caab24f03304
98
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
3abb6894a7 |
Let a container sort its content with its parent (#243)
A container is not drawn, so it had no place in the draw order. The chain from its content to the parent of the container broke there, and a container under a character could not be drawn together with it. - A container gets the switch Sort with parent under Z. It defaults to off, also for new containers, so projects draw as they did. - With the switch on, the whole content is drawn in the slot of the container's parent. The content's own switch does not count there. - Containers in a row pass the content on as long as each has the switch on. On the top level or under a tilemap it has no effect. - drawSlots follows the new sortsWith field of a draw item. Scene view, game and picking all use drawSlots, so all three agree. Found on the way: a scene object hung directly under a container spells the container out, and its template content is dropped. That is the existing spelledOut rule, the tests now keep both cases apart. Tests: - ./check.sh: 178 files, 2573 tests, CHECK EXIT 0 - 7 new unit tests in sort-with-parent.test.ts: the switch, template content, override per container, a chain, top level and tilemap, game and picking - e2e 5b-iii: a real click on the switch in the inspector, with undo - 4 counter-probes over #242 and #243 bite (2 unit, 2 browser) 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> |
||
|
|
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> |
||
|
|
50854389bb |
Let a tilemap layer sort its rows by Y with the objects (#228)
A tilemap layer was one piece in the draw order, so a figure stood either in front of a whole layer of trees or behind all of it. For a top-down game the trees in front of the figure's feet have to cover it and the ones behind must not. - A new "Sort by Y" box under the layer list sets `sortByY` on the painted layer. Every change is its own undo step. - Such a layer comes as one draw slot per row. On its `z`, the rows and all objects on that `z` are sorted by where they stand: the bottom of an object's box, the bottom of a row. Ties keep the hierarchy order, and other `z` values are untouched. - A layer drawn as one piece and the map's own node stay behind the sorted ones on the same `z`, since neither has a single place to stand. Ground belongs on a lower `z` or stays unsorted. - A new tile property "Sort offset" (whole rows, -16 to 16) lets a crown sort with the trunk below it. - `TilemapField` draws a container per row for such a layer, and picking takes the row into account, so a click picks what is drawn in front. Scene view, game view and play share the same rule. Limits: on a rotated map "down" is world Y. A layer sorted by Y costs about one draw call per row and tile instead of one per tile. Tests: - ./check.sh: 160 files, 2350 tests, CHECK EXIT 0 (before 2340). - 10 new unit tests in tests/viewport/sort-by-y.test.ts. - e2e 39-43: ALL PASSED, 148 checks. New check 40p-iii ticks the box and finds the slot of row 0, undo brings back the whole layer. - Counter checks: without the Y sort 5 unit tests fail, with the offset ignored 5, with no row in picking 1. If TilemapField draws no rows, exactly 40p-iii fails in e2e. - The e2e sum in the README is 1378, counted, not yet seen in CI. 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> |
||
|
|
9ef360ba1b |
Let the game view show the picture at a chosen scale (#225)
The game view always stretched the camera picture to fit the pane.
Pixel art came out blurred at odd factors, and there was no way to
see the game at its real size. The view now has a header with a
scale drop-down, and 100 % is the default.
- The header of the new GamePane offers Auto, Fit, 25 %, 50 %,
100 %, 200 % and 400 %. Auto and Fit show the factor they land
on, e.g. "Auto · 100%", and follow the pane when it resizes.
- 100 % means one screen pixel per pixel of the camera resolution
(width x height). The camera zoom scales the world on top of
that, so a zoomed camera still fills the same frame.
- Auto takes the largest fixed step that fits the whole picture
and falls back to 25 % when nothing fits. Fit keeps the old
behaviour.
- A picture larger than the pane is cropped around the camera
centre. Fixed steps start on a whole pixel so pixel art stays
sharp at 100 %.
- The choice is kept in the browser like "Play starts". Nothing
is written into the project.
Found on the way: the first draft applied the step to the world
instead of the camera resolution. The camera zoom then had no
effect at 100 %, which the existing check 12c caught (zoom 1.37
measured as scale 1).
The CI smokes were not run locally, because that would reseed
projects the user works in. All three smoke cameras are 640x360
and fit the 670x383 harness view at 100 %.
Tests:
- ./check.sh: 156 files, 2298 tests, CHECK EXIT 0 (before 2290).
- 8 unit tests in tests/viewport/view.test.ts for whole-pixel
steps, cropping, Auto and Fit with fallback, and zoomed cameras.
- 7 e2e checks in 03g. e2e 1-17,28,30,38-40: ALL PASSED, 727
checks.
- Counter-checks: no cropping, no rounding, no factor reported to
the header, and no zoom share each fail their own tests.
- Check total: 1367 (CI at
|
||
|
|
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> |
||
|
|
d444b27fae |
Let behaviors read and set tilemap cells while the game runs (#135)
A behavior could move sprites around but had no way to look at or
change the tilemap it was standing on. Doors, breakable blocks and
"is there ground ahead?" checks all need that.
- New nodes Tile at, Set tile, World to cell and Cell to world. The
conversion honours position, rotation, scale and mirroring of the
placed map. A small Tile literal node lets a graph compare what it
read against a fixed tile.
- The layer is picked by name from all tilemap layers in the project
through a new field kind. "First layer" takes the map's first one.
Because layer is an input, a text can also be wired in.
- Each placement keeps only its edited cells in liveTilemap.ts,
layered over the asset's cells. A second placement of the same map
stays untouched, and the asset itself is never written.
- A read sees a Set tile from earlier in the same flow. Drawing picks
up the edit at once and neighbouring autotile edges adapt.
- Solids see the edits straight away, so a Move in the same frame
already hits the new cell. Rapier rebuilds its shapes on the next
step.
- After Stop the map is back to what was painted. The editor run shows
the edit in the scene view while it lasts and does not touch the
project.
- An unwired tilemap input means no map, like Get position. Cells
outside the grid are refused and done reports false.
Tests:
- ./check.sh: 155 files, 2271 tests, CHECK EXIT 0 (before 2245).
- tests/core/tile-access.test.ts (14), one physics test for a floor
set at runtime, catalog entries for the five nodes.
- e2e 39,40: all passed, 86 checks (before 81), five new in 40s.
- Counter-checks: collision ignoring edits fails 3 unit tests, the
solids cache missing same-tick edits fails only the "at once" test,
drawing ignoring edits fails the 2 drawing checks, the editor run
not reporting a new tile fails only its check.
- 40s uses a still tile. The animated fixture tile redraws every frame
and hid the last counter-check at first.
- Check total 1351 + 5 = 1356, computed. CI confirmed 1351 at
|
||
|
|
1aae97b335 |
Open a sprite's texture or shader from the menus and inspector (#221)
A sprite's texture and shader were only reachable by hunting for them
in the asset panel. Every place that shows such a reference now leads
straight to it.
- Object menu (hierarchy and scene view): "Open texture" and "Open
shader" sit right below "Open asset" when the sprite has one. The
object's own values count, so an overridden shader opens the
override.
- Asset context menu: the same entries at the top.
- Inspector: every asset field with a value shows its label as a blue,
underlined link. One click selects the asset, a double click opens
it. An override stays gold and gets the underline too.
- The first click rebuilds the inspector, so the second one lands on a
different row. The inspector catches that second click before any
row sees it.
- New pure rule `references.ts`: the asset behind a reference field. A
frame leads to its sheet. Kinds without a tab (palette, font, sound)
get no menu entry.
- `followsPress` in `clicks.ts` keeps the double-click window and
tolerance in one place.
- `activateAsset` in `actions.ts`: opening means the same everywhere
as a double click in the asset panel, so a scene also becomes the
active one. The asset panel uses it too.
Tests:
- ./check.sh: 150 files, 2226 tests, CHECK EXIT 0 (before 148/2214).
- 12 new unit tests: 6 for the reference rule, 4 for the link in
createFieldRow, 2 for followsPress.
- 5 new e2e checks in 28l-ii: both menus, the link's look, click
selects, double click opens.
- e2e 1-16,26,28,35,39,40 on the final build: ALL PASSED, 732 checks.
- Counter-probes: no "Open ..." entries, no underline and a second
click never recognised each fail exactly at their checks.
- Check total 1341 (CI counted 1336 at
|
||
|
|
0e8585665a |
Load the 500 weight and draw missing glyphs as SVG icons (#214)
The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.
- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
DOMParser.
- Tests find icons through the SVG id. The new hook
__gm.locateIcons(id) and the harness helpers findIcon/clickIcon
replace locate() on the old characters.
Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.
Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
character in src/ exists in every shipped face. The cmap reader
matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
back, the struct icon removed (one unit test each). In the browser,
500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at
|
||
|
|
056a622524 |
Refactoring round 6: shared sheet canvas, fewer copies (#85)
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).
- SheetCanvas is the new base of the texture, pixel art and vector art
canvases. It holds background, mask, hint, panning, wheel zoom, fit,
view save and restore and the screen mapping. Pixel art keeps its
whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
GeneratedPictures.has and SnapToggle.snapping. Tests now check the
same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
wiki pages.
Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
has not been called since #128. The rectangle tool therefore records
a no-op undo step when it paints the same tile over itself. Left as
is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
could become 10 without a red test. tests/core/raycast.test.ts now
covers it.
- CI measured 1308 editor checks at
|
||
|
|
940a78e8ee |
The editor ships its own font, Source Sans 3 and Code Pro (#202)
Theme named Inter and JetBrains Mono, but neither was ever shipped, so every machine drew the editor in whatever it had installed. The e2e runs measured text in a different face than the user saw. - Five Latin woff2 faces live in src/ui/fonts/ next to their OFL licences: Source Sans 3 400/500/600/700 and Source Code Pro 400. - fonts.ts loads them through FontFace, and main.ts waits for them before the first widget exists, so Pixi measures the real face. - A face that fails to load is reported in the console and the system fallback in Theme takes over. - Theme is retuned by hand: 12 px body, 22 px rows, smaller gaps and padding, radius 8, and weight 500 for bold and headings. - The scene background default follows Theme.colors.graphBackground. This makes core import from ui for the first time. Findings: - The 500 file is in the folder but fonts.ts does not register it yet, so weight 500 currently falls back to the 400 face. Tests: - ./check.sh: 141 files, 2080 tests, exit 0. - e2e not run. Checks that measure widths or row positions are expected to need updating for the new metrics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
66d6aafbb4 |
Pixel art can be drawn in its own tab with layers and frames (#130)
Textures are uploaded files named by their content, so they cannot be edited in place. Pixel art is therefore its own asset type, like vector art. Its pixels live in the project and the tab rasters them into a texture that sprites, tiles and animations can use. - New asset types Pixel art and Palette. The first pixel art of a project gets a palette with the 16 PICO-8 colours in the same undo step. Every pixel is a palette index, 0 is transparent. - Each layer on each frame is its own file data/pixels/<sha256>.px. The project only holds the names, so undo swaps names. The browser computes the hash and uploads new cels before the project is saved. - New route POST ?r=pixels in api/lib/PixelStore.php. It answers 201 and 422 for broken data. data/pixels/ is git-ignored. - Tools: pencil, eraser, fill (contiguous or all pixels of a colour), eyedropper, line, rectangle and ellipse (outline or filled), brush size 1 to 8. - Layers can be added, deleted, renamed, reordered, hidden, faded and merged down. Frames can be added, duplicated, deleted and reordered, with onion skin and a small playing preview. - Zoom 1x to 32x in whole steps, grid from 8x, a 3x3 tile preview and Resize... with a 3x3 anchor. - All frames sit side by side in one image. Each frame is a region like a cut texture frame, so texture rows offer "Asset / Frame 2" and the context menu has Create animation from frames. Decisions: - Merging down drops the opacity of the upper layer. Indexed colours know no blends. - Only the last palette colour can be removed. Otherwise the indices of every image using the palette would shift. - Undo stores the whole layer per stroke instead of pixel diffs. - Alt+click picks a colour. It is a mouse gesture, not a shortcut. Follow-up issues: #197 import from texture, #198 selection and flipping, #199 PNG export, #200 more tools. Tests: - ./check.sh: 136 files, 2049 tests (before 135 / 2011). 34 new in tests/core/pixel-art.test.ts, 4 from the icon tables. - e2e module 48 fresh: ALL PASSED, 29 checks, console empty. - Counter-probes bite: five on the unit tests, upload order (2 tests fail) and cels not uploaded in the browser (1 check fails). - Check total 1259 in README is computed (1232 measured in CI + 27). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
592ea942d9 |
Vector shapes can fill with gradients and textures (#186)
A vector shape could only be filled and outlined with one flat colour. Shading, glows and patterned surfaces had to be faked with stacks of shapes. Every shape now has a paint type for its fill and its outline. - Fill type: solid colour, linear gradient, radial gradient or texture. Line type offers the same without texture. - Colour stops are edited on a bar in the inspector, as agreed in the issue. A click on the bar adds a stop in the colour the gradient already has there, so adding never changes the picture. At least two stops remain. - The new Paint tool in the tab shows two handles for the selected shape: axis start and end, centre and radius, or the corners of one texture tile. "Handles: Fill / Outline" picks which paint is edited. - Moving a whole shape moves its gradients and tile with it. Every gesture is one undo step. - A texture fill always tiles, because Pixi turns a non-repeating pattern into a repeating one anyway. When switched on, one tile covers the whole shape. - Only uploaded textures and frames can be a fill, never a drawing. A drawing filled with itself would rasterise itself mid-raster. - The fingerprint of a drawing includes the texture and frame a fill points to, so the picture redraws when the image is swapped. Findings: - graphics.destroy(true) also destroyed the textures of the fills. With a texture fill it deleted the tile slice and the next redraw crashed on reading 'scaleMode' of null. It is now destroy(). - The gradient cache destroyed old gradients itself, which gave 20 Pixi warnings "destroyed while still bound". They are now handed to Pixi's texture GC with autoGarbageCollect switched on. The console of module 47 is empty again, the last CI log had 0 of them. Tests: - ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44), 0 console warnings - 25 new unit tests, 10 new e2e checks in block 47i - Four counter-checks bite, originals restored and verified with cmp - Check sum 1232 in README and wiki, calculated (1222 + 10) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3427f84a55 |
A game can pause itself and set its own time scale (#167)
Until now only the editor toolbar could pause a running game. A pause menu inside the game had no way to stop the world while its own graph kept running, and there was no slow motion. - Five new nodes: Pause game, Resume game, Is game paused, Set time scale and Time scale. Pause and resume take effect on the next frame. - A game pause stops world time, physics, animations, particles and shader time. Only behaviors with the new "Run while paused" switch keep running. - Set time scale 0 freezes the world but graphs keep running, as agreed in the issue. - Play and Stop reset the time scale to 1, Load scene keeps it. The pause belongs to the scene, so Load scene and Restart unpause. - The toolbar pause stays separate and also freezes the menu. When the game pauses itself the status line reads "The game paused itself". - While paused, collision events are not reported, sounds keep playing, and the editor run ignores both nodes with a log line. Findings: - The first status line was longer than any before and got cut with an ellipsis. It is shorter now. - The e2e harness did not know the key "p". It does now. Tests: - ./check.sh: 133 files, 1968 tests, exit 0 (before 132 / 1952) - npm run build: exit 0 - GM_STEPS=18 fresh: ALL PASSED, 33 checks (before 26) - 16 new unit tests: 8 catalog cases, game-pause.test.ts, editor run - 7 new e2e checks in block 18i, driven by real keys in the game - Six counter-checks all bite: behavior filter, collision lock, world time, editor run, clock reset and the game clock in the browser - Check total 1217 in README is computed (1210 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4ec23d3d3 |
Colliders take a frame's outline and tiles have their own surface (#193)
Issue #193 asked where friction, bounciness and density of a tile live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a collider could not use the outline drawn on a texture frame (#141). Both gaps are closed here. - A collider has a fifth shape "Frame collision" with a Frame row. The frame is picked like a sprite's texture and stored as the value 'frame'. - The shape is linked, not copied. Redrawing the outline in the texture tab changes every collider using that frame, in the editor and on the next play. - The frame centre sits on the collider origin, one image pixel is one unit. The shape turns, scales and mirrors like any other. - Every outline ring blocks. Each ring is split into convex parts the same way a polygon is. - Without a frame, with a whole texture or with a frame that has no outline, the collider is drawn red as a box and blocks nothing. - Move, Rapier, raycast, detection, hit test, drawing and footprint all resolve the frame. At runtime an entity looks the frame up live in the current project. - A tile has Friction and Bounciness next to Solid and One-way. The rows only show when Solid is on. Defaults and mixing rules match the collider. Density is left out because a tilemap never moves. - Tilemap cells are grouped by surface as well as by one-way angle. Ice and stone never merge into one rectangle, and every cell brings its surface to Move and Rapier. Findings: - A tile with a collision scale below 1 is no longer flush with a neighbour of another surface. It is inset there like at an open side, the same as next to one-way tiles today. - A bounce test first measured 0.84. The ball had landed on the hero at the origin, not on the tile. On the tile it measures 0.993. Tests: - ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917). - 14 new unit tests in tests/core/surface.test.ts. The Move tests run against the editor run and the physics system. - 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider has frame size, a walker climbs the slope in the frame). - GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks. - Three counter-probes bite: Move ignores the tile surface (2 fail), Rapier ignores it (1 fails), frame shape falls back to a box (5 fail). - Check sum 1198 is computed (1195 + 3), the full run belongs to CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bdb3f066cf |
The asset type "vector art" exists as a model, which is stage 1 of #129 and shows nothing in the browser yet on purpose: a vectorart asset is not placeable, sits in the + menu and in the asset panel with its own icon (a curve between two points) and has no tab, so it behaves like a font - selectable, its rows in the inspector, a double click leading nowhere - because the tab is stage 3 and half an asset would only be in the way; the data model in Asset.ts is a shape built from rings (even-odd, so a shape may have holes, answer 6 in the issue), a ring being points with optional bezier handles in/out stored as an offset from the point and a closed flag (an open path is stroke only, answer 4), plus fill colour, opacity and an optional stroke (answer 5) - the two handles are kept apart instead of mirrored, because what "smooth" means is the editor's business and not the model's; canvas width, height and the filter are properties, so they get an inspector row and undo like any other, while the shapes are intrinsic data like the frames of a texture; the revision that #185 hands to the picture path is a fingerprint of what is drawn, not a counter, which is exactly why that key took a string token - no command can forget to bump it - and the name of a shape is deliberately left out of it, renaming changing no pixel; six undo commands in the new vectorArtCommands.ts (add a shape numbered in order, remove one which comes back to its old place - unlike a tilemap layer the last one may go too -, rename, move, write the rings as one step per gesture with the list copied, and set the style where stroke: null takes it away) and the pure function shapeOrderTarget for the four buttons of the planned inspector list (one forward, to front, one back, to back), which also serves dragging in that list; loading from a project file is strict - a point without numbers falls away, a broken handle leaves a corner behind, duplicate shape ids fall away like duplicate layer ids, a stroke of width 0 is no stroke - while a ring with a single point, which is what someone is drawing right now, stays; three findings on the way: JSON.stringify as the fingerprint was wrong because the same drawing came back from a project file as {points, closed} and was built as {closed, points}, so a reload would have rasterized the same picture under a new key (the fingerprint writes its fields in a fixed order now and a test measures that case), toNumber in serialize.ts turns any nonsense into 0 so the Number.isFinite checks ran into nothing and {x: 'far'} became the left edge instead of vanishing (shapes read numbers strictly via asFinite, 0 being a place on the canvas here), and "Vector art" would have been both a menu entry and the panel heading, which an existing test catches because a click by text would hit the heading - the group is called "Vector drawings" now while the menu entry stays "Vector art"; 20 new unit tests in tests/core/vectorart.test.ts and three counter-checks hold: the fingerprint back on JSON.stringify fails 2, reading the numbers loosely again fails 4, and not copying the ring list fails 1; no e2e check and the check sum stays 1157, only module 04 counting 19 asset types instead of 18 (#129)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cb1804dcf2 |
A picture in the editor may now be drawn instead of uploaded, which is the piece #129 (Vector art) and #130 (Pixel editor) both need and neither could build alone: TextureRef.file has been a file name on the server, isTextureFile() checking a sha256 hash plus an extension and TextureLibrary fetching it over HTTP, so an asset that paints its own image had nowhere to put itself - the key now also takes the form gen:<assetId>@<revision>, which an uploaded file can never look like, and everything that only passes a ref along (sprite, tile, tilemap mesh, particle source, shaders, aspect ratio) is untouched because it still sees a Texture; the revision is a string, not a counter, which is the one deviation from the plan in the issue and costs nothing - gen:a1@7 and gen:a1@c0ffee go through the same door, so a generator may fingerprint its own content instead of relying on every command to remember to bump a number; two registries keyed by asset type as a plain string, so a plugin may bring its own type - registerPictureSource(type, read) in the new core/model/generated.ts tells the model canvas size and revision and resolvePicture() builds the ref from it (no rect, no slice, smooth from props.filter like a texture), and registerPictureGenerator(type, draw) in the new viewport/generatedPictures.ts hands out the finished texture, projectPictureFactory(() => state.project) tying the two together per session in App.ts; invalidation is a rule instead of guesswork at twenty call sites: GeneratedPictures<T> holds one picture per asset keyed on the revision and frees the old one as soon as the new is drawn, TextureLibrary destroying it and dropping the crops cut from it, while a generator that cannot draw yet is not remembered but asked again next time - the class imports Pixi as a type only, so the whole bookkeeping is measurable without a GPU; carried along are the picture picker (a drawn asset sits among the textures by name, without frames), the delete warning (counts tiles and animations for it too) and the asset context menu, where "Create sprite from picture" now covers it while "Create animation from texture" stays texture-only because it needs frames; one finding while probing - crop() could not have cut a generated texture at all, a RenderTexture having no source.resource to draw back from and drawImage running onto null, so it is handed out whole, a drawn picture having no sheet whose neighbouring row could bleed in; 18 new unit tests, 10 in tests/core/generated-picture.test.ts (building and splitting the key, an uploaded file is not one, no registry means no picture, resolving to canvas size, a new revision is a new key, a canvas without a size, the filter, the sprite size, picker order) and 8 in tests/viewport/generated-pictures.test.ts (drawing once, freeing the old revision and only for its own asset, null is not remembered, no key means nothing drawn, a session change gives everything back, peek), and three counter-checks hold: dropping generated assets from resolvePicture fails 5 of the 10 core tests, not freeing the old revision fails 1 viewport test, and narrowing the picker back to textures fails 3; no e2e check and the check sum stays 1157, because without a generator the groundwork shows nothing in the browser - that evidence comes with #129 (#185)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eac50e4194 |
Moving against a tilemap whose cells block with an outline no longer costs more than a whole frame: an outlined cell is its own polygon, split into convex parts, so a map that is a handful of merged rectangles when built from plain solid tiles is hundreds of solids under one entity - and Move brought every one of them into world coordinates and tested it exactly, including the ones on the far side of the map, which measured 148 ms per Move call on a 20x20 map with 16-corner outlines (34 ms at 8 corners, 0.4 ms with plain solid tiles), matching the report that standing still holds 60 fps while anything that moves stutters; the fix is a broad phase in two stages that decides nothing and only leaves out what cannot be reached - a move never ends further from its start than it is long, because sliding always shortens the rest, so every collider of the mover gets a rectangle grown by the move length and an obstacle whose rectangle does not meet it is never tested, a ray taking the rectangle around itself, and that rectangle is carried back into the map's own units with invert(m) and turned into cell coordinates (cellsWithin), a rotated map through its own axes, so a far cell is not brought into world form in the first place - solidShapesOf was O(cells) per call, which was the second cost block and the reason the 60x60 case still grew; CollisionSystem.overlaps takes the same range, so the editor run asks a map only for the cells within reach of the other side each frame, two maps narrowing nothing, and a Move obstacle now falls away at build time when its layer does not meet the mover's (#152) instead of inside the loop; the measurements after: 148 -> 0.26 ms, 34 -> 0.27 ms, 60x60 20 -> 3.5 ms, plain tiles unchanged; this is no regression of the recent commits - the same benchmark in worktrees against 947a6b1 (#141), 6a31f5e, 4f55f85 and ae3aa7d gives the same 85-148 ms, the cost has been there since outlined cells arrived with #141; new pure functions shapeBounds/grownBounds/boundsMeet in collider.ts and cellsWithin/cellsMeet in tilemapCollision.ts, 11 new unit tests in tests/core/broadphase.test.ts (the rectangles of all four shapes including a rotated box measuring its diagonal reach, touching counts as meeting, cellsWithin straight and turned by 90 degrees, a map handing out 400 cells without a range and under 10 with one, an 800 unit move from far away still stopping at the first cell, a rotated map blocking where its one painted cell was turned to, a ray across the whole map, and the editor run seeing a collider on a far cell), and four counter-checks hold: dropping the cell filter fails the counting test, padding without the move length fails 4 move tests, ignoring the rotation in cellsWithin fails 3, and a wrong range in the detection fails the touching test; one blunt test on the way - the first rotated map was painted on every cell, where even a wrongly computed cell range blocks and the counter-check stayed green, so exactly one cell is painted now and the test measures where it turned to; no e2e check and the check sum stays 1157, because a timing check in the browser would flutter under CI load while modules 40 and 42 cover the correctness anyway, the speed evidence being the node benchmark (#184)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ae3aa7d3bd |
Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away (#183), DEFAULT_FILTER (#152) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182, and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before (#85)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4f55f85ad4 |
Collision layers and masks: a collider carries a layer and a mask of the layers it meets, and the pair only meets when both masks name the other's layer - the rule is one pure module (src/core/model/collisionLayers.ts) with layerBit, layersMeet and the names a project starts with, so the three places that have to agree read the same function: the editor run through CollisionSystem, the Solids sweep per pair and Rapier, where the mask becomes the collider's interaction groups; the project file gained collisionLayers: ["Default"] (default one layer, no formatVersion bump), the collider schema two new kinds - layer as a dropdown of the project's layers and layerMask as a menu of tickboxes that stays open while it is ticked (Menu.ts learned rows that do not close on a pick) - and the names are edited through "Collision layers..." in the toolbar's Project menu, not a Project tab, which is the decision I made myself because the question ran into the MCP timeout: the dialog (src/editor/CollisionLayersDialog.ts) holds up to 16 names, Add layer appends "Layer <n>" and Apply writes all of them as one undo step (SetCollisionLayersCommand); a mask read from a file goes through a check that refuses anything that is not a finite number, because Number(null) is 0 and a collidesWith: null would silently have meant "meets nothing"; 9 new e2e checks in module 8 cover the menu entry, the dialog, Add layer, Apply and its undo, both inspector rows, the tickbox menu, and the mover walking straight through the crate that 8c blocked on once their layers no longer meet, and 21 new unit tests cover the rule, the schema and the command (#152)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5385d9e152 |
The one-way tolerance angle is a field: a one-way collider and a one-way tile gain One-way angle (0 to 90 degrees, default 45), a contact blocks when its normal is within that angle of the blocking side, without a value the limit is bit-identical to before, and the rule holds for Move, Raycast and dynamic bodies in Rapier; tiles with different angles never merge into one rectangle, the scene view draws the angle as a cone in the middle of the thick side, and the inspector shows Blocks from and One-way angle only while One-way is on, through the new schema option shownWith (#159)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a31f5e0ad |
One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
32057a96fc |
Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1b5e386b60 |
Move with collision reacts along the contact normal: it travels to the surface, stays flush and slides the rest of the move along it with Coulomb friction (the mean of both colliders, a tile counts as 0.5), a circle now rolls over an edge instead of catching on it, pushed dynamic bodies move off along the normal, Move gains the outputs hit, normal x and normal y, an object without its own collider moves freely, and editor run and play share the same path through Solids; contact point and normal come from our own f64 geometry (contactBetween), because Rapier's castShape lands 3e-6 px inside the wall in f32 (#150)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78899b1ace |
Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bffa508ee7 |
A tile gains Collision scale: below 1 each blocking cell pulls in on the sides without a blocking neighbour and stays flush towards its neighbours, so a wall of bushes becomes a narrower wall without gaps and the inner corner of an L closes cleanly; above 1 the cell grows at its open sides; on several layers the largest value counts, and the merged rectangles work in fractions of a cell with exactly the old result at 1 (#140)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4728f4fab5 |
A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c2fd536aaf |
The texture field only offers pictures: each texture with its frames, no animations any more; an animation id left in an older project shows as a missing reference (#143)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c411c1ff3 |
Tilemaps can block movement: a tile gains a Solid switch, the solid cells of all layers are merged into rectangles and the placed tilemap takes part in the collision system as one body, reported as other in collision events (#132)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
732f6518ae |
The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
33a7b1b63e |
Tilemaps are a new placeable asset type that is painted with autotiles in the scene view, with layers, brush, eraser and rectangle, and is drawn at runtime and in the export (#128)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1a9f07c4ac |
Autotiles are a new asset type with a tab that shows every tile of the floor (2x3) or wall (2x2) block and an animated sample pattern (#118)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2a390e52ff |
The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ed2edd0f0 |
An animation is a value a graph can pick, and playing a running one makes it win again (#97)
Play, Pause and Stop animation take the animation on a port instead of a fixed setting, fed by the new Animation literal or a list item tagged `$animation`. Playing an animation already running keeps its frame but counts it as started last, so turning back to a walk that never stopped shows that walk. The actor demo walks through one Play animation over `ItemAt(walk, facing)` again and stops every animation to stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
193378e6a9 |
An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes. - AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one property of the root or of an object inside it, keys sit on whole frames. - The last key decides the length (its frame counts as a frame of its own). The playhead runs between frames, so numbers and colors blend every tick even at 10 fps; everything else holds. - Playback gains `once-reset`, which puts back what was there before the start. A row can be relative, adding to where its target stood. - Play / Pause / Stop animation act on an object; each instance plays on its own, the later start wins on a shared property, and a behavior writing the same property still writes last. - A sprite no longer wears an animation through `texture`. "Create animation from texture" builds a sprite plus a texture row keyed frame by frame instead. - The Animation tab shows a placeholder until the timeline editor (part b) lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
94a0a578ed |
A border drawn in pixels is as wide as the texture says a pixel is, so a small sheet can frame a large sprite (#96)
A sliced frame drew its borders one source pixel to one sprite unit. Pixel-art sheets are small: a 16px corner on a 400-unit button is a thin line, and the only way around it was a larger image. The texture now carries `Nine-slice scale` (default 1), next to `Filter`. It sets how many sprite units one border pixel takes, for every sliced frame of that texture. The UVs stay as they are - a corner shows the same pixels, only larger or smaller. When the sprite is too small for both borders they are still scaled down together, now starting from the scaled size. With `Repeat` the tile size scales too, otherwise a doubled border would sit beside a pattern of the old size. The value is a schema row, so inspector field, saving and undo come without their own code. Anything that is not a size above 0 reads as 1, and documents without it draw as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cc6db5f350 |
Three stores, three importers and three pickers that differ only in their type are one of each (#85)
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts, and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list, check count 958, the two-job CI, the Scenes wiki page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c990dbcb26 |
A name that a sprite already owns cannot name a property, and a division by zero cannot answer (#63, #64)
game-maker / suite (push) Failing after 11m29s
#63: a shader Property node named "width" quietly took over the sprite row of the same name. isSpriteProp lives in PropertySchema now and is derived from FULL_SCHEMAS.sprite instead of being a second list that drifts. isValidPropertyName rejects those ten names, and the compiler reports it at the node as its own problem: "width" is a sprite property, told apart from "not a word" and from a duplicate. #64: Divide emitted (a / b) and handed NaN to the driver. It emits divide(a, b) now, with the guard in the prologue next to straight(), scalar and per channel: zero divided by zero is zero. That was already the house rule in the behavior and particle graphs - only the shader had never been told. Clamp comes along as a shader node for the same reason: the behavior catalog has had it all along. |
||
|
|
1e9d21cd5d |
A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays. |
||
|
|
5671a38044 |
A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter. |
||
|
|
8143130ed2 |
A shared value belongs to no behavior, so it becomes an asset of its own (#91)
game-maker / suite (push) Failing after 8m59s
This reverses the design of the previous commit. `PropertyDef.scope` is gone without replacement: a behavior variable is an object property again, without exception, and sharing is something you point at rather than something you declare twice. A **Variables asset** is declarations and nothing else -- no graph, no position, never placed, no tab of its own (`TAB_OF_ASSET` is already `Partial<>`, so that costs no line). A `Variables` node names one through its new `variablesAsset` setting, empty meaning the behavior's own variables as before, and then reads and writes one value per key for the whole run. Its property list follows the chosen source, which was the actual request: a value another graph declared is now offerable, where before every participant had to declare the name itself. That deletes more than it adds (-72 lines net): `globalConflicts` and its run-stopping kind clash, `SetVariableScopeCommand`, the scope picker, both object-inspector filters and the "shared by the whole run" note on the placed object. A typo is no longer a silent second variable -- naming an asset is picking from a list. And two Variables assets may carry the same key without meeting, because the asset is the key, not the name. The one shape it copies is the one that already existed: `Set state` picks its machine the same way. Lifetime is unchanged. The bag is built at `BehaviorSystem.start()` from the declarations and never written back, so playing in the editor cannot change the project -- which is the whole reason a counter must not be an object property (`World.syncToModel` would `commit()` it, and the coin counter would stand at 7 for good). Full symmetry across that gap, without mirroring anything. Two named wires: `Inspector.liveVariables` shows the values as they stand while a run is on and falls back to the declarations when it ends, and `adoptDeclarations()` takes a key into the run *only where the document changed it since the last sync*. Without that condition any unrelated project edit would snap a running counter back to its starting value; a counter-test bends exactly that and falls over. Nothing was ever cached, incidentally: `GraphInterpreter` only pulse-caches nodes without a `read`, and the variable node has one. The wire was missing. The state machine `Scope` row is now an ordinary `enum` row through `createFieldRow`. It was hand-built from an `HStack` with a ghost dropdown and began ~34px left of every value above it; the explanations beside `Local` and `Global` are gone from the menu, the sentence below the row keeps saying it. Machine scope is otherwise untouched -- `StateMachineAsset.scope`, `SetMachineScopeCommand`, `stateBag()` and its persistence all stand, so #59 stays closed as built and `Scope` as a type now carries machines alone. Six counter-proofs, each biting at its own guard. One of them did not bite at first: the suggestion list was computed twice, in `nodeSection` and `portSection`, so only the browser test covered the request itself. It is one named function now, `variableChoices`, with a unit test of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d1fcfe8b81 |
A shared value belongs to the run, so a declaration carries its scope (#91)
A variable or a state machine declared `global` keeps one value for the whole run instead of one per behavior instance. The scope sits on the declaration -- `PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence meaning `local` -- and `local` is never written, so every project made before this saves back unchanged. **The name is the key.** Two behaviors declaring a global `score` mean one variable; a behavior declaring `score` locally keeps its own. Kinds that disagree about one global name stop the run before it starts, with the two behaviors named; starting values that disagree are a warning on the behavior that declares it, and the first declaration wins (`globalConflicts`). The runtime needs no redirection layer. A global variable falls out of `schemaForObject`, so it never reaches `entity.props` -- and then the whole rule is "whoever declares the name takes their own value" (`graph/sharedState.ts`). The `Variables` node is untouched and stays transparent: switching a variable to global breaks no existing graph, because `GraphInterpreter.readAll` builds a fresh object per read anyway and only asks `port.key in all`. That same cut answers the inspector. A shared value has no owner, so the placed object shows no row for it and can hold no override -- `SetVariableScopeCommand` takes the deviations that were there and undo gives them back. Both places say it in words rather than leaving a glyph to be guessed: the declaration explains the mark below the list, and the placed object names the variable it no longer holds instead of claiming the behavior declares none -- which is what it said before, untruthfully. Screenshots of the e2e run turned that up; no check had been looking at it. `SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from the *declarations* rather than from any instance (a global variable exists before its behavior is placed), and is thrown away with the run: nothing is written back, not even in the editor run. `GraphInterpreter.call` passes it on through `...ctx` while nested calls keep getting fresh local variables, which is why `program.defaults` is stripped of global keys. Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or the inspector does not rebuild after the switch -- and a state machine has no rows at all, so its scope is appended to the signature by hand. And the object inspector does not go through `schemaForObject` for behaviors, it reads `asset.variables` directly; filtering in one place was not enough, and no unit test covered that path. The e2e caught it (`global: true` where false belonged), and the rule now lives in one named function, `objectRows`. Scopes `object` and `scene` are deliberately not built. The object case already has a route (`Object by name` -> `Properties`), and a run knows exactly one scene, so `scene` would be indistinguishable from `global` until #39 exists. Closes #59: a machine's scope is a word on the machine asset, not a setting per `Set state` node, so the same machine cannot live in two places at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d5e67cd80c |
A shader costs a source nothing, because a field was already one draw call (#88)
game-maker / suite (push) Successful in 10m9s
A particle source can wear a shader asset the way a sprite does, and it is the
cheapest place in the editor to wear one: a shaded sprite gives up its batch and
costs a draw call of its own, while `ParticleContainerPipe` breaks the batch for
every field regardless. Five hundred shaded particles are the one call five
hundred plain ones were.
The fragment the compiler emits needed no change, only a second vertex stage:
Pixi's particle vertex is GLSL ES 1.00 and ours is ES 3.00, so it is translated
rather than invented. What it sees is a particle's picture, not its life — the
buffer carries five fixed attributes and takes no sixth, so age, index and seed
stay on the graph, which is what the graph is for.
Two things the issue did not ask about:
`Shader.resources` is an accessor over the names its constructor declared, so a
slot that is not declared becomes a loose JS property and is never bound. The
adaptor writes `uTranslationMatrix` into whichever shader the container names —
without an empty block for it the vertex gets no matrix and the particles land
elsewhere.
`schemaOf` in serialize.ts hung on `schema !== schemaFor('sprite')`, an identity
check a source never satisfies, so its shader values would have been dropped on
save.
A source is the one asset drawing rows from two graphs, so a name can arrive
twice. Its own row wins and the uniform keeps its default; nothing is rejected
on the shader's side, because the collision exists only at this one wearer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|