ca5b2db449ebf575ea65edf4ef477b2a7bfdfb11
71
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca5b2db449 |
Keep where each divider was dragged to across a reload
A panel dragged wider was back at its first size after every reload. The sizes are now kept in the browser, next to the folded groups. - editor/panelSizes.ts holds one entry in localStorage, game-maker.panel-sizes: a name per divider and its width, height or share. - A panel reads its size once when it is built, held to the bounds its divider keeps today. Every drag writes the new size. - Kept are the columns left and right of the centre, the console, the split of the left column, the tilemap panel, the preview of the Shader, Particles and Pixel art tabs, each under its own name, and the split of the user management. - The sizes belong to the browser, not to the project. They hold for every project opened there. - A browser that refuses the storage, or holds rubbish under the entry, starts with the sizes the editor comes with. Tests: - Gate on melektaus-atx: 219 files, 3400 tests, CHECK EXIT 0. - Seven new unit tests in tests/editor/panel-sizes.test.ts. All five counter-probes bite. - Smokes on melektaus-atx: 24, 19 and 21, all pass. - One new e2e check in module 48: the left column dragged 40 px wider is as wide after a reload, and dragged back after. - Full suite on melektaus-atx: 1692 checks, all pass. It ran before the fix to the animation players of the commit before last. - Browser counter-probe: without the write to the browser exactly that check fails. - Only the left column is measured across a reload. The other dividers go the same way and have no check of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ec2fd9084f |
Say what a button without words does when the pointer rests (#209)
The top bar, the magnet, the eye and many marks of the inspector are
pictures without a caption. Whoever did not know one could only try
it. The toolkit had no hint on hover, which also forced the point
menu of #195 to be words. The answers in the issue settle the open
points: a cut tab name shows in full, shortcuts are part of the
text, the point menu stays as it is.
- Button takes a tooltip, a text or a function asked each time. It
shows on a small sign once the pointer has rested for half a
second.
- There is one sign for the page, a TooltipLayer on the overlay
above the menus. A control reports each pointer move and the
pointer leaving, the layer does the waiting.
- A press, a key, the wheel or a blur of the window takes the sign
away. initUI listens for them on the window.
- placeTooltip in ui/tooltip.ts is the rule for the place as a pure
function: under the control and flush left, flush right at the
right border, above the control at the bottom.
- toolbarTip says what the five buttons of the top bar do right now.
Undo and redo name their step from CommandStack.undoLabel and
redoLabel, as in "Undo Paint tiles (Ctrl+Z)".
- A SortableList reports its own marks with a key per mark: the eye,
the duplicate mark, the cross and the plus.
- The magnet, both fullscreen buttons, the eye of the scene tab, the
overflow of the tab strip, the plus of the assets panel, the marks
of the inspector rows and the nine anchors of the resize dialog
carry a text.
Decided here:
- Every button whose text is cut short shows the whole text, not
only a tab.
- A row action of the inspector names the one thing a click does. A
button that opens a menu says only Reset, Apply or Size from the
picture.
- A button just clicked stays quiet until the pointer has left it.
- A greyed out button shows no sign.
- The switches of the eye popup, the cross of a document tab and the
tools of pixel art have none. The tools carry words, other than
the issue assumes.
- The marks of a list say Show or Hide, Duplicate, Remove and Add,
whatever the list holds.
Found on the way:
- Signing out empties the overlay and would have destroyed a sign on
screen with it. The editor takes the tooltip away first. This has
no test of its own.
- The text hooks locate and has skip the sign, or it would answer
for the button under it.
Tests:
- Gate on melektaus-atx: 210 files, 3209 tests (3176 + 33), exit 0
- e2e 46 on melektaus-atx: 20 checks, all pass
- e2e 1-14 on melektaus-atx: 508 checks, all pass
- e2e 1-9,14,39,40,44-48: 685 pass, 2 fail, both in module 14. It
measures tabs that modules 10 to 13 open, and the chain 1-14 is
green. The cut chain was not run on the state before.
- No full run and no smokes. The sum of 1667 (1664 + 3) is reckoned,
1664 is what CI counted for
|
||
|
|
0f85476b93 |
Add points by double-click and delete the picked one (#288)
Four editors edit points, and all of them put a point down on a single press on the line and took one away on a double-click. A stray click added a point, and there was no way to see which point a key would hit. All four now work the same way: the Curve tab, the Vector art tab, a polygon collider in the scene view and a frame's collision outline in the Texture tab. - A double-click on a line, edge or segment puts a point there, in the Curve tab on free space too. The point is picked and stays in place. It can be dragged with the next press, as answered in the issue. - A click on a point picks it, and it is drawn filled. A double-click on a point takes nothing away any more. - Delete or Backspace takes the picked point away. Without a picked point the key removes the shape, the object or the outline as before. - A right-click on a point picks it and opens a menu with Delete point. In the Curve and Vector art tab the four point types stand below it. Alt-click stays. - A polygon keeps three corners and a curve keeps its ends. The entry is greyed out then. - The pick ends with a click beside the points, with another object, shape or frame, and after the point is gone. - editor/pointGestures.ts holds the rule as a pure function, and editor/pointMenu.ts builds the menu for all four. Decided here: - A single click on a line no longer adds anything. In Vector art it drags the shape, on a polygon it drops the pick, and in the Texture tab it makes that outline the active one and drops the pick. - In the Texture tab a click inside the frame still appends a corner, and that corner is picked. Delete takes the last corner first. To remove the whole outline, click one of its edges before. - The point the Pen put down last is picked, so Delete takes it and not the shape. - Delete hits the picked point also when the tab has lost the keyboard focus, after a click in the shape list or after the menu. - A vector shape that loses its last point is removed whole. Before, an empty shape stayed in the list. Findings: - Fixed: the new hint line in the Texture tab was cut off at the edge. The shorter text was not looked at in a picture again. - Not fixed: in the scene view the collider's name label covers a corner that sits near the centre. That was so before. Tests: - Gate: 200 files, 2967 tests (2944 + 23), exit 0 - e2e, full run: 1629 checks passed (1625 + 4), exit 0 - Smokes: 24 / 19 / 21, exit 0 - After the full run an unused parameter and a comment were tidied. For that final state the gate and e2e 42,47,53 (105 checks) ran again, the full run did not. - 23 unit tests in tests/editor/point-gestures.test.ts, for the rule and for each of the four editors - Four e2e checks with a real mouse, two in module 42 and one each in 47 and 53. The existing checks for adding and removing a point use the new gestures. - 20 unit counter-probes all fail tests, run on stein2 - Two browser counter-probes fail, the build went through both times. Without the Delete wiring the key removes the collider. Without the right-click hook the menu is missing, and that run stops in module 42, so 47 and 53 were not reached with it. - Three screenshots looked at, the picked point is filled. None of the Vector art tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cfd288c4b7 |
Halve the frame time of a playing scene with a tilemap (#284)
The performance panel showed Physics and Editor as the large parts of a frame in a scene of 222 entities. Nearly all of it came from a tilemap that only stands there. - Whoever writes cells in place now says so with cellsChanged. Only then does cellsVersion compare the content. The comparison ran about 60 times per frame and was the largest single item at 14 percent. - The four writers call it: the painter during and at the end of a stroke, PaintTilesCommand on execute and on undo. A new writer that forgets it stays invisible. - The game view is drawn once per frame in play. With an animated tile in view it was drawn twice, once by the run and once by animate(). animate() now gets the redraw count from the start of the tick. - TilemapField builds the geometry of a mesh only when its source changes: the cells, the tile size, the origin or the image. For that groupByTile and layerRows return the same objects for the same list. - PhysicsSystem skips a tilemap whose source and switch are unchanged. The switch is kept because a map that starts switched off would otherwise have dead colliders once it is switched on. - World.syncToModel looks entities and assets up in a map per pass instead of a linear search per object. Measured on stein2 with a copy of the reference project, ms per frame: Physics 2.64 to 0.97, Drawing 6.89 to 2.56, Editor 1.95 to 1.31. Left open: syncToModel still mirrors every entity every frame. That is its own issue. Tests: - Gate: 198 files, 2887 tests (2883 + 4), exit 0 - e2e 1-17,28,33,39-43,59: 810 checks passed, 40 has +3 - Smokes: 24 / 19 / 21, exit 0 - Three unit tests wrote cells directly and now call cellsChanged - Eleven counter-probes fail. Two did not at first: the stroke needed a check of its own, the physics test has to start with the map switched off. That last one was repeated on stein2 only. - e2e total 1613 is computed, not measured in a full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
77f4c2c1a3 |
Show entities and time per area while playing in the Game View (#275)
A game that stutters gave no hint where the frame goes. The Game tab now has a Performance switch. It shows a small overlay in the top left corner of the Game View while a game runs: fps, the number of entities and the mean milliseconds per frame for each area, averaged over half a second like the fps counter. - src/editor/frameMeter.ts holds the pure rule. FrameMeter sums the areas per frame, closes a window every 0.5 s and formats the lines. - World.timeSystem times every system the world ticks. PlaySession maps the systems to Behaviors, Physics and Animation and sets the hook again after Load scene. - Animation has its own line. Counting the AnimationSystem under Behaviors would have skewed that number. - Drawing is the build of the game picture plus app.render(). It is main thread time only. The GPU work after it is not in the number, and app.render() draws the whole canvas including the panels. - Editor is the rest of the editor tick after the systems and the game picture: mirroring into the model, panels and console. - The overlay lets clicks through to the game. It shows dashes until the first window closes. The switch is off by default and is not saved. The released game (#266) runs without a meter. Tests: - Unit frame-meter: 9 passed. - Gate on melektaus-atx: 197 files, 2853 tests, CHECK EXIT 0. - e2e 59 on melektaus-atx: ALL PASSED, 18 checks (13 before, +5 in 59e with a real mouse on the switch). Run twice. - Three mutations were caught: the editor share not subtracting the systems (2 unit tests), systems running in pause not reported (1 unit test), the overlay getting no times (59e). - The e2e total of 1601 is computed (1596 + 5), not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ded71a92c9 |
Pick the target of an object variable in the Scene View (#276)
Choosing the target of an object variable meant finding its name in a dropdown. With many objects, or several with the same sprite, the name alone does not say which one it is. The eyedropper lets you click the object where you see it instead. - Every object dropdown gets an eyedropper button next to it. This covers variables, struct fields and list entries, in the Inspector and in the Variables tab. - The button opens the Scene View or the container tab the object lives in. A strip above the view names the field and the current candidate and offers Take and Cancel. The cursor is a crosshair. - A click picks a candidate and writes nothing yet. The candidate gets its own cyan frame, the selection stays on the object with the variable. - Clicking follows the selection rule. `pickOn` in `src/viewport/pick.ts` uses `pressOn` and only counts objects the dropdown offers. A click on empty space keeps the candidate. - Enter or Take writes through the same handlers as the dropdown. That is one undo step, or none if nothing changes. - Esc, Cancel, a right click in the view or a second click on the button cancel without writing. So do a tab switch, another selection, undo, redo, play and loading a project. - After Take or Cancel the editor goes back to the tab the pick came from. - While the eyedropper runs, the view does not drag, marquee, paint, show handles or open its context menu. Panning and zooming stay. Found on the way: - Started from the Variables tab, the click missed the object. The Scene View is hidden while that tab is open, and the layout skips hidden children. The strip appeared then, the view moved down by 32 px but kept its old height. `Editor.display()` now lays the row out again after showing it. Tests (on melektaus-atx): - unit pick and object-pick: 27 passed, 13 new - gate: 196 files, 2844 tests, CHECK EXIT 0. Two more come from svg-icons, which checks every icon twice. - e2e 57: ALL PASSED, 12 checks, 5 new (57e to 57g) - Counter-checks work. Without the layout fix 57g fails. Picking objects the dropdown does not offer fails 2 tests. Writing on cancel fails 2 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cab1c0177f |
Show only the game view in fullscreen, in the editor and the link (#267)
A game is easier to judge and to play without the editor around it. The Game tab and the game link now both have a button that puts the game view itself into fullscreen, not just the browser window. - The Game tab header has a fullscreen button on the right. It hides toolbar, panels, tab headers and console and keeps the zoom from the picker. A running game keeps running. - A corner button at the top right, or Esc, leaves fullscreen and puts the view back into the tab where it was. - The game link shows the same corner button. Over a running game it hides after 2 s without mouse movement and returns on the next move. - A click on the corner button never reaches the game. - Stop ends fullscreen. Stop switches back to the Scene tab, and any switch away from the Game tab ends fullscreen, so no empty screen is left behind. - src/viewport/fullscreen.ts wraps the Fullscreen API and holds the rule for when the button shows. FullscreenCorner.ts is the button used by both pages. Two SVG icons are new. - Editor.showGameFullscreen moves the GameViewport into the root while fullscreen is on. Tests: - ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests). - e2e 60: 2 new checks for the corner button on the game link. - e2e 61 (new): 4 checks for fullscreen in the editor. - Full e2e run (local): ALL PASSED, 1580 checks. - Counter-checks fail as expected: rule without "no game running", no stopPropagation (60h), no end on tab switch (61c), no reparenting of the view (61a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4194f3e150 |
Share a link that plays the game without the editor (#266)
A project could only be played inside the editor, behind the login.
To show a game to someone else there was no way short of an account.
A release freezes the saved project under a random token and serves
it on a page that holds nothing but the game view.
- Menu -> Release... opens a dialog with the state of the release,
the link with Copy link and Open, Publish or Update release, and
Take down behind a confirmation.
- Publish saves first and refuses while anything is unsaved, like
snapshots. Updating keeps the link. Take down kills it, and a new
Publish hands out a new token.
- The link is play.html?game=<32 hex>. The page needs no session,
paints Theme.colors.background, zooms with Auto and waits for
"Click to play" so that sound and keyboard focus are allowed.
- The game always starts in the start scene. Persistent variables
are stored per link in the browser. Plugins are not loaded, and the
dialog warns about them and about a project without a scene.
- GET ?r=releases/{token} is the only route besides auth/* that works
without a login. The public answer carries the token as project.id,
so the project id stays hidden. Release files are written with 0640
and nginx does not serve them directly (measured 403).
- Starting, the frame and Load scene moved into runtime/GameRun.ts.
Play in the editor and the play page run through the same code.
- WorldViewport moved to src/viewport/ and no longer needs the
EditorState. Pointer forwarding (gamePointer.ts) and setting up
textures, fonts and sounds (projectFiles.ts) exist only once.
- Vite builds two pages. The play page loads 4 kB of its own code
plus the shared chunk (Pixi, runtime, drawing, 653 kB) and Rapier,
without the editor.
Found on the way, not fixed here: data/projects/<id>.json answers 200
without a login because the files are 0644.
Tests:
- ./check.sh: 194 files, 2808 tests, exit 0.
- tests/php/releases.test.php: 20 checks, also added to CI.
- New e2e module 60 with 10 checks, played in a browser without a
session. Module 46 counts the menu entries and was updated.
- Full e2e run: 1572 checks passed.
- Counter-checks: 3 unit, 2 PHP and 3 browser mutations all fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
94c1055895 |
Throttle what the editor shows while a game runs (#265)
With the behaviors fixed, the scene Performance test still ran at about 23 fps with 500 entities. The rest of the frame went to the editor: every spawn or destroy rebuilt the hierarchy with all its rows, redrew the scene view behind the game tab and refreshed every panel. The game itself is cheap. What the panels show of it now follows a preset in the project settings. - New section While playing in the project settings with three presets: Live, Balanced and Minimal. Every project reads as Balanced, including those saved before the setting existed. - Live keeps today's behavior. Balanced brings hierarchy and panel values up to date 4 times a second and stops redrawing the hidden scene view. Minimal updates hierarchy and values only on pause and throttles the graph tabs as well. - The graph and state machine tabs stay live under Balanced. At 4 updates a second the beads on exec edges stutter, and the tabs draw only while on screen. This differs from the issue proposal. - The game itself is never throttled. Pause, step and stop bring every panel up to date at once, and a change of structure that falls between two updates is kept for the next one. - The rule lives in editor/playThrottle.ts as panelPolicy and PanelClock. PlaySession asks it after every frame. - Objects spawned at the top level go into a hierarchy group named after their asset, so hundreds of gold pieces fold into one row. Groups start folded. Measured on a throwaway copy of Performance test, 3 s each, fps: - About 500 entities: Live 38.6, Balanced 56, Minimal 60.6. - About 315 entities: Live 56.9, Balanced 60.5, Minimal 60.6. - p95 frame time at 500 entities: 60 ms, 41 ms and 23 ms. Tests: - ./check.sh: 193 files, 2799 tests, CHECK EXIT 0. - GM_STEPS=1,18,59: ALL PASSED, 54 checks. - Full e2e run (local): ALL PASSED, 1562 checks. - tests/editor/play-throttle.test.ts: presets, update rates, kept structure changes, flush, spawn group and loading old projects. - New e2e module 59: preset chosen with the mouse, measured update rates, scene view behind the game tab, Minimal with pause. - Module 03 now expects spawned objects in their group row after the next update. - Counter-checks: 4 in unit tests and 2 in the browser, each failing as expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d7d0a6e7d4 |
Let Tab reach the game in the Game tab (#261)
The game did get Tab, but the editor moved the keyboard focus to a button at the same time. The next Space then pressed that button instead of reaching the game. The key field of On key down could not capture Tab at all, so an inventory on Tab could not be built. - FocusManager asks tabBelongsElsewhere before it moves the focus. The editor answers yes while the Game tab is in front, so Tab stays with the game as long as no field has the focus. - A widget can claim Tab through takesTab. The key capture button does so while it is capturing, so On key down accepts Tab. - The harness knows the key Tab. Found on the way: - The first version of check 20g left the Behavior tab in front, and module 21 then failed in the chained run. 20g now goes back. Tests: - ./check.sh: 2746 tests, CHECK EXIT 0. - e2e 20 has two new checks: Tab reaches the game and leaves the keyboard with it, the key field captures Tab. Both failed against the old bundle. - GM_STEPS=19,20: all passed, 44 checks. The full local run with all three topics of this push passed with 1543 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4857bb75e6 |
Show lists in the inspector and variables in their own tab (#252)
The List tab could show one list at a time and was the only place to see its items. A list now shows its items right where it lives, and a new Variables tab shows all variables of an asset or placed object. - InlineList draws the items of a list under its row in the inspector, in struct fields and in the Variables tab. A list inside a list or a struct field is drawn the same way, one step indented. - The index column is the indent. Items sit under their list without an own margin, and the index column is 16 px wide instead of 28 px. - Above 20 items (INLINE_LIMIT) the rest collapses into "... N more - open in tab". During a run the rows show the values from the game. - ListHead puts "N items" and "+ Item" in one row. "N items" opens the list in a tab. A separate Open button squeezed the counter to zero width in the narrow inspector column. - VariablesEditor shows every variable of a behavior asset, a variables asset or a placed behavior. On an asset it edits the declarations. On a placed object an edit only overrides that object, with the usual reset and push buttons. - Opening a list inside a tab adds a new tab and keeps the open one. valuesKey gives each list its own document, so a list that is already open is shown instead of opened twice. - Titles read "Pickup · Variables" or "Listy · nums · [3]". The tab closes when its asset or object is deleted. - Lists on node inputs stay inline only, a node owns no variables. - The List tab, ListEditor and the __gm.list hook are gone. Tests: - ./check.sh: 181 files, 2645 tests, CHECK EXIT 0 - GM_STEPS=1,21,22,32: ALL PASSED, 123 checks - e2e 21 is rewritten for inline lists and the new tab, 22 and 32 are adapted. Sum in README: 1486 (+6 in 21, -1 in 22), counted by hand. - Counter-probes fail as expected: sub-lists not told apart (1 unit test), sub-list replacing the open tab, all items drawn inline (60 rows instead of 20), no live values (0 rows at 24 items in a run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
92fad6b9e3 |
Show the list a running game fills in the list tab (#251)
While playing, the inspector said "2 items" for a list a graph had filled, but clicking it opened an empty list tab. The inspector reads a Variables asset from the running game, the list tab only read the list stored in the project. It did not follow the run's ticks either. - resolveList takes the live values. For a Variables asset it uses the running game's value while one runs, the project's otherwise. This is the inspector's rule. - ListEditor refreshes on runtime ticks about 10 times a second, and not while a field in the tab is being typed in, like the inspector. - Editor hands the list editor the inspector's liveVariables. - __gm.list.info() also returns rows, the rows the last build drew. - Editing in the tab while playing goes into the run's copy of the project, as in the inspector. A very long, growing list rebuilds all rows up to 10 times a second. Tests: - ./check.sh: 180 files, 2634 tests, CHECK EXIT 0 - e2e GM_STEPS=1,32: ALL PASSED, 30 checks - e2e 32b-ii, 2 checks: a graph adds an item every frame, the test pauses and clicks "N items" with a real mouse. The tab draws 26 rows for 26 items, grows to 50 after resuming and is empty after Stop. - Counter-checks: without live values the tab draws 0 rows for 23 items, and without the refresh it stays at 23 - README: 1481 editor checks, counted by hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
393b8ec7f9 |
Build your own easing curve and use it on keys and Lerp (#247)
The built-in eases cover the usual shapes, but a game often wants its own: a coin that bounces twice, a door that overshoots and settles. A new asset type holds such a curve, and keys and the Lerp node can pick it instead of a built-in ease. - New asset type "curve": points like in Vector art, x is t and y is the value. A new curve is the straight line from (0, 0) to (1, 1). - One pure evaluation in src/core/model/curve.ts serves the timeline preview, the running game and the Lerp node. - The curve always has one value per t. The end points stay at t 0 and 1, points cannot pass each other, and a handle that reaches past its neighbour is shortened along its direction. Loading a hand-edited file applies the same rules. - Curve tab: click on the line to insert a point that keeps the shape, click on free space to put one there, drag points and handles, double-click or Delete removes, Alt-click toggles round and sharp. Every step is one undo step. - Presets: all 30 built-in eases plus Linear, fitted with adaptive subdivision. Each stays within 0.4 % of the built-in ease. - Preview: a dot runs along the curve once a second, a second dot shows only the value. The Preview button stops it. - Animation keys get "Curve" under Easing with a curve picker. Lerp gets "Curve" under Ease, and its result may overshoot b. - A missing or unpicked curve runs linear. The inspector says so, the node shows it as a problem, and deleting a used curve warns with the number of keys and Lerp nodes. Tests: - ./check.sh: 180 files, 2612 tests, CHECK EXIT 0 - GM_STEPS=1-6,25,53: ALL PASSED, 309 checks - New e2e module 53 with 13 checks, all with the real mouse. Expected values come from its own Bezier evaluation. - Counter-checks bite: without Bezier evaluation 4 unit tests fail, without handle clamping 2. In the browser, a key that cannot find its curve, a click that sets no point and a handle drag that moves the point all turn checks red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
30de4d35a8 |
Give the scene tab a header with Fit and the eye (#245)
The scene view had no header bar of its own, and its eye sat in the top bar. Switching between Scene and Game also showed two different pictures, since nothing tied the free view to the camera. - Theme.headerPadding (4) replaces Theme.padding in the top bar and in TabHeader, for the padding and for minHeight. Both bars shrink from 40 to 32 px. Nothing else uses the new value. - The new ScenePane wraps the scene view in a header bar with Fit and the visibility eye. The eye is gone from the top bar, which keeps the menu, undo and redo. - Fit with an active camera shows the world exactly as the Game tab does at its chosen step: the same zoom, and the camera centre on the same screen pixel. The rule is the pure freeViewOfCamera in viewport/view.ts, which also mirrors the letterbox snapping. - Fit without a camera frames all content on a whole step, as before. - New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there. - The doc comment above Theme.treeIndent went away in the working tree along with this. It is committed as found. Not checked: the two views only match while they have the same size. With the tilemap panel open the scene view is narrower, and Auto may pick a smaller step there. Tests: - ./check.sh: 178 files, 2575 tests, CHECK EXIT 0 - 2 new unit tests in view.test.ts: three cameras times four steps land on the same screen point as in the Game tab, Auto stays whole - e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate, camera frame and view box match in both tabs - GM_STEPS=1-3,46: ALL PASSED, 94 checks - Counter-probes: without the centre offset the unit test fails, with Fit ignoring the camera both 3h checks fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2b89292eb4 |
Copy and paste scene objects and graph nodes (#240)
Copy and paste only existed for animation keys. Building a scene or a graph meant placing every object and node again by hand, even when the same arrangement was needed twice. - Ctrl+C copies the selected objects with their whole subtree, or the selected nodes with their links, groups and comments. Ctrl+V pastes into the scene, container or graph of the visible tab. - The copy is a snapshot in the editor's own clipboard, in memory. It holds asset ids, so the system clipboard is not used. It is cleared when another project opens. - A paste lands under the mouse pointer when it is over the view, and keeps its arrangement. Otherwise it lands one grid step beside the original. Each further copy that still exists moves one step on, so an undone copy does not count. - Objects get new ids for the whole subtree. They paste into the one selected object and keep their look under the new parent. If that object is the original or an earlier copy, the paste lands beside it instead of nesting deeper with every Ctrl+V. - A placed container stays a container. Content copied out of a container pastes as plain objects. Pasting a container into its own template is refused. - Nodes paste only where the palette would offer them. A second Start or End, a State in a behavior or a behavior calling itself is left out, and a notice names what was dropped. - Menus: Copy and Paste in the object menu, a new Paste menu on empty scene space, Copy in the node and box menus, Paste in the palette. - Ctrl+V still imports a picture from the system clipboard when the copy does not fit the tab. While objects are copied, the scene and container tabs use Ctrl+V for them, as the timeline does for keys. Not checked in the browser: paste in the container tab, in state machine, shader and particle graphs, and from the hierarchy menu. The shared code path covers them. Tests: - ./check.sh: 175 files, 2531 tests, CHECK EXIT 0 - e2e 51 (new): ALL PASSED, 11 checks with real keys and clicks - 13 unit tests in tests/editor/clipboard.test.ts, one of them pins the grid step after undo found during e2e - 7 counter-checks all fail as expected (6 unit, 1 in the browser) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94749b4f3e |
Let a project keep named snapshots and restore them (#231)
Autosave only keeps the latest revision. Before a risky change there
was no way to put a known good state aside and go back to it later.
The main menu now has "Snapshots..." to create, restore and delete
named copies of the open project.
- Server: SnapshotStore.php keeps each snapshot as a copy of the
project JSON under data/snapshots/<project>/ (git-ignored). Four
routes under projects/{id}/snapshots use the same owner check as the
project, so an admin also sees foreign snapshots.
- Create writes the autosave first. With unsaved edits (play with open
edits, failed save, conflict) no snapshot is made and a message says
why. The rule is the pure function in src/editor/snapshots.ts.
- An empty name falls back to the local date and time.
- Restore asks first, then keeps the current state as "Before
restoring <name>". It writes the snapshot back as a new revision and
reloads the project, so the undo list is empty. A restore can itself
be undone by restoring that snapshot.
- Deleting a project deletes its snapshots too. Images, sounds, fonts
and pixel art are content-addressed and never deleted, so a snapshot
can always reach them.
- Client: SnapshotStore and HttpSnapshotStore in src/core/store, the
SnapshotsDialog, and the flows in ProjectSession. A stale revision
(409) becomes StaleRevisionError.
- CI runs the new PHP test in the gate job next to accounts.
Not covered by e2e: the lock during play and the conflict case. Both
are unit tests only.
Tests:
- check.sh: 164 files, 2401 tests, CHECK EXIT 0.
- tests/php/snapshots.test.php: 14 checks, all passed.
- tests/editor/snapshots.test.ts: 5 tests for the time format, the
blocker rule, the routes and 409.
- New e2e module 50 on a throwaway project: create, rename, restore,
undo the restore, delete, then delete the project and get 404 on
its snapshots. 46b expects the new menu entry. e2e 46,50: 20 checks
passed. The check sum goes from 1385 to 1393 (computed).
- Counter-checks, originals restored and compared with cmp: blocker
always allowed fails 1 unit test. No sorting fails 2 PHP checks.
Restore without keeping the old state fails 50c and aborts 50d.
- The first sort test with 2 snapshots passed by chance half the time
because of random ids. It now uses 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
9ef360ba1b |
Let the game view show the picture at a chosen scale (#225)
The game view always stretched the camera picture to fit the pane.
Pixel art came out blurred at odd factors, and there was no way to
see the game at its real size. The view now has a header with a
scale drop-down, and 100 % is the default.
- The header of the new GamePane offers Auto, Fit, 25 %, 50 %,
100 %, 200 % and 400 %. Auto and Fit show the factor they land
on, e.g. "Auto · 100%", and follow the pane when it resizes.
- 100 % means one screen pixel per pixel of the camera resolution
(width x height). The camera zoom scales the world on top of
that, so a zoomed camera still fills the same frame.
- Auto takes the largest fixed step that fits the whole picture
and falls back to 25 % when nothing fits. Fit keeps the old
behaviour.
- A picture larger than the pane is cropped around the camera
centre. Fixed steps start on a whole pixel so pixel art stays
sharp at 100 %.
- The choice is kept in the browser like "Play starts". Nothing
is written into the project.
Found on the way: the first draft applied the step to the world
instead of the camera resolution. The camera zoom then had no
effect at 100 %, which the existing check 12c caught (zoom 1.37
measured as scale 1).
The CI smokes were not run locally, because that would reseed
projects the user works in. All three smoke cameras are 640x360
and fit the 670x383 harness view at 100 %.
Tests:
- ./check.sh: 156 files, 2298 tests, CHECK EXIT 0 (before 2290).
- 8 unit tests in tests/viewport/view.test.ts for whole-pixel
steps, cropping, Auto and Fit with fallback, and zoomed cameras.
- 7 e2e checks in 03g. e2e 1-17,28,30,38-40: ALL PASSED, 727
checks.
- Counter-checks: no cropping, no rounding, no factor reported to
the header, and no zoom share each fail their own tests.
- Check total: 1367 (CI at
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
73a71473f1 |
The shader tab previews the effect on the last picked image (#168)
A shader could only be judged by placing it on a sprite and running the scene. The shader tab now shows its effect next to the graph, on the image the user picked last, without a picker to maintain. - The preview follows the last selection that has an image: a placed sprite with its overrides, size and tint, a sprite asset, a texture or a frame. Clicking the shader, a node, a behavior or a scene keeps the picture. - Without such a selection, or when the picked image is deleted, a built-in 64 px test image is shown. Its colour runs with u and v, a checkerboard shows where a UV lands and the border fades for alpha. - Each property gets a slider. The sliders belong to the preview and never write to the project. They start at the sprite's values when it carries this shader, otherwise at the default. - The preview has its own clock. Hold and Restart are only enabled when a Time node is in the graph. - The graph | splitter | panel layout of the particles tab moves into PreviewSplit, and both tabs use it. - A nine-sliced frame is previewed unsliced. This is documented. Findings: - The slider row stayed squeezed to a few pixels when the properties changed while the tab was hidden. The layout was triggered one level too high. The slider check in block 28n holds this. - The graph canvas is narrower now, 385 px at a 1280 px window, the same as in the particles tab. Block 28b clicked at a fixed x+420 and now uses the measured width. Tests: - ./check.sh: 132 files, 1952 tests (was 131 / 1941), exit 0 - npm run build: exit 0 - GM_STEPS=28,31 freshly seeded: ALL PASSED, 105 checks - 11 new unit tests in tests/editor/shader-preview.test.ts - 7 new e2e checks in block 28n - Counter-probes: start values ignoring the sprite fail 1 unit test, a preview ignoring the selection fails 2 e2e checks - Check total 1209 in README is computed (1202 + 7), CI measures it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3d7ad65f34 |
Vector art has a tab to draw in, which is stage 3 of #129
Stages 1 and 2 gave the asset a model and a picture, but the only way to put a shape into it was a test calling mutate. The tab closes that gap: what you draw there is what the sprite in the scene carries, with no extra step, because the fingerprint changes and the picture registry of #185 redraws. - The tab has a header with Select, Pen, Grid and Fit, a chequerboard behind the canvas and the canvas itself as a frame. It paints with the same commands as the rasterizer, paintDrawing being exported from viewport/vectorArtPicture.ts for it, so the tab cannot drift away from the picture. - Pen sets points on a click and pulls the handles out on a drag, both of them mirrored; a click on the first point closes the ring and Esc ends an open line. - Select drags a point, a handle or a whole shape, a click on an edge puts a point there, a double click takes one away, and Alt turns a corner round and back. Delete removes the selected shape. - The rules grew inside polygonEdit.ts instead of a second module, as the plan says, so a collider polygon and a frame outline get the same behaviour where it applies. - A point on a curved edge splits the curve by de Casteljau instead of cutting it: both halves together draw exactly what was there before. - A curved edge is walked instead of measured over its chord, or a click on the bow of a curve would grab nothing; the same walk carries the test for the inside of a ring. - One gesture is one undo step. The preview writes the model directly and the old state is put back on release before the command runs, which is the pattern the frame outlines of #141 already use. - state.ts gained a selection kind "shape". The inspector shows the rows of the asset for it, the shape list with colour, opacity and stroke being stage 4. One counter-check was no counter-check: disabling the pen's closing tolerance by multiplying it with 0 left the run green, and rightly so, because the closing click lands on exactly the screen coordinate point 0 came from, so the distance is 0 and <= 0 is true. Only an unsatisfiable condition really switched it off, and then 2 checks fail. A second trap on the way: a false && in front of the guard breaks the TS narrowing, npm run build bails out, and two runs went against the old bundle. Tests: check.sh 127 files, 1878 tests, EXIT 0 (was 126 / 1860). Full e2e suite ALL PASSED - 1172 checks, module 47 alone 17 (was 10). 18 new unit tests in tests/editor/ring-edit.test.ts and 7 new e2e checks in blocks 47c and 47d. Two counter-checks hold: no de Casteljau fails 1 unit test, measuring a curve over its chord fails another. Originals checked back with cmp and rebuilt. |
||
|
|
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> |
||
|
|
60d0146bde |
The topbar is icons only: the leading Project dropdown, the Scene picker and the labelled Play/Pause buttons are gone and in their place stand five icon buttons - menu, undo, redo on the left, play and pause in the middle - while Notice, the save state, fps and the Account menu keep the right side; the Play button swaps its icon for a stop square while the project runs and Pause swaps for a second play sign while it is held (Button.setIcon()), and iconSpace() drops the gap between icon and label when a button has no label, which is what kept a 24 px icon button 3 px off its own centre; the menu carries New project, Open project..., a separator and Project settings..., where the picker (src/editor/OpenProjectDialog.ts) preselects the open project and marks it "open", opens on a double click and puts Delete on the left of Cancel and Open - it closes itself before it asks whether to delete, because two modals on top of each other would have left the question standing over a list it no longer speaks about - and the settings dialog (src/editor/ProjectSettingsDialog.ts) holds the name, the "Play starts" switch (Start scene / Active scene, a preference of this browser and so no command) and the collision layers from #152, which retires CollisionLayersDialog.ts and the "Collision layers..." entry; what Apply actually writes is the pure function settingsChanges(), so renaming and the layers are each their own undo step and each drop out when nothing differs; the Scene dropdown falls away without replacement, the active scene is switched by double clicking it in the Assets panel, which is the way that was there anyway; new module 46 runs last with 9 checks (no label left in the bar, the run buttons in edit mode, the four menu entries, the picker with the open project preselected, Cancel leaving the project standing, the settings dialog on the state of the project, renaming with undo and redo through the icon buttons) and it neither opens nor deletes a project, because that would pull the project out from under every earlier module; modules 8, 18 and 35 and the harness follow the new bar, 6 unit tests cover settingsChanges and 3 the icons (#183)
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> |
||
|
|
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> |
||
|
|
60ea56a2ce |
Open asset greyed out and real tab cards: Open asset in the scene object menu is only pickable for kinds with an editor of their own, the table of which asset opens which tab moved from Editor.ts into the new module assetTabs.ts with hasAssetEditor so the menu and the editor read the same list, Select asset stays as it was; tabs are drawn as index cards (new module TabButton.ts): the one showing is a light grey card instead of the aggressive blue, rounded above and square below, its name in plain weight and measured bold so it never clips itself, and a mouse down on the tab already showing keeps its colour; the padding left and right grew to 6 px in one place for both kinds of tab, Scene and Game carry an icon too (the scene one from the asset panel, a new game.svg play sign), Plugins and a plugin's own tab have none; new test hook menuEnabled next to menuRows and menuIcons, e2e 01 checks the greyed out entry on a sprite row, 14a-ii the card and the icons of the fixed tabs, 44a the icon at the wider padding (#176)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3ebf44593 |
Asset panel grouped by kind with icons: assets are listed under a header per kind (Sprites, Behaviors, ...) with the count on the right, empty groups are not shown, every group starts folded when a project is opened, a click on the header folds and unfolds it, selecting an asset elsewhere unfolds its group and folding the group of the selected asset clears the selection; every asset kind has an SVG icon in src/editor/panels/icons/ drawn before the name, the pure rule for grouping and sorting lives in assetGroups.ts; the same icons replace the kind word in document tabs and the overflow menu (a list icon for the List tab, which also fixes the close x lying over the end of the active tab's text), stand before every entry of the + menu (Button got an icon option) and on the right of scene object rows instead of the type name; the colour swatch next to sprite names is gone from both panels; the SVG loader moved to src/ui/svg.ts for both icon sets; test hook assetRow unfolds the group and scrolls to the row, assetRowShown only reads, new hook menuIcons; e2e 4g and 4h, 14 and 44a check the icons (#176)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cc285af10a |
Tabs are open documents: Scene, Game and Plugins stay as fixed tabs, every other asset opens in its own tab (double click or Enter in the asset panel, on creation, from the list button), several assets of one kind can be open at once, a tab is labelled with its kind dimmed before the name, cut at 160 px and renamed with its asset, closes with its x or a middle click and goes away when its asset is deleted; a tab always shows its own document instead of following the selection, the picker in the tab head is gone, zoom and scroll are kept per document for the session and dropped on close, a double click on a call node stays in the same tab with breadcrumbs per tab; one editor per kind is switched between documents, the placement rules live in the pure module documents.ts, and the test hook showTab stays as a compatibility path; new e2e module 44 documents (#171)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a31f5e0ad |
One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5046ddb810 |
The tilemap painting tools move out of the inspector into their own panel beside the scene view, shown only while exactly one placed tilemap is selected in the scene tab; the free viewports keep their content in place on screen when they are resized (#142)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
732f6518ae |
The autotile asset is renamed to tile and gains a plain None (1 × 1) kind, which is now the default; older projects load their autotiles as floor tiles (#131)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55bfa7521a |
The assets and scene objects panels in the left column are split by a draggable splitter that keeps its ratio when the window is resized
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1a9f07c4ac |
Autotiles are a new asset type with a tab that shows every tile of the floor (2x3) or wall (2x2) block and an animated sample pattern (#118)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c17b2b5426 |
Plugins are switched on and off in a Plugins tab, the server keeps the state in data/plugins/plugins.json, and switched-off plugins are never delivered (#114)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
57a97ba94f |
Plugins in data/plugins are loaded at startup and can add a tab with a read-only node board, and the demo plugin shows every asset as a node (#113)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bf02c9e8b3 |
Timeline keys can be copied and pasted at the playhead, duplicated with Ctrl+drag, and frames inserted or removed from a right-click menu (#102)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1e9d21cd5d |
A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays. |
||
|
|
5671a38044 |
A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter. |
||
|
|
7babf88636 |
A state change is something the graph can react to, so the machine announces it (#53, #54, #55, #57)
game-maker / suite (push) Successful in 10m58s
Four issues that all reach for the same line: how the current state is read and written. Splitting them into four commits would mean four states of the tree that do not compile, so they arrive together. #55 start state — the machine asset carries an optional `start` holding a State node's id. Right-click a state to set or remove it, a ring in the node's title says which one it is. A dead id (the state was deleted) reads as "no start", so nothing has to clean up after a deletion. #53 entered/exited — `On state entered` and `On state exited`, one exec output per state. Every change now goes through a single `setState` that reports it; BehaviorSystem delivers it after the exec flow that caused it, `exited` of the old before `entered` of the new, round after round until the queue is empty, with a cap that stops a ping-pong graph with an endless-loop error. A local machine tells the instance that switched, a global one tells every instance holding a matching event node. The start state fires `entered` too: per instance for a local machine, once per run for a global one. #57 reading — `Current state` with `name` for showing and `id` for comparing, `Is in state` with one boolean per state. Neither has a `run`, so `read` learned `def` and `lookup`, and the catalog test knows this third kind of node. #54 seeing it — during a run the state machine tab marks every state some instance is holding faintly, and the one held by the selected scene object's instance strongly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bc57f61c91 |
A still editor has nothing to draw, so the frame loop wakes on change (#44)
game-maker / suite (push) Successful in 10m46s
The renderer is taken off the ticker; the ticker itself keeps running, so the clock, a live world and the panels still get their step. Drawing now happens inside a 200 ms trailing window that every pointer, wheel and key event, every Widget.invalidate/resize, every Stack.relayout, every EditorState.emit and both library onChange hooks reopen. The window rather than a single frame is what makes the ~70 direct redraw() calls that never pass through invalidate() safe, along with repaints an event only starts: a microtask, a texture upload, a resolved font. Editor.movesByItself() keeps asking for every frame while Play or an editor run is on, a time shader is on screen, or the particle or animation preview is showing. The FPS counter now counts drawn frames over the time the renderer was awake. Idle seconds are not slow seconds, so it answers "how smooth while it moves" and holds its last answer instead of falling to 0 at rest. Measured on the built stand, 20 s per variant, CPU of all chromium processes from /proc: kept awake 75.5 % of a core at 60 fps, render on change 8.8 % with 0 frames drawn, awake again 83.9 % as an order probe. No colour check can see a missing repaint: pixelAt extracts from the stage on demand and is just as green on a screen frozen a minute ago. Step 33 watches the new __gm.renderStats() counter instead. |
||
|
|
8143130ed2 |
A shared value belongs to no behavior, so it becomes an asset of its own (#91)
game-maker / suite (push) Failing after 8m59s
This reverses the design of the previous commit. `PropertyDef.scope` is gone without replacement: a behavior variable is an object property again, without exception, and sharing is something you point at rather than something you declare twice. A **Variables asset** is declarations and nothing else -- no graph, no position, never placed, no tab of its own (`TAB_OF_ASSET` is already `Partial<>`, so that costs no line). A `Variables` node names one through its new `variablesAsset` setting, empty meaning the behavior's own variables as before, and then reads and writes one value per key for the whole run. Its property list follows the chosen source, which was the actual request: a value another graph declared is now offerable, where before every participant had to declare the name itself. That deletes more than it adds (-72 lines net): `globalConflicts` and its run-stopping kind clash, `SetVariableScopeCommand`, the scope picker, both object-inspector filters and the "shared by the whole run" note on the placed object. A typo is no longer a silent second variable -- naming an asset is picking from a list. And two Variables assets may carry the same key without meeting, because the asset is the key, not the name. The one shape it copies is the one that already existed: `Set state` picks its machine the same way. Lifetime is unchanged. The bag is built at `BehaviorSystem.start()` from the declarations and never written back, so playing in the editor cannot change the project -- which is the whole reason a counter must not be an object property (`World.syncToModel` would `commit()` it, and the coin counter would stand at 7 for good). Full symmetry across that gap, without mirroring anything. Two named wires: `Inspector.liveVariables` shows the values as they stand while a run is on and falls back to the declarations when it ends, and `adoptDeclarations()` takes a key into the run *only where the document changed it since the last sync*. Without that condition any unrelated project edit would snap a running counter back to its starting value; a counter-test bends exactly that and falls over. Nothing was ever cached, incidentally: `GraphInterpreter` only pulse-caches nodes without a `read`, and the variable node has one. The wire was missing. The state machine `Scope` row is now an ordinary `enum` row through `createFieldRow`. It was hand-built from an `HStack` with a ghost dropdown and began ~34px left of every value above it; the explanations beside `Local` and `Global` are gone from the menu, the sentence below the row keeps saying it. Machine scope is otherwise untouched -- `StateMachineAsset.scope`, `SetMachineScopeCommand`, `stateBag()` and its persistence all stand, so #59 stays closed as built and `Scope` as a type now carries machines alone. Six counter-proofs, each biting at its own guard. One of them did not bite at first: the suggestion list was computed twice, in `nodeSection` and `portSection`, so only the browser test covered the request itself. It is one named function now, `variableChoices`, with a unit test of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0e0a21dee1 |
One clock under everything that moves by itself (#90)
game-maker / suite (push) Successful in 10m4s
Particle age and shader time each read performance.now() off their own library, and the world got the frame's delta straight from the ticker. Three clocks, so a global time scale would have been three multiplications and a pause three separate freezes. core/time.ts holds one clock instead: it accumulates dt x scale per frame rather than reading the wall clock, and Editor.tick hands the very step it took to the play session, the editor run and the animation preview. Delta time, particle age, shader time and that preview are the same quantity now, so the scale is a single multiplication and a pause is that scale at 0 — nothing underneath needs rebasing, because the clock itself stands still. The Math.min(dt, 0.1) caps in PlaySession and EditorRun are gone: the cap (MAX_FRAME) now sits on the raw frame before scaling, and capping twice would make the world run slower than the particles at scale 2. A pause gates the scale rather than overwriting it — effectiveScale() answers 0 while paused, and for anything that is not a positive finite number, while gameClock.scale keeps its value. A slider later survives a pause that way; the issue asks for none yet. The clock goes back to zero at both ends of a run, so a run is reproducible from second 0 and the scene view looks freshly loaded after Stop. Resetting only on Stop would not do it: the editor clock runs on afterwards, and the next Play would start at "seconds since the last Stop". The trap in that: ParticleField keeps its origin as an absolute second of this clock, so after a reset it lies in the future and every particle would age backwards. One line in sync() starts such a field over, which covers the scene, the game and the tab preview at once — and fixes an older bug of the same shape, where releasing a particleTime() freeze left the preview empty for good. Found while measuring that: ParticlePreview stood at second 0.5 by computing the clock reading that would mean 0.5 since the field started. Over 200000 random origins the round trip misses the last bit in 18.8% of them and shows one particle too few in 9.4%, because floor(0.5 * 20) comes out 9. The check had been flaky since it was written. The panel hands the field the age itself now (ParticleField.hold), and the faked clock is gone. 833 checks (824 + 9), 931 unit tests (921 + 10). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0f9e1611d1 |
A particle is a function of its age, not a thing that is stepped (#73)
game-maker / suite (push) Failing after 4m22s
The asset type `particles` is placeable and carries a graph at the same time — `behavior` was the blueprint. Because a continuous emitter gives birth to particle n at time n/rate, the whole field is a function of the clock rather than of the frames: no ring-buffer bookkeeping, exact time jumps, reproducibility as a measurable property. compile.ts follows compileShader's frame but emits JS closures into a Float64Array instead of GLSL, and without eval; a node with two consumers is evaluated once. Drawing is one Pixi ParticleContainer per source, so one draw call. Collision, wind and braking in flight are out for good — that is the price of the formula model, not an omission. The tab count goes from nine to ten, which is exactly the case #84 was built for one day earlier: at 1280 px the row no longer holds every tab, so guard 14b now asks for hidden.length > 0 instead of === 0. |
||
|
|
f83c568999 |
Tab row: what does not fit goes behind a menu, the active tab always stays (#84)
game-maker / suite (push) Successful in 9m18s
Nine tabs need 631 px; at 1280x800 the centre column offers 666 px, so the tenth tab would have spilled under the inspector - the same overflow CI caught in run 5. TabStrip replaces the bare HStack: it fits the longest prefix that stays inside the budget, always appends the active tab, and offers the rest behind a fixed 28 px button. The button's width is fixed on purpose - deriving it from the number of hidden tabs would make width depend on the count and the count on the width. The rule itself is a pure function (tabRow.ts), so it is checked without a browser. setActive() calls relayout() itself because Widget.resize only lays out on a changed size, and switching tabs does not change any size. Adding a tab now needs no space arithmetic - only a line in TAB_ORDER, TAB_LABELS and panes, all three Record<CentreTab, ...>, so forgetting one is a type error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
86b5363a01 |
Fonts: a new asset type, picked from the server or dropped in, worn by text objects (#74)
game-maker / suite (push) Successful in 9m10s
A font asset holds a file and nothing else, the twin of a texture: the server keeps it
content-addressed under data/fonts/{sha256}.{ext}, with the format read from the file's own
four-byte signature (finfo reports fonts inconsistently across libmagic versions). A text
wears one through its ordinary Font property, so overrides, reset, undo and the Properties
node come for free; no font means the editor's own typeface.
The picker lists everything the server holds and renders each row in the face it offers.
The family name is gm-font-<hash16>, never the name inside the file, so two "Regular" cannot
collide. WorldViewport.syncText is the single place that draws it, which covers scene, game
preview and play at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f85606427b |
Shader graph: UV, Sample texture, Texture size, Time, Sin/Floor/Fract/Step; edge mode on Output (#78)
game-maker / suite (push) Successful in 8m38s
Fragment-side coordinate nodes instead of vertex nodes: the quad stays the quad, a UV read plus a sample at a computed coordinate covers flip, scroll, wave, pixelate and scanlines. Time is a uniform only when a Time node reaches the Output; a viewport that drew a Time shader redraws from the ticker so the Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a second texture variant in TextureLibrary because WebGL binds no TextureStyle resource of the shader. The compiler now emits #version 300 es first: Pixi only compiles as ES 3.00 when the fragment says so, and textureSize needs it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |