Commit Graph
96 Commits
Author SHA1 Message Date
adminandClaude Opus 5 66d6aafbb4 Pixel art can be drawn in its own tab with layers and frames (#130)
game-maker / gate (push) Successful in 2m24s
game-maker / suite (push) Failing after 7m40s
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>
2026-09-21 17:33:44 +02:00
adminandClaude Opus 5 592ea942d9 Vector shapes can fill with gradients and textures (#186)
game-maker / gate (push) Successful in 2m14s
game-maker / suite (push) Failing after 7m14s
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>
2026-09-21 13:05:02 +02:00
adminandClaude Opus 5 1116818011 Vector shapes can be combined by union, subtract and more (#187)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m51s
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>
2026-09-21 11:56:41 +02:00
adminandClaude Opus 5 2d79230b55 A frame's collision outline can be generated from its pixels (#161)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m42s
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>
2026-09-21 08:00:30 +02:00
adminandClaude Opus 5 f4ec23d3d3 Colliders take a frame's outline and tiles have their own surface (#193)
game-maker / gate (push) Successful in 2m11s
game-maker / suite (push) Successful in 7m33s
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>
2026-09-21 07:29:26 +02:00
adminandClaude Opus 5 3ff7e91dbf Vector art points have four types and the pen draws whole curves (#195)
game-maker / gate (push) Successful in 2m10s
game-maker / suite (push) Successful in 7m29s
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>
2026-09-21 06:51:35 +02:00
admin 3d7ad65f34 Vector art has a tab to draw in, which is stage 3 of #129
game-maker / gate (push) Canceled after 37s
game-maker / suite (push) Canceled after 37s
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.
2026-09-20 21:34:14 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 2m9s
game-maker / suite (push) Successful in 7m26s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:51:54 +02:00
adminandClaude Opus 5 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>
2026-09-20 20:29:35 +02:00
adminandClaude Opus 5 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>
2026-09-20 20:09:42 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 2m4s
game-maker / suite (push) Successful in 7m21s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:25:53 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 2m0s
game-maker / suite (push) Successful in 7m17s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 17:24:42 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 2m41s
game-maker / suite (push) Failing after 8m21s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:31:22 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 8m21s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 14:24:27 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 1m58s
game-maker / suite (push) Successful in 6m54s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 18:36:20 +02:00
adminandClaude Opus 5 47eea2f6e4 Each tilemap layer has its own Z: an offset added to the Z of the placed tilemap, like a child's Z to its parent, default 0 keeps the previous order (layers right after their map, in list order), a negative Z sorts a layer below the map, on a tie layers keep their list order and follow the hierarchy against other objects; editor scene and game use the same order through the new pure function drawSlots, a tilemap draws into one container per layer, and the tilemap panel gains a Z field for the selected layer with one undo step per change (#174)
game-maker / gate (push) Successful in 1m58s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:03:20 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:13:29 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 1m56s
game-maker / suite (push) Successful in 6m54s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 12:54:53 +02:00
adminandClaude Opus 5 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
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:36:11 +02:00
adminandClaude Opus 5 947a6b1977 Collision outlines for autotiles: a frame holds several outlines (click adds a corner to the active one, Ctrl/Cmd-click starts a new one, clicking another outline makes it active, Delete removes the active outline and never the frame in collision mode), an autotile's outlines are drawn over the whole block, cut at the quarters and each cell takes the pieces of exactly the quarters its image is made of, so edges, outer corners and notches follow automatically; a tile of the block with nothing drawn blocks fully, cells of only full quarters merge back into rectangles, collision scale does not apply to an outlined autotile, and the previous format with one flat outline still loads (#141)
game-maker / gate (push) Successful in 1m53s
game-maker / suite (push) Successful in 6m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:56:20 +02:00
adminandClaude Opus 5 32057a96fc Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
game-maker / gate (push) Successful in 1m52s
game-maker / suite (push) Successful in 6m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:56:25 +02:00
adminandClaude Opus 5 78899b1ace Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
game-maker / gate (push) Successful in 1m46s
game-maker / suite (push) Successful in 6m17s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:11:43 +02:00
adminandClaude Opus 5 6d7d0005cf The tab headers are one TabHeader, the node board and the tile board pan and zoom through one helper, reordering tile pictures and tilemap layers goes through one moveItem, and a tilemap cell is one named type (#85)
game-maker / gate (push) Successful in 1m42s
game-maker / suite (push) Successful in 6m9s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:01:12 +02:00
adminandClaude Opus 5 bffa508ee7 A tile gains Collision scale: below 1 each blocking cell pulls in on the sides without a blocking neighbour and stays flush towards its neighbours, so a wall of bushes becomes a narrower wall without gaps and the inner corner of an L closes cleanly; above 1 the cell grows at its open sides; on several layers the largest value counts, and the merged rectangles work in fractions of a cell with exactly the old result at 1 (#140)
game-maker / gate (push) Successful in 1m43s
game-maker / suite (push) Successful in 6m27s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:00:07 +02:00
adminandClaude Opus 5 4728f4fab5 A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
game-maker / gate (push) Successful in 1m43s
game-maker / suite (push) Successful in 6m8s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:58:03 +02:00
adminandClaude Opus 5 0b32d3cbf3 The tilemap panel gains Fill and Pick: Fill floods the connected cells of the active layer that carry the same tile (edge neighbours only, bounded by the grid) as one undo step, Pick takes the tile of the clicked cell on the active layer, and holding Alt picks while any tool is active (#133)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m7s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:31:19 +02:00
adminandClaude Opus 5 bfbcbe604c The tile list in the tilemap section is a palette of pictures: each tile shows its lone tile (the four convex corners for floor and wall, the picture itself for none) with its name below, laid out with the image-picker grid (#139)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m15s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:15:51 +02:00
adminandClaude Opus 5 4c411c1ff3 Tilemaps can block movement: a tile gains a Solid switch, the solid cells of all layers are merged into rectangles and the placed tilemap takes part in the collision system as one body, reported as other in collision events (#132)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m3s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 09:47:03 +02:00
adminandClaude Opus 5 10b49fac46 Tilemap cells are stored compactly in the project file: each layer keeps a tile list, an origin and one run-length encoded string per row; older projects with a cell map still load (#134)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 5m59s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 09:17:49 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 1m40s
game-maker / suite (push) Successful in 5m56s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 08:01:07 +02:00
adminandClaude Opus 5 33a7b1b63e Tilemaps are a new placeable asset type that is painted with autotiles in the scene view, with layers, brush, eraser and rectangle, and is drawn at runtime and in the export (#128)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m51s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:41:47 +02:00
adminandClaude Opus 5 0607221423 Autotile floor tiles take their inner quadrants rotated by half a tile and their inner corners from the upper right block tile, matching RPG Maker MZ (#118)
game-maker / gate (push) Successful in 1m39s
game-maker / suite (push) Successful in 5m47s
The corner tables are now derived from rmmz_core.js (Tilemap._addAutotile,
FLOOR_AUTOTILE_TABLE, WALL_AUTOTILE_TABLE): the fill quadrants are taken
rotated by half a tile so that neighbouring tiles meet with the quarters that
sit next to each other on the sheet, and the inner corners come from the upper
right tile of the block, not the upper left one. A test pins the model against
both MZ tables, generated from rmmz_core.js rather than typed out.

The animation check in e2e module 39 read its four pixels one by one and could
straddle a frame change; step and pixels are now read in a single call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:28:11 +02:00
adminandClaude Opus 5 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)
game-maker / gate (push) Successful in 1m50s
game-maker / suite (push) Successful in 5m59s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:35:29 +02:00
adminandClaude Opus 5 2a390e52ff The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
game-maker / gate (push) Successful in 1m36s
game-maker / suite (push) Successful in 5m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:19:53 +02:00
adminandClaude Opus 5 195fc94e08 Animations blend out on Stop and at the end of Once (reset), and Play can restart a running animation (#106)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m37s
Stop animation gets a blend (s) port that fades the animation back to what
lies underneath. Play animation gets blend out (s) for Once (reset) and a
restart port that starts a running, paused or fading animation from frame 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:50:10 +02:00
adminandClaude Opus 5 8a377d9239 Play animation's blend is given in seconds, not in frames of the new animation (#106)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:14:15 +02:00
adminandClaude Opus 5 04dc254b3d Play animation can blend into the new animation over a number of frames (#106)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m34s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:03:53 +02:00
adminandClaude Opus 5 beeea355d9 Animation rows can target behavior variables and shader parameters (#104)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m38s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:04:29 +02:00
adminandClaude Opus 5 b278605c53 Keys carry an easing for the span to the next key: linear, step, ease in, ease out, ease in-out (#101)
game-maker / gate (push) Successful in 1m32s
game-maker / suite (push) Successful in 5m36s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 07:27:44 +02:00
adminandClaude Opus 5 cc14988338 Ping-pong turns on the last key, and stepped rows hold the key they last passed (#112)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 07:08:44 +02:00
adminandClaude Opus 5 f68941fa3a An animation reaches the scene's own background and objects inside placed containers (#109)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m37s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 06:48:21 +02:00
adminandClaude Fable 5.1 89c46c30a0 An animation is previewed above its timeline: the root at the playhead, drawn but never written (#99)
game-maker / gate (push) Successful in 1m47s
game-maker / suite (push) Failing after 4m55s
The Animation tab shows the root asset over the timeline, with every row's value at the
playhead written onto a copy of it – the sprite, text or container placed at the origin as a
scene would place it, a scene as itself. Rows reach the root through that placed object and its
children through the object's instance edits, the route a placed container's content takes,
so the ids match what the runtime gives them. The runtime player and the preview evaluate a row
through one function (trackWriteAt), relative rows included; past the end of a once-reset pass
the preview shows the starting values, as the player would. Play, pause and the ruler drive the
preview through the timeline's playhead; a drag pans, the wheel zooms, Centre frames it. The
document is never touched and undo never hears of scrubbing.

On animation finished no longer describes itself as hearing "a sprite of the owner".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:00:18 +02:00
adminandClaude Opus 5 4ed2edd0f0 An animation is a value a graph can pick, and playing a running one makes it win again (#97)
Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:47:31 +02:00
adminandClaude Opus 5 5af0a1c83a An animation's rows are edited on a timeline: double click sets a key, keys are banded, dragged and deleted, the ruler scrubs (#98)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:38:34 +02:00
adminandClaude Opus 5 193378e6a9 An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes.

- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
  property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
  between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
  relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
  start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
  builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:02:25 +02:00
adminandClaude Opus 5 bf0be66c9d A frame with borders tiles unless told to stretch
game-maker / gate (push) Successful in 1m32s
game-maker / suite (push) Successful in 5m47s
The fill of a sliced frame defaulted to `Stretch`. It is `Repeat` now, and the Fill dropdown lists
`Repeat` first.

The saved document keeps writing only the fill that differs from the default, so the key it
carries is now `stretch`, and a frame without it repeats. A frame saved before with the key
absent and meaning stretch would therefore tile; none of the stored projects has one - both sliced
frames on the server carry `fill: "repeat"` explicitly, so they draw as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:13:11 +02:00
adminandClaude Opus 5 94a0a578ed A border drawn in pixels is as wide as the texture says a pixel is, so a small sheet can frame a large sprite (#96)
A sliced frame drew its borders one source pixel to one sprite unit. Pixel-art sheets are small:
a 16px corner on a 400-unit button is a thin line, and the only way around it was a larger image.

The texture now carries `Nine-slice scale` (default 1), next to `Filter`. It sets how many sprite
units one border pixel takes, for every sliced frame of that texture. The UVs stay as they are -
a corner shows the same pixels, only larger or smaller. When the sprite is too small for both
borders they are still scaled down together, now starting from the scaled size. With `Repeat`
the tile size scales too, otherwise a doubled border would sit beside a pattern of the old size.

The value is a schema row, so inspector field, saving and undo come without their own code.
Anything that is not a size above 0 reads as 1, and documents without it draw as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:09:43 +02:00
adminandClaude Fable 5.1 cc6db5f350 Three stores, three importers and three pickers that differ only in their type are one of each (#85)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:38:54 +02:00
adminandClaude Opus 5 be8f2331de A frame that keeps its corners can be stretched to any size (#41)
game-maker / suite (push) Successful in 6m21s
TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.

The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.

A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.

The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:04:09 +02:00
admin 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.
2026-09-15 13:24:26 +02:00