021a6dcb5649992f4248bdad317258ca17240ca9
189
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
d257298bc7 |
Keep a dragged rigidbody on the mouse while the game plays
Dragging a sprite with a dynamic rigidbody in the scene view during play barely moved it. Only a quick drag and release got through. A dragged object should follow the mouse as if it had no rigidbody and start from rest when it is let go. - The scene view reports the dragged objects as held through EditorState. The game tick no longer writes its own position over a drag position that has not been read in yet. This affected every object in play, not only rigidbodies. - A held dynamic body turns kinematic and is set straight to the drag target. Gravity and velocity no longer pull it away between two mouse moves, and the "moved directly" warning stays quiet. - A body counts as held when the object itself, an object above it or its rigidbody is dragged. - On release the body turns dynamic again with linear and angular velocity set to 0. Not changed: a press on an unselected crate still lands on its same-sized collider (rule from #203). Dragging while play is paused and in the editor run is untouched. Tests: - ./check.sh: 155 files, 2273 tests, CHECK EXIT 0 (before 2271). - e2e 41 alone: 21 checks. e2e 1-16,39-43: 641 checks, all passed. - 2 unit tests: a held body stays where the drag puts it and starts from rest, also for a grip shorter than one physics step. The tick does not overwrite a drag position that is not read in yet. - 1 e2e check in 41b: the crate is lifted 150 units with the real mouse, holds exactly at -3490 for a second, then falls from rest (1.3 px, then 5.1 px) with an empty log. - Counter-checks: without holding in physics the crate falls back to the ground and the old warning shows. With the tick writing back the unit test fails. Without zeroing, a short grip keeps 490 px/s and the test fails. - Check total 1356 + 1 = 1357, calculated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
abf3b1f618 |
Pick an existing collider or rigidbody from the object menu (#222)
"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.
- "Add collider…" and "Add rigidbody…" open a second menu at the same
spot: a search field, "Create new …", a separator, then every asset
of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
the collider commands. An object without a resolvable asset is
greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
collider can now sit on several sprites. A value equal to the asset
value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
90 degrees and gives a circle the radius that fits (min/2). It is
greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
"Add Component" in Unity. That costs one click more than before.
Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
centring each fail at their own check. Breaking the override rule
fails 2 unit tests.
- CI confirmed 1345 checks 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
|
||
|
|
ceaa44e5b8 |
Add roles and a user management tab for admins (#180, #179)
Until now every account could do the same, and managing accounts meant running make-user.php on the server. Accounts now carry a role, and admins get a tab in the editor to create, rename, re-role, reset and delete accounts. Both issues share the same server files, so they land in one commit. #180 Roles: - Every account is admin or user. A missing or unknown role counts as user, on the server and in the client. - auth/session and auth/login return {user, role}. - Auth::requireAdmin() answers everyone else with 403. - make-user.php takes the role as a third argument. Without it an existing account keeps its role, so a password reset from the CLI never demotes an admin. - Admins see and save other users' projects like their own. Saving keeps the original owner. #179 User management: - The account menu gets "User management" (admins only) and "Plugins" (everyone). Both open the same closable tab, accounts on top and plugins below. The fixed Plugins tab is gone. - Per account: Rename, Role, Password (no old password needed) and Delete with a confirmation. "New user..." creates one. - Each account lists its projects with an Open button. - Delete is refused while the account still owns projects. - Renaming moves the projects along and ends every session of the account. transfer takes the same lock as save. - On their own row an admin can only set the password. The last admin cannot be demoted or deleted. The server enforces both. - The account rules live in AccountRules.php and accountRules.ts as pure functions, so they are testable without a server. Findings: - At 1280 px the Delete button reached past the column edge and was not clickable. The name column is flexible now, and module 49 checks the layout. - The server lets an admin delete other users' projects, but the editor has no button for it yet. Left open in the issue. Tests: - ./check.sh: 148 files, 2214 tests, exit 0 (was 147 / 2204). - New tests/php/accounts.test.php, 51 checks, plain PHP. CI runs it as its own step in the gate job, the host has no PHP. - New e2e module 49 with 18 checks. CI creates the e2e-admin account in serve.sh. Modules 14, 38, 46 and the scaffold follow the new tab. - e2e 1-16,37,38,45,46,49: all passed, 581 checks. - Counter-checks: owner on save, admin view, default role and self delete in PHP fail 7, 5, 5 and 1 checks. Unchecked requireAdmin, a wider column and the caller as owner each fail exactly their e2e check. - Check sum 1314 -> 1333 (+1 in 38, +18 in 49), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f9d5e8bca5 |
Move the one-way rule and collider geometry out of physics (#215)
PhysicsSystem had 940 lines. The step logic shares its state with the engine handles of parts and bodies, so a separate class would need almost the whole system as its interface. That part stays. Two pieces without own state move out instead. Behaviour is unchanged. - passesOneWay in oneWay.ts is the one-way rule as a pure function. It can now be tested without Rapier. - bodyGeometry.ts holds the collider shapes and the pose maths. - PhysicsSystem is down to 820 lines. Tests: - ./check.sh: 146 files, 2199 tests, CHECK EXIT 0 - 5 new unit tests for the one-way rule without an engine. - New tests/core/body-geometry.test.ts: a capsule turned by 90 degrees went unnoticed before. Both tests fail against that bend. - Counter-checks on landing hold and teleport detection fail in 3 to 6 tests each. - e2e 1-28,31,36,39-43 on the final state: ALL PASSED, 1060 checks - Each of the four #215 commits was checked green on its own in a scratch worktree: 2190, 2192, 2192, 2199 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
487b3aa5b4 |
Painting over identical tiles no longer adds an empty undo step (#212)
A rectangle drawn over cells that already hold exactly that tile
pushed a command that changed nothing. Undo then seemed to do nothing
and the project was marked as modified. PaintTilesCommand.changes()
was meant to prevent that, but nothing called it since #128.
- New paintTiles() in tilemapCommands.ts returns the command for a
finished stroke or fill, or null when no cell changes. It is the
first caller of PaintTilesCommand.changes().
- All three paint paths in the Scene View go through it: brush and
eraser, rectangle (both in endStroke) and flood fill (fillFrom).
- Flood fill was not affected, because floodFill already returns []
for the same tile. The guard is there anyway, as the issue asked.
- A rectangle that changes even one cell stays a normal undo step.
Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (before 2187).
- 3 new unit tests in tests/core/tilemap.test.ts: same rectangle
twice gives no command, a wider rectangle still does, a fill that
finds nothing gives no command.
- 1 new e2e check in 40d: the same rectangle again, then one undo
must take 9 cells to 0. A redo restores them for later blocks.
- GM_STEPS=39,40 on the final build: ALL PASSED, 81 checks.
- Counter-probe with changes(...) || true: exactly the two unit
tests fail, and in the browser exactly the new check fails with
{"afterUndo":9}. Original restored via cmp (IDENTICAL), rebuilt.
- Editor suite check total: CI measured 1312 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
|
||
|
|
d7c0b7cfdb |
Variables and state machines can reset with every scene (#164)
A shared variable or machine used to live either per object or for the
whole run. Levels need a third lifetime: shared like global, but built
fresh from the declarations whenever a scene is loaded.
- New scope `scene` for Variables assets and state machines. A missing
field keeps the old meaning: `global` for assets, `local` for
machines, so existing projects do not change.
- Returning to a scene that was visited before is fresh as well. There
is no memory per scene.
- A `scene` machine announces its start state again after a scene swap,
so `On state entered` in the new scene hears it. A global machine
stays quiet because it never left its state.
- The rule lives in two pure functions in `sharedState.ts`
(`carriedForward`, `restartedAssets`). BehaviorSystem only calls them.
- The inspector shows a Scope dropdown on Variables assets and a third
step on the machine dropdown, with a note on what `scene` means.
Decisions:
- `scene` and `persistent` exclude each other (variant A). The choice
dialog was cancelled, so this is an assumption to revisit. A
scene-scoped asset hides the Persistent switch, and switching to
`scene` drops the flag (undo restores it). The guard sits in
`isPersistent` itself, so no path can save values the scope throws
away anyway.
Findings:
- `clickText('Scene')` hits the Scene tab, not the menu entry. One e2e
check was red because of that, and its partner was falsely green
because it compared `undefined` with `undefined`. Both use `clickLast`
now.
Tests:
- ./check.sh: 144 files, 2142 tests, CHECK EXIT 0 (before 143 / 2133)
- 9 new unit tests in tests/core/scene-scope.test.ts
- e2e 32 fresh: ALL PASSED, 17 checks
- e2e 1-3,17,18,26,32,35 fresh: ALL PASSED, 213 checks
- 3 new e2e checks (35: scene-scoped asset resets on swap, 32h: dropdown,
note, hidden Persistent switch)
- Counter-checks bite: ignoring `restarted` fails 2 unit tests and the
new check in 35, the old announcement rule fails 1, dropping the scope
guard in `isPersistent` fails 1. Originals restored, cmp IDENTICAL.
- Check sum 1307 (1304 + 3) is computed. Last CI measurement: 1293.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6e3e8d23c5 |
Combining vector shapes keeps the curves it did not cut (#208)
Until now every boolean operation on vector art threw the curves away. The shapes were flattened into line segments, the operation ran on those segments, and the result came back as a polygon. A circle that nothing even touched came out of an Exclude with 40 corners instead of its four points and handles. Now the result carries the original curves again, and only the edges the operation really drew stay straight. - While flattening, every sample point remembers which curve it came from and where on that curve it sits. The operation runs on the same segments as before, so nothing about the boolean result changes. - When a run of result points walks along one of those curves, that piece is cut out of the original curve again with de Casteljau. This is an exact cut, not a fit through the points. - An edge whose two ends sit on the same curve but whose middle does not follow it stays straight. Without that rule a cut-off cap would bulge back into the shape. - A closed contour keeps at least three points, so a lens shape does not collapse into two. - The rule lives in its own module, src/core/model/vectorCurves.ts. vectorBoolean.ts only hands it the table of sampled points. Findings: - The two intersection points themselves sit up to 0.09 canvas pixels off the original curve. Clipper cuts the flattened segments, not the curves, so this was the case before as well and stays inside the 0.2 px sampling tolerance. - Refining those points with Newton steps on the curve was measured and gave nothing back (0.02952 px instead of 0.02978 px), because the error is in the intersection itself and not in the estimate. The code was removed again. - Theme.ts rowHeight 22 -> 24 is in this commit. The change is not mine, it was already in the working tree; gate and module 47 are green with it. Tests: - ./check.sh: 143 files, 2133 tests, exit 0 (was 2127) - e2e module 47 on a fresh seed: all passed, 59 checks (was 57) - 6 new unit tests: untouched circle, untouched side plus a straight cut edge, a cut in the middle of a curve, a chord crossing one curve twice, two overlapping circles, the three point minimum - 2 new e2e checks in block 47p: point count and handles from the model, plus a pixel pair on the round edge - Four counter-proofs, all bite: no reconstruction -> 5 unit tests and the e2e check fail with 35 points and nothing curved; no chord test -> the cap bulges back to 28.28; no three point rule -> the lens test fails; handles halved -> the pixel check reads background Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d7a2521ca7 |
A port can take its type from what is plugged into it (#207)
Until now every port of "First not empty" was grey and typeless, so the
editor could not tell you what actually travels through the node. The
output is now typed as soon as both inputs agree, and every any-input is
painted in the colour of its own edge. Nothing changes at runtime.
- A NodeDef can declare which of its ports carry the same value
(`sharedType: { inputs, outputs }`). The rule itself lives as a pure
function `agreedType` in `src/core/graph/portTypes.ts`.
- Only the output inherits. It becomes the shared type when every input
of the group is connected and they all carry the same type, otherwise
it stays `any`.
- The inputs stay `any` on purpose. `canConnect` checks against the port
type and a busy input is replaced by the new edge, so a typed `a`
could never be re-plugged with a text without disconnecting first.
- Chains do not propagate. When looking up what feeds an input, a node
of the same kind reports the `any` it is registered with, so two of
them in a row stay untyped and a data cycle cannot ask itself in a
circle.
- An edge that the new type makes invalid stays where it is and turns
red. `EdgeLayer` already paints a mismatching edge with `danger`,
nothing is disconnected on its own.
- New `PortDef.paintType`: colour only, never truth. It is set in
`drawnDef` and read in `NodeView.redraw`, so `canConnect`, the
interpreter and the compiler keep seeing `any`. Every connected
any-input is painted, not just members of a `sharedType` group; ghost
ports keep their hollow red look.
- The pin now matches the edge that ends in it, because both take the
colour from the resolved output type of the neighbour. Mixed sources
therefore show two colours, which explains why the output stays grey.
- New hooks `__gm.graph.portTypes(id)`, `__gm.graph.edgeColor(id)` and
`__gm.themeColor(name)`. `portTypes` reports type and paint colour
separately so the type checks do not silently become colour checks.
Findings:
- `resolveDef` only appends derived ports to the registered ones, so a
fixed port cannot be retyped that way -- it would appear twice. The
shared type is therefore resolved before the ports are built.
- Answer 5 of the issue (inline field follows the type) has no case left
under variant A: an unconnected input stays `any` and keeps its text
field.
- `Item at`, `Join` and `Equals` do not profit: `Item at` has no
any-input, the other two have fixed outputs. The next candidates are
the shader arithmetic nodes, which were left alone.
Tests:
- ./check.sh -> 143 files, 2127 tests, exit 0 (was 142 / 2113)
- GM_STEPS=1-6 fresh -> ALL PASSED, 245 checks (was 242), 9 of them new
- 14 new unit tests in tests/core/shared-port-type.test.ts
- 9 new e2e checks in block 6z, one of them a pixel check straight on
the pin via pixelAt + themeColor
- Counter-probes all bite: filtering unconnected inputs out of
`agreedType` fails 3 unit tests, ignoring `inherit` fails 1, killing
the derivation in the bundle fails exactly the 2 expected e2e checks,
and never setting `paintType` fails 4 unit tests plus the 3 pin
checks (#bdbdbd instead of #e6a8ff / #6fcf97)
- Screenshot read: `a` pink, `b` green, output `value` grey, edges in
the same colours
- Check total 1302 (1299 + 3), 1293 was measured by CI at
|
||
|
|
1a3babf9c5 |
First not empty gets a setting for what counts as empty (#206)
The node treated 0, false and an empty text alike, which is right for a missing behavior parameter but wrong the moment a real 0 travels through the graph. A setting now says how much counts as empty, so the same node works for both cases. - The new parameter "Empty means" has three steps: "Any empty value" (the default, the behaviour so far), "Nothing there (0 and false count)" and "Only a missing value". - The default keeps every existing graph as it is, including the common setup where an unset animation parameter arrives as an empty text. - The rule lives in isEmptyValue(value, mode) in values.ts; the node only passes the step through. An unknown setting behaves like the default, the same way Compare does. - The node description is shorter because the old one was cut off in the inspector. Findings: - In the "Nothing there" step a struct is empty only when its fields are empty under the same step, so a position (0, 0) stays a real value. Tests: - ./check.sh: 142 files, 2113 tests, exit 0 (before 2104). - 9 new cases in the node catalog table: the edge cases per step (0, false, empty text, empty list, position (0, 0), unconnected a) plus an unknown setting. - Two counterproofs hold: letting "Nothing there" act like "Any" drops the 3 cases with 0, false and the position; removing the "Only a missing value" step drops the 2 cases with the empty text and list. - Seen in the browser: the node shows "Empty means: Any empty value" and the inspector dropdown lists all three steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d98c2898b1 |
A new node takes the first of two values that is not empty (#205)
A graph that reads an optional value needed a chain of Equals and
Branch just to fall back on a default. The new node does that in one
step: it passes a through unless a is empty, in which case b comes
out. Both ports are any, so it works for numbers, text, lists,
structs and objects alike.
- `First not empty` sits in the data category with two any inputs
`a` and `b` and one any output `value`. Are both empty, b comes
out, so the second input doubles as the default.
- `isEmptyValue` in values.ts holds the rule as a pure function:
nothing connected, 0, an empty text, false, an empty list, and a
struct whose fields are all empty.
- The struct case is what covers a position: behavior graphs have no
vector type of their own, so (0, 0) arrives as a struct. `{x: 0,
y: 2}` therefore counts as set, `{x: 0, y: 0}` as empty.
Findings:
- A real 0 or false on a is passed over. That follows from counting
0 as empty and is written down in the issue.
- The output stays any. Ports that inherit their type from an input
do not exist in the graph; `Item at` has the same shape.
Tests:
- ./check.sh: 142 files, 2104 tests, exit 0 (was 2094).
- 10 new catalog cases: a set, a unconnected, 0, "", false, an empty
list, a position (0, 0), a struct with one field set, an object,
and both empty.
- Counter-check: with structs never empty and 0 a real value exactly
those two cases fail, the rest stays green. Originals restored and
compared with cmp, then rebuilt.
- No e2e check: single data nodes have no e2e coverage here, the
palette fills itself from BUILT_IN_NODES.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
41772e53f7 |
Variables assets can keep their values from one run to the next (#166)
A game needs a high score or an unlocked level to survive the end of
a run. As agreed in the issue there are no save or load nodes. A
Variables asset gets a switch instead, and the existing Variables
nodes read and write the values as before.
- A "Persistent" checkbox in the Variables inspector. Toggling it is
one undo step, and absent means not persistent.
- The behavior system loads the saved values at start and writes them
after every frame that changed one, so a closed tab loses nothing.
- Values live in localStorage per project, not in the project file.
Another browser or person starts fresh.
- Play and "Run in editor" save apart, so trying a behavior in the
editor leaves the Play save alone.
- Saved values are read through the current declaration. A removed
variable drops out, a new one starts from its default, and a changed
kind is converted.
- "Clear saved values" in the inspector clears this asset in both
areas. The main menu gets "Clear saved data" for the whole project.
Clearing takes effect from the next run on.
Decisions:
- The switch covers the whole asset, not single variables. Mixing
both means two assets.
- A changed default in the inspector shows only after the save is
cleared. That is what persistent means.
Findings:
- The first e2e run died at "debug hook present" right after a Vulkan
error from Chrome. The second run passed, so it was a GPU flake at
start-up.
Tests:
- ./check.sh: 140 files, 2070 tests, exit 0 (before 138 / 2055).
- 15 new unit tests in tests/core/saved-values.test.ts and
tests/editor/saves.test.ts.
- GM_STEPS=32,46 fresh: ALL PASSED, 24 checks. Six new checks in
module 32 go through the UI. Module 46 knows the new menu entry.
- Counter-checks both fail as expected. Loading switched off: 2 unit
tests and 2 e2e checks fail. Editor run writing into the Play area:
1 e2e check fails.
- Check total 1278 (1272 measured by CI at
|
||
|
|
bbbf34109b |
Lists duplicate, rename and edit values right in their rows
Name, opacity and Z each sat in a separate field below or beside
the list. You had to select a row first and then look elsewhere to
change it. Duplicating was a separate button or not possible at all.
Now the row itself carries everything.
- SortableList gets three optional callbacks. onDuplicate shows a
copy button between the eye and the delete button. onRename opens
a text field on double click, Enter or a click outside commits,
Esc cancels, an empty name is ignored. value puts a NumberField
into the row.
- Dragging on the number changes the value and never moves the row.
A whole drag is one undo step, the canvas follows live.
- Copy and delete only show on the hovered and the selected row.
Their space stays reserved, so nothing jumps on hover.
- Pixel layers: copy right above with the same pixels on all
frames, rename, opacity in percent. The Name and Opacity fields
below the list are gone.
- Pixel frames: copy right after, replaces the Duplicate button.
- Tilemap layers: copy with cells, Z and visibility, rename, Z in
the row. The Name and Z fields in the panel are gone.
- Vector shapes: copy at the same place and select it, rename.
- New in core: DuplicateVectorShapeCommand,
DuplicateTilemapLayerCommand, withLayerDuplicated and copyName
("X copy", "X copy 2"). copyName lives in src/core/util/names.ts
because the model never imports from commands/.
Findings:
- Double clicking a shape that is not selected yet only selects it.
The first click moves the list up, so the second click misses the
name. Module 47 selects the row first.
Tests:
- ./check.sh: 138 files, 2055 tests, CHECK EXIT 0 (before 2052).
- GM_STEPS=47,48 (fresh, twice): ALL PASSED, 91 checks (before 86).
- GM_STEPS=39,40,47,48: module 40 green, 47 died on the rename
check, which was a test fault and is fixed.
- Counter probes: a constant copyName fails the shape and pixel
layer tests. A dead copy hit fails the copy check in 47.
- Check total 1272 in README and wiki, computed (1266 + 6).
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> |
||
|
|
1116818011 |
Vector shapes can be combined by union, subtract and more (#187)
Building a shape out of simpler ones meant drawing the result point by point. The Vector Art tab now selects several shapes at once and combines them with a boolean operation into one ordinary shape. - Ctrl/Cmd+click in the tab and Ctrl or Shift+click in the shape list add a shape to the selection. Shift stays pan in the canvas. - A COMBINE block in the inspector offers Union, Subtract, Intersect and Exclude. Subtract removes every upper shape from the lowest one. - The result replaces the lowest selected shape and keeps its name, colours, stroke and place in the order. It is one undo step. - Curves are flattened to 0.2 canvas pixels, a circle gets about 48 points. Open lines and single shapes cannot be combined, the buttons are off and a hint gives the reason. An empty result is rejected. - Delete now removes every selected shape, and the selection drops shapes that no longer exist. - The geometry comes from clipper2-ts 2.0.1, pinned. It has no dependencies and speaks even-odd directly. Only the engine module is imported, the bundle grows by 55 kB instead of 110 kB. Findings: - clipper2-js was tried and dropped. It pulls in Angular 15 with a high npm audit finding and no fix. - Pixi finds holes by containment, not by even-odd depth. An island inside a hole would be drawn as a hole again. Result rings are emitted outer ring before its islands, and a test mirrors Pixi's rule. - A depth sort turned out to be dead code in a counter-check and was removed. A pixel check in 47h stayed green without the subtract and now checks both colours. Tests: - ./check.sh: 134 files, 1986 tests, exit 0 (before 133 / 1968) - npm run build: exit 0 - GM_STEPS=47, freshly seeded: ALL PASSED, 44 checks (before 39) - 18 new unit tests, 5 new e2e checks in block 47h - Five counter-checks bite, originals restored and verified with cmp - Check sum 1222 in README and wiki, calculated (1217 + 5) 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> |
||
|
|
2d79230b55 |
A frame's collision outline can be generated from its pixels (#161)
Drawing an outline by hand for every frame of a sheet is slow, and the outline mostly follows the opaque part of the image anyway. The texture tab now traces it: one click, a corner budget, and every Frame collision collider and tile using that frame blocks with the result. - "Generate collision..." sits in the texture tab header and is off while the texture has no frames. - The dialog has Max points (default 8, 3 to 64), Solid from ... % alpha (default 50) and All frames of the texture. A live line says what Generate will write, and Generate stays off when nothing is solid enough. - traceCollision walks each island of solid pixels along the pixel edges, then drops the least important corners until at most N are left and no stair step remains. A slope becomes one straight edge. - An outline never crosses itself. Where the image would force that, the island's convex hull is taken instead. - Crumbs under 1 % of the frame area are dropped unless they are the only island. Holes are not reproduced. - Existing outlines are replaced. One frame or all frames is a single undo step. - TextureLibrary hands out the sheet's RGBA data, and a pure function cuts a frame's region from it. Findings: - The wedge in the e2e fixture gets 4 corners, not 3. Its tip is one pixel wide in the image, and that real edge stays while corners are still free. Tests: - ./check.sh: 131 files, 1941 tests, CHECK EXIT 0 (was 130 / 1931). - 10 new unit tests in tests/core/trace-collision.test.ts, including 150 random shapes that must all be valid and within the budget. - GM_STEPS=42, freshly seeded: ALL PASSED, 20 checks (was 16). - Three counter-probes bite: no hull fallback, no stair threshold, dialog ignoring the alpha threshold. - Check sum 1202 in README is computed (1198 + 4), 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> |
||
|
|
3ff7e91dbf |
Vector art points have four types and the pen draws whole curves (#195)
The pen only pulled out the outgoing handle of a new point, because the opposite handle was mirrored only if it already existed. The first segment of every dragged point was therefore half a curve. Beyond that bug, a point had no way to say how its handles relate. - A vector point has an optional mode: straight, corner, smooth or symmetric. Missing means corner. The mode is not part of the fingerprint, because it changes no pixel, only the next drag. - Dragging a handle follows the mode. Corner leaves the other handle alone, smooth turns it along and keeps its length, symmetric mirrors it exactly. - The pen pulls out both handles mirrored and marks the point symmetric. A plain click still sets a point without handles. - The tab header has a "Point: ..." menu for the last clicked or set point. That point is drawn filled. Changing its type is one undo step. Text instead of four icons, because the UI kit has no tooltips. - Switching a point without handles to a curved type first grows handles along its neighbours. Smooth and symmetric line the handles up and keep the rough direction of the curve. - A point inserted on a curve is smooth. Symmetric neighbours become smooth, because the split shortens one of their handles. - Alt-click still toggles between straight and symmetric. Found on the way: the old Alt rounding took the point at the other end of an open line as a neighbour, so its handles came out skewed. Fixed and held by a unit test. Existing points with handles now count as corner. Before, every drag mirrored. No pixel changes, only the next drag behaves differently. Tests: - ./check.sh: 129 files, 1917 tests, exit 0 (before 1903) - GM_STEPS=47, freshly seeded: ALL PASSED, 39 checks (before 32) - Check sum 1195 in README and wiki is computed (1188 + 7), CI measures it - Counter-checks: pen without 'symmetric' fails 3 checks, no smooth branch fails 1 unit test and 1 e2e check Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6371576d37 |
A vector art asset is a picture now, which is stage 2 of #129 and the first stage that shows something in the browser: the asset sits in the picture picker, a sprite carries it in its Texture row and what the sprite shows has been rasterized from the shapes, the generator of #185 being registered for the type vectorart - installVectorArtPictures(app.renderer) hooks the source (core) and the generator (viewport) in once per page from App.ts, by a call and not by a module side effect, so a test may install it or leave it out; the rule lives in core and the painting in viewport - the new core/model/vectorPath.ts turns a shape into plain drawing commands (move/line/curve/close), closed rings being filled while all rings are stroked, a segment staying a line as long as neither of its ends carries a handle - a single handle is enough for a curve, the missing control point sitting on its own end, so a corner may stand next to a round side - and the way back to the first point is only drawn when it is curved, close doing it otherwise; holes go through one door only, Pixi recognising them from a GraphicsPath with checkForHoles, which tests containment rather than even-odd depth, which is enough for a ring inside a ring while without the flag the inner ring simply paints over the outer one (measured, see the counter-checks); the oversampling is the 2x from the plan - RenderTexture.create({resolution: 2}) puts 512 px on the GPU for a 256 canvas while texture.width stays the canvas size, which is the measure the sprite hangs on, and the render texture is the clip itself, so what juts out over the canvas is not in it and needs no mask; drawn pictures get the same sampler state as uploaded ones, setPictureFactory wrapping the factory for it, mirroring keep(); two measuring traps cost me runs - __gm.objectRect(id) gives the centre of the object and not its corner, so the first pass read four pixels next to the sprite and reported background while the rasterizer was correct, and the grid lines of the scene cross exactly in the centre point, so the hole check read #363940 and failed for the right reason, measuring beside it now; renderer.render() runs inside the picture request, which is safe here because every caller asks during its own widget redraw and the frame is drawn on the ticker afterwards, a nested render inside a running render pass would not be - that warning stands in the module head and in the notes; new module 47 with 8 own checks in its own lane (the picture on the GPU at double resolution measuring the canvas, the sprite showing the fill, the inner ring being a hole with the scene shining through, the later shape on top, the stroke running along the ring, a changed drawing getting a new key with the old one freed, and the sprite showing the new colour), the shapes being put into the model by mutate because the tab that draws them is stage 3, and 21 new unit tests in tests/core/vector-path.test.ts; three counter-checks hold: oversampling back to 1 fails the GPU check, checkForHoles off fails the hole check, and not drawing the curved way back fails a unit test; the full suite ran locally instead of only in CI because the change sits in TextureLibrary and touches every picture: ALL PASSED - 1165 checks, measured and carried into README and the wiki (#129)
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> |
||
|
|
ffdf0a8ddb |
The new node "Raycast all" answers with every collider along the ray instead of only the nearest one: it carries the same inputs and settings as Raycast (Ignore owner, Hit triggers, Layers from #158) plus "max hits" where 0 means all, and it hands back parallel lists sorted by distance - objects, colliders, points x/y, normals x/y, distances - next to count, so index i of every list speaks about the same hit; a collider counts once no matter how many convex parts its shape is built from, while a tilemap counts per cell it is hit in, and the filter sits in Solids.raycastAll like the one of #158, the one path the editor run and Play both take, so all 12 new unit tests run through describe.each against both systems; the node needed the value system to learn something it could not do - a graph list carried numbers, text, booleans and bundles but no object handles, asList turned an object into its name, so objects/colliders would have been lists of names, which loses the object when two carry the same name, and collider children usually do; Value now knows a travelling list that may hold handles and "Item at" gives such an item out as an object, ready for Destroy, Apply force or Name of, while the single door into a stored file - writeTyped/asTypeOf - runs the list through the new storedList() so every object becomes its name again and nothing living reaches a project file, which is its own unit test over JSON.stringify; three list nodes follow the handles: "Index of" compares objects by identity because a name is not the object of that name, "Set item" and "Fill" write an object as an object, and "Join text" prints the names instead of the innards; this decision was mine to make because the question ran into the MCP timeout - the alternative was a list of names plus "Object by name", which is the smaller change but lossy; 4 new e2e checks in the new block 43e (shield and floor in one go reading 2 and "145, 280", the second object answering to its name, which only a real object in a list can do, "max hits" 1 stopping at the nearest, and the node showing the row) and 18 new unit tests, 12 in raycast.test.ts, 4 on the value system and 2 in the node catalog; two counter-checks hold: not counting tilemap cells one by one drops 2 unit tests, and answering with the nearest hit only drops module 43 with 2 FAILs; one finding on the way - the node read "maxHits" while every other input carries a spoken label, because int() took the key as its label, so it has a third parameter now, and the e2e check caught that, no unit test did (#157)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0fdde6cb1e |
The Raycast node filters by collision layers: the node gained a "Layers" setting, a tickbox per collision layer of the project - the same field kind the collider uses for "Collides with" - and the ray only hits what sits on a ticked layer, tilemaps counting as colliders; the default is every layer, and a graph without the param answers exactly as before, which is its own unit test; the check is one-sided on purpose, unlike the symmetric rule of #152: a ray is not a collider, so it never reads the collidesWith of the thing it hits - a collider that meets nothing is still seen by a ray - because who a ray may look at is the ray's business, and a unit test holds that case; the filter sits in Solids.raycast in front of the intersection test, which is the one path the editor run and Play both take, so every unit test runs through describe.each against both systems; NodeView did not know the field kinds layer/layers, so the node would have read "Layers: 65533" - it now shows the name of the layer, or "All layers"; 5 new e2e checks in the new block 43d (a shield on its own layer between the caster and the floor stops the ray, the "Layers" row on the node, the tickbox menu with the project's layers, the ray reaching 280 instead of 145 units once the shield's layer is unticked, and the mask read as a name) and 6 new unit tests, three against each system; two counter-checks hold: disarming the filter line drops 6 unit tests and the unticking check in module 43, and switching the formatting off makes the node check fail with the raw bits; two findings from measuring came out of it and stand in the wiki and the notes - __gm.rays() only measures while the Scene tab is in front, so a node selection blinds it and the block measures through a Log node on distance instead, and a running behavior instance keeps the graph it started with, so the editor run casts with the old mask until the instance is built anew (#158)
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> |
||
|
|
5ba796a2b6 |
Plugins belong to the project, and they load before it: the project file gained the field plugins: ["demo-plugin.js"] (default empty, no formatVersion bump), the Plugins tab's switch writes into it as an ordinary undo step (SetPluginActiveCommand) instead of into a server-wide plugins.json, and the start order is turned around - startPlugins only fetches the list of files, then editor.boot() runs and ProjectStore.load(id, prepare) hands the raw JSON through before it is deserialized, where pluginsOf(raw) reads the list and PluginManager.syncTo() activates exactly those; that is the point of the issue, because what a plugin registers has to stand there before normalizeGraph would throw it away as unknown (#126, #127), a project switch first stops what the new project does not name and then starts what it does, a file the server does not have is passed over silently, and after signing out the manager is dead so a late event cannot start anything in a torn-down session; on the server plugins.json, PluginStore::setActive, isActive, the route PUT ?r=plugins/{file} and the 403 for inactive modules are gone and GET ?r=plugins only answers {file,bytes,updatedAt}, which makes every plugin file fetchable - the consequence accepted in #117, written down in the wiki for #115 and #116; publishPlugins(root) only copies files now, the seeder switches the demo plugin on in the fixture project, and the Plugins tab carries a puzzle piece as its icon while only a plugin's own tab still goes without one (the knob and the socket are cubic curves, not arcs, which Pixi's parser draws as a spike after an L); 3 new e2e checks in module 38 and 5 new unit tests cover the project as the owner of the switch, its undo, the order, pluginsOf against crude input and an unknown file name (#119)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c830c7a3aa |
Changing the password: the Logout button at the far right of the toolbar became an Account menu that names the signed-in account in an entry of its own that cannot be picked, with Change password... above Logout, and the new dialog (src/editor/auth/PasswordDialog.ts) asks for the current password, the new one and its repetition, all three as bullets, and stays standing when the server refuses so that it can say why, unlike the other form dialogs; the rules for empty, too short, new equal to old and a differing repetition sit as the pure function passwordProblem() next to loginProblem(), the API gained POST ?r=auth/password which answers 204, insists on the current password although a session is there, enforces a minimum length of 8 in UserStore::MIN_PASSWORD_LENGTH and again in the client so the mask can answer without asking the server, and the new SessionStore::destroyAllFor($user, $keep) ends every other session of the account while the tab that changed the password keeps its own; the placeholders admin/admin and user/user stay as they are, which the length check does not touch because it only runs when a password is set through the API; e2e 45 grew by 8 checks for the menu, the dialog, both refusals and signing in with the old and the new password, and it sets the e2e account back at the end - a run dying in between leaves it on <password>-tmp, which a make-user.php call repairs (#182)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1ef497fa17 |
e2e 45 no longer hangs on the project name: the two checks that opened the editor compared the project's name against "E2E fixture", which is what the seeder writes locally, while CI creates the shell with the key from tools/ci/project-shells.mjs and so calls the same project "fixture" - that is why the run for 8e4bb6e was red with two FAILs although signing in, the editor and the project all worked; both checks now read the project id and the seeded scene name "Level 1", which say the same thing in both places, and since the firewall in front of nockal.com stopped turning every non-2xx into its own 418 page (measured today: the API's 401 comes through with its body) the wrong-password check asks for the wording again instead of just for any message, with the unit test for a page made up in between kept but no longer named after that firewall (#177)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8e4bb6e1f9 |
User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
25ec7879a0 |
Several transitions between the same pair of states: make transition onto a state that is already connected in that direction adds another transition, which gets its own name right away (Transition 2, Transition 3, skipping names that already leave the state), so two unnamed transitions between one pair never exist; renaming is refused when another transition between the same pair in the same direction already carries the name, the empty name included, while equal names to other targets are only warned about as before; the behavior graph keeps Already connected; self transitions may repeat too, each further loop drawn as a ring around the previous one with the preview showing the next ring, and transition labels that would cover another one slide along their own edge until they are free (pure function freeLabelSpot); test hook transitionLabelAt also returns width and height, e2e 26e6 (#162)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
010b6a7f6c |
A state can transition to itself: make transition and a second click on the same state connects a self transition, taking it fires On state exited and then On state entered every time, for local and global machines, while Set state into the current state stays silent as before; only states may connect to themselves, every other node is still refused, one self transition per state; the loop is drawn over the middle of the top edge with its arrow pointing down and its name at the apex, the preview already shows it while armed, and the inspector lists it once under Transitions out; new test hook transitionLabelAt, e2e 26e5 (#163)
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> |
||
|
|
d4ea04d4d6 |
The scene view outlines a tilemap's collision only along its outer edges: where two pieces meet, their shared stretch runs once in each direction and is not drawn, so an autotile lake is one outline without cross strokes at the quarters, merged rectangles of plain tiles and one-way tiles follow the same rule, the bridges from splitting concave outlines disappear and a piece doubled on two layers is outlined once; fill and collision stay as they were, and the result is cached per tilemap and rebuilt only when its collision data changes (#173)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |