diff --git a/.changeset/debug-freezes-world-values.md b/.changeset/debug-freezes-world-values.md deleted file mode 100644 index 32d1ccf0..00000000 --- a/.changeset/debug-freezes-world-values.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@typeonce/ritmo": minor ---- - -**Breaking (debug runtimes only): a runtime made with `debug` freezes every value it is handed, so mutating one afterwards throws where it mutates.** Values were stored by reference: an object passed to `Game.Command.spawn`, `commands.insert`, a cell's `set`/`update`, a resource write, `emit`, `runtime.send`, a next-state `set`, or `Game.Runtime.make` stayed connected to the code that made it. Mutating it later changed the world with no write, so `changed` readers missed it, and a `rewind` brought back the mutated value instead of the checkpointed one. A debug runtime now deep-freezes each value as it is queued or written, and the mutation throws a `TypeError`. Typed arrays are skipped, `Map` and `Set` contents stay writable, and runtimes without `debug` behave as before. - -If your game's tests or devtools sessions now throw, build a new value instead of mutating the one you handed over: - -```diff --const body = { x: 1, trail: [] } --commands.spawn(Game.Command.spawn([Body, body])) --body.x += 1 -+commands.spawn(Game.Command.spawn([Body, { x: 1, trail: [] }])) -+// later, in a system -+data.body.update((body) => ({ ...body, x: body.x + 1 })) -``` - -`Runtime.tick` now points to `FixedLoop.start` for ticking at a fixed rate from a frame loop. The `ritmo` skill now treats a variable a system closes over like a module variable: host input reaches systems through a resource, a sent event, or an input source service. diff --git a/.changeset/effect-4-1-1.md b/.changeset/effect-4-1-1.md deleted file mode 100644 index 5d3db7b8..00000000 --- a/.changeset/effect-4-1-1.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@typeonce/ritmo-tools": patch ---- - -`ritmo-tools` depends on Effect 4.1.1 (`effect`, `@effect/platform-node`, and `@effect/platform-node-shared`, still pinned exactly and together). diff --git a/.changeset/fx-native-size-approved-pixels.md b/.changeset/fx-native-size-approved-pixels.md deleted file mode 100644 index 5b906d3b..00000000 --- a/.changeset/fx-native-size-approved-pixels.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@typeonce/ritmo-tools": minor -"@typeonce/ritmo": patch ---- - -`ritmo-tools fx`: judge effects at native size, and see when an approved effect has changed. Ideas from [PVFX Foundry](https://nerijs.itch.io/pvfx-foundry). - -- **1× zoom** on the review page, to judge whether an effect reads at its native size. -- **Approval keeps what the person saw.** Approving an effect saves the draft in use, its fps, and a hash of each frame's final colors. When any of them later differ, `fx notes` and `fx render` list `changedSinceApproved` (`"frames 3-5 look different"`), and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. A palette edit counts as a change. A review saved before this has no hashes, and reports nothing until the effect is approved again. -- The `ritmo-fx` skill adds a craft rule (each material dies its own way), the native-size question for the person, what to do with `changedSinceApproved`, and PVFX Foundry's Strict tier as a reference to look at. -- The example `explosion` draft `a` now sits on the floor, with thrown debris, embers, dust, and smoke that comes apart in clumps. - -Breaking: `fx render` writes sprite sheets in rows of up to 8 frames, not one row, so a long effect stays within a device's texture limit. A game that reads each frame's rectangle from the JSON (`frames["hit 0"].frame`, as Pixi and Phaser do) needs no change. Replace code that computes a frame's place as `x = frame * width` with `x = (frame % 8) * width` and `y = Math.floor(frame / 8) * height`. diff --git a/.changeset/music-review-all.md b/.changeset/music-review-all.md deleted file mode 100644 index 9c0bc974..00000000 --- a/.changeset/music-review-all.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@typeonce/ritmo-tools": minor -"@typeonce/ritmo": patch ---- - -`ritmo-tools music` and `sound`: review all of a game's music on one page, and see when something approved has changed. - -- **`music review` takes songs, audio files, and folders**, and shows them all on a list with each one's status, filtered by what is left to do. A folder is searched for `*.song.json` and audio files, leaving out what the tool wrote beside a song (its render, its stems, its hum). What the person has not saved yet is written before another piece opens. -- **Plain audio files can be reviewed** (`.wav`, `.flac`, `.mp3`, `.ogg`, `.m4a`): play, loop, hear the loop point, read the length, loudness, and peak, approve or ask for changes, and comment on a stretch of time. Their reviews live in one `music.review.json` beside the game's `package.json`, not beside the files, since those often sit in a folder that ships with the game. -- **Approval keeps what the person heard**, in `music` and `sound` as it already did in `fx`. When an approved song, sound, or file later differs, `music notes`, `music render`, `sound notes`, and `sound render` list `changedSinceApproved`, and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. An edit to words alone (an `about`, a track's name) is no change. A review from before this keeps its approval, with nothing to compare against until it is approved again. -- The music page checks a song in a worker, so a long song no longer holds the page for seconds when it opens, and going back to a song is instant. - -Breaking: `music notes` now prints `{ summary, music: [...] }` for any paths, one entry per piece, with `file`, `kind`, and `review` beside what it printed before. Replace `.status`, `.note`, `.comments`, `.edits`, and `.next` with `.music[0].status` and so on. `music review` prints `music` (the list of what it found) in place of `song` and `review`. diff --git a/.changeset/node-22.md b/.changeset/node-22.md deleted file mode 100644 index f9a643b2..00000000 --- a/.changeset/node-22.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@typeonce/ritmo": minor -"@typeonce/ritmo-browser": minor -"@typeonce/ritmo-render": minor -"@typeonce/ritmo-pixi": minor -"@typeonce/ritmo-math": minor -"@typeonce/ritmo-netcode": minor -"@typeonce/ritmo-devtools": minor -"@typeonce/oxlint-plugin-ritmo": minor -"@typeonce/ritmo-tools": minor ---- - -**Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. diff --git a/.changeset/pixel-holds-onion-seams.md b/.changeset/pixel-holds-onion-seams.md deleted file mode 100644 index 4bb6be62..00000000 --- a/.changeset/pixel-holds-onion-seams.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@typeonce/ritmo-tools": minor -"@typeonce/ritmo": patch ---- - -`ritmo-tools pixel`: hold a frame, see an animation as a still, and check a tile's edges. Ideas from [Pixelorama](https://github.com/Orama-Interactive/Pixelorama). - -- **Holds.** `pixel sheet --animations "attack=5,6x3,7,8"` shows frame 6 for 3 beats, a beat being one frame at `--fps`. The JSON's `duration`, the GIF, the recipe (`sheet.frames[i].beats`), and the page's player all follow it, the page sets it on the chosen frame, and the report lists each frame's time in `durations`. -- **Onion pictures.** `pixel sheet` writes `..onion.png`: a panel per step of playback, each frame over the one before in red and the one after in blue, so an agent that cannot watch an animation can see a jump, a drift, or a change of size. -- **Tile seams.** `pixel lint tile.png --tile both` (or `x`, `y`) warns of a `seam` when a tile's edges do not meet, and `pixel preview tile.png --tile both` writes it repeated. -- The `ritmo-pixel-art` skill says when to use each, and that a game plays each frame for its `duration`. - -Breaking: the sprite sheet JSON of `pixel sheet` and `fx render` no longer has `meta.fps`, since one rate would lose a frame's hold. Replace `meta.fps[name]` with each frame's time, `frames[" 0"].duration`, in milliseconds. With Pixi, give `AnimatedSprite` `{ texture, time }` objects built from those durations. diff --git a/.changeset/pixel-outlines-bent-grids.md b/.changeset/pixel-outlines-bent-grids.md deleted file mode 100644 index c80ff674..00000000 --- a/.changeset/pixel-outlines-bent-grids.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@typeonce/ritmo-tools": minor ---- - -`ritmo-tools pixel`: one-pixel outlines, bent grids, and a shrink that keeps strokes whole, after [ToPixel](https://github.com/daisuqe/ToPixel). - -- **Outlines** (`outlines` in a recipe, `--outlines auto|on|off`, **Outlines** on the page; `"auto"` by default): when the picture's dark strokes are about one cell wide, a stroke a grid line split between two pixels goes back to one, never breaking the outline or a corner, and followed lines keep off dark strokes. The report's `grid.outlines` says whether it ran. -- **Follow lines** is now a choice: `"off"`, `"straight"` (every line placed at once, each on a strong edge with every gap near the cell size, instead of one after another), or `"bend"` (lines placed in each row and column of cells on their own, for a grid the generator warped). The page draws bent lines bent. -- **Keep outlines** when shrinking (`--shrink-to`, **Size**) carries outlines over as strokes: thinned back to one pixel without breaking, one-pixel steps turned into diagonals. A one-pixel ring shrunk from 16 to 11 pixels stays whole, where it used to lose its sides. - -Breaking: a recipe's `follow` is `"off"`, `"straight"`, or `"bend"`, no longer `true` or `false`, and `--follow` takes one of them. Replace `"follow": true` with `"follow": "straight"` (`--follow straight`), and `"follow": false` with `"follow": "off"` (or leave it out). diff --git a/.changeset/pixel-review-fixes.md b/.changeset/pixel-review-fixes.md deleted file mode 100644 index 7c085ee9..00000000 --- a/.changeset/pixel-review-fixes.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@typeonce/ritmo-tools": patch ---- - -`pixel review` and `pixel notes` fixes, from trying them on real games: - -- The page keeps saving after the agent renames or replaces art it holds a review of (`pixel fix` does, putting `X.fixed.png` in place of `X.png`). Saves used to fail until a reload, which lost what was unsaved. The old review stays in `pixel.review.json` under its old name, and `pixel notes` lists it under `gone`. -- A page that has not heard an asset came back can no longer write over that asset's review. -- A comment being typed survives a click on a status or a reply from the agent, and is added when the person moves to another asset, animation, or view. Cancel or Esc drops it. -- `pixel notes` no longer says "Leave it as it is" about an approved asset with open comments. An asset with a note or comments but no status comes ahead of untouched ones, and the summary counts it apart. -- The server answers while it reads a large folder, and the page says how far it is. Past 500 assets, `pixel review` and `pixel notes` warn to name narrower folders. The page gets a picture's file as it is, rather than a copy written for it up front, so a large folder takes about a third less time and far less memory. -- Holding an arrow key down in Safari no longer stops the page moving through assets. diff --git a/.changeset/pixel-review.md b/.changeset/pixel-review.md deleted file mode 100644 index 7118f5f3..00000000 --- a/.changeset/pixel-review.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@typeonce/ritmo-tools": minor -"@typeonce/ritmo": patch ---- - -`ritmo-tools pixel`: review all of a game's pixel art with a person on one page, and read back what they said. - -- **`pixel review `** serves a local page (port 5193) with every PNG and text sprite under the folders named, by folder, each with its status. A person looks at each asset at whole-number zooms, opening as large as fits with 1× a key away, over a dark, light, checkerboard, or custom backdrop, plays sheets by animation with their durations, sees a tile repeated, lines the whole set up at one scale on a grid by size (picking several to compare or approve at once), approves or asks for changes, and drags over the art to pin a comment to an area of pixels on one frame. Each asset's lint against its nearest `pixel.config.json` shows beside it. What the tools write beside the art (previews, project PNGs, onion pictures, scaled copies, frame folders) is left out, and a fix shows as one row with what it was made from. -- **`pixel notes `** prints a `summary` of what is left, then each asset's status, note, open comments (numbered, with their area and frame), how many are resolved, its lint, and `next`, changes asked first. **`pixel resolve --reply "..."`** marks comments done; the open page shows them with the reply. -- **Approval keeps the pixels the person saw**: a hash of each frame as decoded, and its duration. A re-export that looks the same is no change, and neither is a color hidden under a transparent pixel. When an approved asset later looks different or plays at other timings, `pixel notes` and the page say which frames under `changedSinceApproved`, and the page offers **Approve it as it is now**. -- The review lives in one `pixel.review.json` beside the game's `package.json`, keyed by path, out of the folder that ships. -- `review`, `notes`, and `resolve` exit 0 or 2 only. -- `pixel lint` no longer runs out of memory on a picture of thousands of colors (a smooth app icon took gigabytes comparing every pair): past 512 colors it says the picture is not pixel art yet instead. On the review page and in `pixel notes`, a sheet frame with nothing drawn in it is an `empty-frame` error. - -The `ritmo-pixel-art` skill says when to open the review and how to act on it, and that approving on the page does not replace looking at the art in the game at display size. diff --git a/.changeset/prototype-where-fun-lives.md b/.changeset/prototype-where-fun-lives.md deleted file mode 100644 index 0158e5b0..00000000 --- a/.changeset/prototype-where-fun-lives.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@typeonce/ritmo": patch ---- - -**The `ritmo-prototype` skill prototypes where the fun lives.** It used to start from a verb or the smallest goal, with one exception for story. Now it names five cases. For a verb, build the verb and its feedback. For goals, build the smallest goal. For a story, characters, or a mystery, write a small slice by hand; paper or text is fine when it is faster. For a place to explore, hand-build a small map with a landmark you can see before you can reach it and something off the obvious path. For a mood, bring in the look and sound early. Before this, a game about wandering and discovery got a one-screen loop as its prototype, which tests none of what makes it fun. diff --git a/.changeset/query-entities-and-data.md b/.changeset/query-entities-and-data.md deleted file mode 100644 index f5ee392c..00000000 --- a/.changeset/query-entities-and-data.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@typeonce/ritmo": minor -"@typeonce/ritmo-render": patch -"@typeonce/oxlint-plugin-ritmo": patch ---- - -**Breaking: a query's `each()` returns entities, and values are read through `queries.x.data`. Matches, their `data` cells, and `entity.proof` are gone.** Iterating no longer keeps an object per entity and query, so loops over many entities run up to 3x faster, a game-shaped frame about 2x faster, and memory no longer grows with each query. - -```diff --for (const { data } of queries.movers.each()) { -- const velocity = data.velocity.get() -- data.position.update((position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) --} -+const { data } = queries.movers -+for (const entity of queries.movers.each()) { -+ const velocity = data.velocity.get(entity) -+ data.position.update(entity, (position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) -+} -``` - -Every slot method takes the entity first. The fix for each old form: - -| Before | After | -|---|---| -| `match.data.x.get()` | `data.x.get(entity)` | -| `data.x.set(value)`, `update(f)` | `data.x.set(entity, value)`, `update(entity, f)` | -| `data.x.setRaw(raw)`, `updateRaw(f)` | `data.x.setRaw(entity, raw)`, `updateRaw(entity, f)` | -| `data.state.transition({ from, to })` | `data.state.transition(entity, { from, to })` | -| `data.state.update({ state, data })` | `data.state.update(entity, { state, data })` | -| `data.sprite?.get()` | `data.sprite.get(entity)`, `undefined` when it lacks the component | -| `data.note !== undefined` | `data.note.has(entity)` | -| `commands.despawn(match.entity)` | `commands.despawn(entity)` | -| `match.entity.id` | `entity.id` | -| `found.value.data.x.get()` after `queries.q.get(id)` | `queries.q.data.x.get(found.value)` | -| `QueryMatch` | `QueryEntity`, with `QueryData` for the slots | -| `Query.Cells` | `QueryData` | -| `WriteCellForDescriptor` | `WriteSlotOf` | -| `match.entity.proof.position` | `data.position.get(entity)` | - -`single()`, `singleOptional()`, `get`, `getHandle`, `children`, and `descendants` return entities too. `ReadCell` and `WriteCell` remain for resources. - -`Entity.EntityRef` now takes ``. `Entity.EntityMut`, `Entity.ref`, and `Entity.mut` are removed: only a query produces an entity. - -The type checks are the same or stricter: a read slot has no `set`, a slot rejects an id or an entity whose query does not prove its component, and entities carry their proofs to `commands`. - -**Kept values changed.** `queries.x.data` kept past its run throws, as a kept match did. A kept entity no longer throws by itself: it keeps naming its entity, reading its current value, and throws once the entity no longer holds the component. It never reads another entity, since ids are never reused, but it does not re-check the query, so an entity that left through `without` stays readable. To follow an entity across runs and re-check the query, keep `entity.id` and resolve it with `queries.x.get(id)`, as before. - -`RenderSync` callbacks are unchanged: `context.data.x.get()` still works. Lint messages name the new forms. diff --git a/packages/browser/CHANGELOG.md b/packages/browser/CHANGELOG.md index 30334485..2dd55fc4 100644 --- a/packages/browser/CHANGELOG.md +++ b/packages/browser/CHANGELOG.md @@ -1,5 +1,23 @@ # @typeonce/ritmo-browser +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- Updated dependencies [50597a8] +- Updated dependencies [15e5aa9] +- Updated dependencies [6b7df96] +- Updated dependencies [d1d864d] +- Updated dependencies [f30c9c6] +- Updated dependencies [d989c0b] +- Updated dependencies [027634b] +- Updated dependencies [87466e9] + - @typeonce/ritmo@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/browser/package.json b/packages/browser/package.json index d5cc1a75..44f78377 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-browser", - "version": "0.20.0", + "version": "0.21.0", "description": "Browser fixed-step loop, keyboard, gamepad, pointer, and touch actions, merged input, local multiplayer, and input capture for Ritmo", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 40ddd508..0b21ca7a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,105 @@ # @typeonce/ritmo +## 0.21.0 + +### Minor Changes + +- 50597a8: **Breaking (debug runtimes only): a runtime made with `debug` freezes every value it is handed, so mutating one afterwards throws where it mutates.** Values were stored by reference: an object passed to `Game.Command.spawn`, `commands.insert`, a cell's `set`/`update`, a resource write, `emit`, `runtime.send`, a next-state `set`, or `Game.Runtime.make` stayed connected to the code that made it. Mutating it later changed the world with no write, so `changed` readers missed it, and a `rewind` brought back the mutated value instead of the checkpointed one. A debug runtime now deep-freezes each value as it is queued or written, and the mutation throws a `TypeError`. Typed arrays are skipped, `Map` and `Set` contents stay writable, and runtimes without `debug` behave as before. + + If your game's tests or devtools sessions now throw, build a new value instead of mutating the one you handed over: + + ```diff + -const body = { x: 1, trail: [] } + -commands.spawn(Game.Command.spawn([Body, body])) + -body.x += 1 + +commands.spawn(Game.Command.spawn([Body, { x: 1, trail: [] }])) + +// later, in a system + +data.body.update((body) => ({ ...body, x: body.x + 1 })) + ``` + + `Runtime.tick` now points to `FixedLoop.start` for ticking at a fixed rate from a frame loop. The `ritmo` skill now treats a variable a system closes over like a module variable: host input reaches systems through a resource, a sent event, or an input source service. +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. +- 87466e9: **Breaking: a query's `each()` returns entities, and values are read through `queries.x.data`. Matches, their `data` cells, and `entity.proof` are gone.** Iterating no longer keeps an object per entity and query, so loops over many entities run up to 3x faster, a game-shaped frame about 2x faster, and memory no longer grows with each query. + + ```diff + -for (const { data } of queries.movers.each()) { + - const velocity = data.velocity.get() + - data.position.update((position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + -} + +const { data } = queries.movers + +for (const entity of queries.movers.each()) { + + const velocity = data.velocity.get(entity) + + data.position.update(entity, (position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + +} + ``` + + Every slot method takes the entity first. The fix for each old form: + + | Before | After | + |---|---| + | `match.data.x.get()` | `data.x.get(entity)` | + | `data.x.set(value)`, `update(f)` | `data.x.set(entity, value)`, `update(entity, f)` | + | `data.x.setRaw(raw)`, `updateRaw(f)` | `data.x.setRaw(entity, raw)`, `updateRaw(entity, f)` | + | `data.state.transition({ from, to })` | `data.state.transition(entity, { from, to })` | + | `data.state.update({ state, data })` | `data.state.update(entity, { state, data })` | + | `data.sprite?.get()` | `data.sprite.get(entity)`, `undefined` when it lacks the component | + | `data.note !== undefined` | `data.note.has(entity)` | + | `commands.despawn(match.entity)` | `commands.despawn(entity)` | + | `match.entity.id` | `entity.id` | + | `found.value.data.x.get()` after `queries.q.get(id)` | `queries.q.data.x.get(found.value)` | + | `QueryMatch` | `QueryEntity`, with `QueryData` for the slots | + | `Query.Cells` | `QueryData` | + | `WriteCellForDescriptor` | `WriteSlotOf` | + | `match.entity.proof.position` | `data.position.get(entity)` | + + `single()`, `singleOptional()`, `get`, `getHandle`, `children`, and `descendants` return entities too. `ReadCell` and `WriteCell` remain for resources. + + `Entity.EntityRef` now takes ``. `Entity.EntityMut`, `Entity.ref`, and `Entity.mut` are removed: only a query produces an entity. + + The type checks are the same or stricter: a read slot has no `set`, a slot rejects an id or an entity whose query does not prove its component, and entities carry their proofs to `commands`. + + **Kept values changed.** `queries.x.data` kept past its run throws, as a kept match did. A kept entity no longer throws by itself: it keeps naming its entity, reading its current value, and throws once the entity no longer holds the component. It never reads another entity, since ids are never reused, but it does not re-check the query, so an entity that left through `without` stays readable. To follow an entity across runs and re-check the query, keep `entity.id` and resolve it with `queries.x.get(id)`, as before. + + `RenderSync` callbacks are unchanged: `context.data.x.get()` still works. Lint messages name the new forms. + +### Patch Changes + +- 15e5aa9: `ritmo-tools fx`: judge effects at native size, and see when an approved effect has changed. Ideas from [PVFX Foundry](https://nerijs.itch.io/pvfx-foundry). + + - **1× zoom** on the review page, to judge whether an effect reads at its native size. + - **Approval keeps what the person saw.** Approving an effect saves the draft in use, its fps, and a hash of each frame's final colors. When any of them later differ, `fx notes` and `fx render` list `changedSinceApproved` (`"frames 3-5 look different"`), and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. A palette edit counts as a change. A review saved before this has no hashes, and reports nothing until the effect is approved again. + - The `ritmo-fx` skill adds a craft rule (each material dies its own way), the native-size question for the person, what to do with `changedSinceApproved`, and PVFX Foundry's Strict tier as a reference to look at. + - The example `explosion` draft `a` now sits on the floor, with thrown debris, embers, dust, and smoke that comes apart in clumps. + + Breaking: `fx render` writes sprite sheets in rows of up to 8 frames, not one row, so a long effect stays within a device's texture limit. A game that reads each frame's rectangle from the JSON (`frames["hit 0"].frame`, as Pixi and Phaser do) needs no change. Replace code that computes a frame's place as `x = frame * width` with `x = (frame % 8) * width` and `y = Math.floor(frame / 8) * height`. +- 6b7df96: `ritmo-tools music` and `sound`: review all of a game's music on one page, and see when something approved has changed. + + - **`music review` takes songs, audio files, and folders**, and shows them all on a list with each one's status, filtered by what is left to do. A folder is searched for `*.song.json` and audio files, leaving out what the tool wrote beside a song (its render, its stems, its hum). What the person has not saved yet is written before another piece opens. + - **Plain audio files can be reviewed** (`.wav`, `.flac`, `.mp3`, `.ogg`, `.m4a`): play, loop, hear the loop point, read the length, loudness, and peak, approve or ask for changes, and comment on a stretch of time. Their reviews live in one `music.review.json` beside the game's `package.json`, not beside the files, since those often sit in a folder that ships with the game. + - **Approval keeps what the person heard**, in `music` and `sound` as it already did in `fx`. When an approved song, sound, or file later differs, `music notes`, `music render`, `sound notes`, and `sound render` list `changedSinceApproved`, and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. An edit to words alone (an `about`, a track's name) is no change. A review from before this keeps its approval, with nothing to compare against until it is approved again. + - The music page checks a song in a worker, so a long song no longer holds the page for seconds when it opens, and going back to a song is instant. + + Breaking: `music notes` now prints `{ summary, music: [...] }` for any paths, one entry per piece, with `file`, `kind`, and `review` beside what it printed before. Replace `.status`, `.note`, `.comments`, `.edits`, and `.next` with `.music[0].status` and so on. `music review` prints `music` (the list of what it found) in place of `song` and `review`. +- f30c9c6: `ritmo-tools pixel`: hold a frame, see an animation as a still, and check a tile's edges. Ideas from [Pixelorama](https://github.com/Orama-Interactive/Pixelorama). + + - **Holds.** `pixel sheet --animations "attack=5,6x3,7,8"` shows frame 6 for 3 beats, a beat being one frame at `--fps`. The JSON's `duration`, the GIF, the recipe (`sheet.frames[i].beats`), and the page's player all follow it, the page sets it on the chosen frame, and the report lists each frame's time in `durations`. + - **Onion pictures.** `pixel sheet` writes `..onion.png`: a panel per step of playback, each frame over the one before in red and the one after in blue, so an agent that cannot watch an animation can see a jump, a drift, or a change of size. + - **Tile seams.** `pixel lint tile.png --tile both` (or `x`, `y`) warns of a `seam` when a tile's edges do not meet, and `pixel preview tile.png --tile both` writes it repeated. + - The `ritmo-pixel-art` skill says when to use each, and that a game plays each frame for its `duration`. + + Breaking: the sprite sheet JSON of `pixel sheet` and `fx render` no longer has `meta.fps`, since one rate would lose a frame's hold. Replace `meta.fps[name]` with each frame's time, `frames[" 0"].duration`, in milliseconds. With Pixi, give `AnimatedSprite` `{ texture, time }` objects built from those durations. +- d989c0b: `ritmo-tools pixel`: review all of a game's pixel art with a person on one page, and read back what they said. + + - **`pixel review `** serves a local page (port 5193) with every PNG and text sprite under the folders named, by folder, each with its status. A person looks at each asset at whole-number zooms, opening as large as fits with 1× a key away, over a dark, light, checkerboard, or custom backdrop, plays sheets by animation with their durations, sees a tile repeated, lines the whole set up at one scale on a grid by size (picking several to compare or approve at once), approves or asks for changes, and drags over the art to pin a comment to an area of pixels on one frame. Each asset's lint against its nearest `pixel.config.json` shows beside it. What the tools write beside the art (previews, project PNGs, onion pictures, scaled copies, frame folders) is left out, and a fix shows as one row with what it was made from. + - **`pixel notes `** prints a `summary` of what is left, then each asset's status, note, open comments (numbered, with their area and frame), how many are resolved, its lint, and `next`, changes asked first. **`pixel resolve --reply "..."`** marks comments done; the open page shows them with the reply. + - **Approval keeps the pixels the person saw**: a hash of each frame as decoded, and its duration. A re-export that looks the same is no change, and neither is a color hidden under a transparent pixel. When an approved asset later looks different or plays at other timings, `pixel notes` and the page say which frames under `changedSinceApproved`, and the page offers **Approve it as it is now**. + - The review lives in one `pixel.review.json` beside the game's `package.json`, keyed by path, out of the folder that ships. + - `review`, `notes`, and `resolve` exit 0 or 2 only. + - `pixel lint` no longer runs out of memory on a picture of thousands of colors (a smooth app icon took gigabytes comparing every pair): past 512 colors it says the picture is not pixel art yet instead. On the review page and in `pixel notes`, a sheet frame with nothing drawn in it is an `empty-frame` error. + + The `ritmo-pixel-art` skill says when to open the review and how to act on it, and that approving on the page does not replace looking at the art in the game at display size. +- 027634b: **The `ritmo-prototype` skill prototypes where the fun lives.** It used to start from a verb or the smallest goal, with one exception for story. Now it names five cases. For a verb, build the verb and its feedback. For goals, build the smallest goal. For a story, characters, or a mystery, write a small slice by hand; paper or text is fine when it is faster. For a place to explore, hand-build a small map with a landmark you can see before you can reach it and something off the obvious path. For a mood, bring in the look and sound early. Before this, a game about wandering and discovery got a one-screen loop as its prototype, which tests none of what makes it fun. + ## 0.20.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 77f9a6c1..78dead6f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo", - "version": "0.20.0", + "version": "0.21.0", "description": "The agent-first, type-safe ECS runtime for TypeScript", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/devtools/CHANGELOG.md b/packages/devtools/CHANGELOG.md index 42aa37fd..d4e6af56 100644 --- a/packages/devtools/CHANGELOG.md +++ b/packages/devtools/CHANGELOG.md @@ -1,5 +1,24 @@ # @typeonce/ritmo-devtools +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- Updated dependencies [50597a8] +- Updated dependencies [15e5aa9] +- Updated dependencies [6b7df96] +- Updated dependencies [d1d864d] +- Updated dependencies [f30c9c6] +- Updated dependencies [d989c0b] +- Updated dependencies [027634b] +- Updated dependencies [87466e9] + - @typeonce/ritmo@0.21.0 + - @typeonce/ritmo-netcode@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 7e461911..93a5cc87 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-devtools", - "version": "0.20.0", + "version": "0.21.0", "description": "Headless debug sessions, trace history, text reports, replays, and fuzzing for Ritmo runtimes", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/devtools/src/internal/version.ts b/packages/devtools/src/internal/version.ts index f9d382eb..ca5cd40c 100644 --- a/packages/devtools/src/internal/version.ts +++ b/packages/devtools/src/internal/version.ts @@ -3,4 +3,4 @@ * `package.json` by `scripts/sync-version.ts` (run by \`pnpm version-packages\`) * and checked by a test. */ -export const libraryVersion = "0.20.0" +export const libraryVersion = "0.21.0" diff --git a/packages/math/CHANGELOG.md b/packages/math/CHANGELOG.md index 7ca0e75a..add05b42 100644 --- a/packages/math/CHANGELOG.md +++ b/packages/math/CHANGELOG.md @@ -1,5 +1,23 @@ # @typeonce/ritmo-math +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- Updated dependencies [50597a8] +- Updated dependencies [15e5aa9] +- Updated dependencies [6b7df96] +- Updated dependencies [d1d864d] +- Updated dependencies [f30c9c6] +- Updated dependencies [d989c0b] +- Updated dependencies [027634b] +- Updated dependencies [87466e9] + - @typeonce/ritmo@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/math/package.json b/packages/math/package.json index ce8a49ed..ae6c8571 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-math", - "version": "0.20.0", + "version": "0.21.0", "description": "The 2D helpers most Ritmo games would otherwise rewrite (vectors, angles, swept collision, flow field), plain data in and out, built on Ops so they replay the same on every engine", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/netcode/CHANGELOG.md b/packages/netcode/CHANGELOG.md index 74725f4f..33040c7d 100644 --- a/packages/netcode/CHANGELOG.md +++ b/packages/netcode/CHANGELOG.md @@ -1,5 +1,23 @@ # @typeonce/ritmo-netcode +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- Updated dependencies [50597a8] +- Updated dependencies [15e5aa9] +- Updated dependencies [6b7df96] +- Updated dependencies [d1d864d] +- Updated dependencies [f30c9c6] +- Updated dependencies [d989c0b] +- Updated dependencies [027634b] +- Updated dependencies [87466e9] + - @typeonce/ritmo@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/netcode/package.json b/packages/netcode/package.json index f20a542d..3d188b3c 100644 --- a/packages/netcode/package.json +++ b/packages/netcode/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-netcode", - "version": "0.20.0", + "version": "0.21.0", "description": "Rollback and lockstep sessions over any transport for Ritmo: input timelines, checkpoint history, pacing, resync, and desync checks", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/oxlint-plugin/CHANGELOG.md b/packages/oxlint-plugin/CHANGELOG.md index ad009b81..53d36b15 100644 --- a/packages/oxlint-plugin/CHANGELOG.md +++ b/packages/oxlint-plugin/CHANGELOG.md @@ -1,5 +1,56 @@ # @typeonce/oxlint-plugin-ritmo +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- 87466e9: **Breaking: a query's `each()` returns entities, and values are read through `queries.x.data`. Matches, their `data` cells, and `entity.proof` are gone.** Iterating no longer keeps an object per entity and query, so loops over many entities run up to 3x faster, a game-shaped frame about 2x faster, and memory no longer grows with each query. + + ```diff + -for (const { data } of queries.movers.each()) { + - const velocity = data.velocity.get() + - data.position.update((position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + -} + +const { data } = queries.movers + +for (const entity of queries.movers.each()) { + + const velocity = data.velocity.get(entity) + + data.position.update(entity, (position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + +} + ``` + + Every slot method takes the entity first. The fix for each old form: + + | Before | After | + |---|---| + | `match.data.x.get()` | `data.x.get(entity)` | + | `data.x.set(value)`, `update(f)` | `data.x.set(entity, value)`, `update(entity, f)` | + | `data.x.setRaw(raw)`, `updateRaw(f)` | `data.x.setRaw(entity, raw)`, `updateRaw(entity, f)` | + | `data.state.transition({ from, to })` | `data.state.transition(entity, { from, to })` | + | `data.state.update({ state, data })` | `data.state.update(entity, { state, data })` | + | `data.sprite?.get()` | `data.sprite.get(entity)`, `undefined` when it lacks the component | + | `data.note !== undefined` | `data.note.has(entity)` | + | `commands.despawn(match.entity)` | `commands.despawn(entity)` | + | `match.entity.id` | `entity.id` | + | `found.value.data.x.get()` after `queries.q.get(id)` | `queries.q.data.x.get(found.value)` | + | `QueryMatch` | `QueryEntity`, with `QueryData` for the slots | + | `Query.Cells` | `QueryData` | + | `WriteCellForDescriptor` | `WriteSlotOf` | + | `match.entity.proof.position` | `data.position.get(entity)` | + + `single()`, `singleOptional()`, `get`, `getHandle`, `children`, and `descendants` return entities too. `ReadCell` and `WriteCell` remain for resources. + + `Entity.EntityRef` now takes ``. `Entity.EntityMut`, `Entity.ref`, and `Entity.mut` are removed: only a query produces an entity. + + The type checks are the same or stricter: a read slot has no `set`, a slot rejects an id or an entity whose query does not prove its component, and entities carry their proofs to `commands`. + + **Kept values changed.** `queries.x.data` kept past its run throws, as a kept match did. A kept entity no longer throws by itself: it keeps naming its entity, reading its current value, and throws once the entity no longer holds the component. It never reads another entity, since ids are never reused, but it does not re-check the query, so an entity that left through `without` stays readable. To follow an entity across runs and re-check the query, keep `entity.id` and resolve it with `queries.x.get(id)`, as before. + + `RenderSync` callbacks are unchanged: `context.data.x.get()` still works. Lint messages name the new forms. + ## 0.20.0 ## 0.19.1 diff --git a/packages/oxlint-plugin/package.json b/packages/oxlint-plugin/package.json index 2cca9006..465d8c09 100644 --- a/packages/oxlint-plugin/package.json +++ b/packages/oxlint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/oxlint-plugin-ritmo", - "version": "0.20.0", + "version": "0.21.0", "description": "Oxlint rules for Ritmo games: deterministic, headless simulation code and checked runtime results", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/pixi/CHANGELOG.md b/packages/pixi/CHANGELOG.md index 3f7bc135..14bdc930 100644 --- a/packages/pixi/CHANGELOG.md +++ b/packages/pixi/CHANGELOG.md @@ -1,5 +1,17 @@ # @typeonce/ritmo-pixi +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- Updated dependencies [d1d864d] +- Updated dependencies [87466e9] + - @typeonce/ritmo-render@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/pixi/package.json b/packages/pixi/package.json index f3b18225..6b7562ca 100644 --- a/packages/pixi/package.json +++ b/packages/pixi/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-pixi", - "version": "0.20.0", + "version": "0.21.0", "description": "Pixi container adapter for Ritmo render-node registries", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/render/CHANGELOG.md b/packages/render/CHANGELOG.md index 9ca868d8..4c4688ed 100644 --- a/packages/render/CHANGELOG.md +++ b/packages/render/CHANGELOG.md @@ -1,5 +1,65 @@ # @typeonce/ritmo-render +## 0.21.0 + +### Minor Changes + +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. + +### Patch Changes + +- 87466e9: **Breaking: a query's `each()` returns entities, and values are read through `queries.x.data`. Matches, their `data` cells, and `entity.proof` are gone.** Iterating no longer keeps an object per entity and query, so loops over many entities run up to 3x faster, a game-shaped frame about 2x faster, and memory no longer grows with each query. + + ```diff + -for (const { data } of queries.movers.each()) { + - const velocity = data.velocity.get() + - data.position.update((position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + -} + +const { data } = queries.movers + +for (const entity of queries.movers.each()) { + + const velocity = data.velocity.get(entity) + + data.position.update(entity, (position) => ({ x: position.x + velocity.x, y: position.y + velocity.y })) + +} + ``` + + Every slot method takes the entity first. The fix for each old form: + + | Before | After | + |---|---| + | `match.data.x.get()` | `data.x.get(entity)` | + | `data.x.set(value)`, `update(f)` | `data.x.set(entity, value)`, `update(entity, f)` | + | `data.x.setRaw(raw)`, `updateRaw(f)` | `data.x.setRaw(entity, raw)`, `updateRaw(entity, f)` | + | `data.state.transition({ from, to })` | `data.state.transition(entity, { from, to })` | + | `data.state.update({ state, data })` | `data.state.update(entity, { state, data })` | + | `data.sprite?.get()` | `data.sprite.get(entity)`, `undefined` when it lacks the component | + | `data.note !== undefined` | `data.note.has(entity)` | + | `commands.despawn(match.entity)` | `commands.despawn(entity)` | + | `match.entity.id` | `entity.id` | + | `found.value.data.x.get()` after `queries.q.get(id)` | `queries.q.data.x.get(found.value)` | + | `QueryMatch` | `QueryEntity`, with `QueryData` for the slots | + | `Query.Cells` | `QueryData` | + | `WriteCellForDescriptor` | `WriteSlotOf` | + | `match.entity.proof.position` | `data.position.get(entity)` | + + `single()`, `singleOptional()`, `get`, `getHandle`, `children`, and `descendants` return entities too. `ReadCell` and `WriteCell` remain for resources. + + `Entity.EntityRef` now takes ``. `Entity.EntityMut`, `Entity.ref`, and `Entity.mut` are removed: only a query produces an entity. + + The type checks are the same or stricter: a read slot has no `set`, a slot rejects an id or an entity whose query does not prove its component, and entities carry their proofs to `commands`. + + **Kept values changed.** `queries.x.data` kept past its run throws, as a kept match did. A kept entity no longer throws by itself: it keeps naming its entity, reading its current value, and throws once the entity no longer holds the component. It never reads another entity, since ids are never reused, but it does not re-check the query, so an entity that left through `without` stays readable. To follow an entity across runs and re-check the query, keep `entity.id` and resolve it with `queries.x.get(id)`, as before. + + `RenderSync` callbacks are unchanged: `context.data.x.get()` still works. Lint messages name the new forms. +- Updated dependencies [50597a8] +- Updated dependencies [15e5aa9] +- Updated dependencies [6b7df96] +- Updated dependencies [d1d864d] +- Updated dependencies [f30c9c6] +- Updated dependencies [d989c0b] +- Updated dependencies [027634b] +- Updated dependencies [87466e9] + - @typeonce/ritmo@0.21.0 + ## 0.20.0 ### Patch Changes diff --git a/packages/render/package.json b/packages/render/package.json index c92cf67e..b315bcba 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-render", - "version": "0.20.0", + "version": "0.21.0", "description": "Renderer-agnostic entity-to-node mirroring for Ritmo", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/tools/CHANGELOG.md b/packages/tools/CHANGELOG.md index f7389ed0..7ba1e5cb 100644 --- a/packages/tools/CHANGELOG.md +++ b/packages/tools/CHANGELOG.md @@ -1,5 +1,64 @@ # @typeonce/ritmo-tools +## 0.21.0 + +### Minor Changes + +- 15e5aa9: `ritmo-tools fx`: judge effects at native size, and see when an approved effect has changed. Ideas from [PVFX Foundry](https://nerijs.itch.io/pvfx-foundry). + + - **1× zoom** on the review page, to judge whether an effect reads at its native size. + - **Approval keeps what the person saw.** Approving an effect saves the draft in use, its fps, and a hash of each frame's final colors. When any of them later differ, `fx notes` and `fx render` list `changedSinceApproved` (`"frames 3-5 look different"`), and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. A palette edit counts as a change. A review saved before this has no hashes, and reports nothing until the effect is approved again. + - The `ritmo-fx` skill adds a craft rule (each material dies its own way), the native-size question for the person, what to do with `changedSinceApproved`, and PVFX Foundry's Strict tier as a reference to look at. + - The example `explosion` draft `a` now sits on the floor, with thrown debris, embers, dust, and smoke that comes apart in clumps. + + Breaking: `fx render` writes sprite sheets in rows of up to 8 frames, not one row, so a long effect stays within a device's texture limit. A game that reads each frame's rectangle from the JSON (`frames["hit 0"].frame`, as Pixi and Phaser do) needs no change. Replace code that computes a frame's place as `x = frame * width` with `x = (frame % 8) * width` and `y = Math.floor(frame / 8) * height`. +- 6b7df96: `ritmo-tools music` and `sound`: review all of a game's music on one page, and see when something approved has changed. + + - **`music review` takes songs, audio files, and folders**, and shows them all on a list with each one's status, filtered by what is left to do. A folder is searched for `*.song.json` and audio files, leaving out what the tool wrote beside a song (its render, its stems, its hum). What the person has not saved yet is written before another piece opens. + - **Plain audio files can be reviewed** (`.wav`, `.flac`, `.mp3`, `.ogg`, `.m4a`): play, loop, hear the loop point, read the length, loudness, and peak, approve or ask for changes, and comment on a stretch of time. Their reviews live in one `music.review.json` beside the game's `package.json`, not beside the files, since those often sit in a folder that ships with the game. + - **Approval keeps what the person heard**, in `music` and `sound` as it already did in `fx`. When an approved song, sound, or file later differs, `music notes`, `music render`, `sound notes`, and `sound render` list `changedSinceApproved`, and the page shows it with **Approve it as it is now**. The status stays approved: only the person changes it. An edit to words alone (an `about`, a track's name) is no change. A review from before this keeps its approval, with nothing to compare against until it is approved again. + - The music page checks a song in a worker, so a long song no longer holds the page for seconds when it opens, and going back to a song is instant. + + Breaking: `music notes` now prints `{ summary, music: [...] }` for any paths, one entry per piece, with `file`, `kind`, and `review` beside what it printed before. Replace `.status`, `.note`, `.comments`, `.edits`, and `.next` with `.music[0].status` and so on. `music review` prints `music` (the list of what it found) in place of `song` and `review`. +- d1d864d: **Breaking: the packages require Node 22 or newer (Node 20 reached end of life on 2026-04-30).** `engines.node` is `>=22` for `@typeonce/ritmo`, `-browser`, `-render`, `-pixi`, `-math`, and `-netcode`; `>=22.12` for `@typeonce/oxlint-plugin-ritmo` (oxlint's own floor); `>=22.15` for `@typeonce/ritmo-devtools` (unchanged); and `>=22.19` for `@typeonce/ritmo-tools` (what `@effect/platform-node` needs). On Node 20, upgrade Node: `nvm install 22` (or 24), or `"node": ">=22"` in your CI's setup step. +- f30c9c6: `ritmo-tools pixel`: hold a frame, see an animation as a still, and check a tile's edges. Ideas from [Pixelorama](https://github.com/Orama-Interactive/Pixelorama). + + - **Holds.** `pixel sheet --animations "attack=5,6x3,7,8"` shows frame 6 for 3 beats, a beat being one frame at `--fps`. The JSON's `duration`, the GIF, the recipe (`sheet.frames[i].beats`), and the page's player all follow it, the page sets it on the chosen frame, and the report lists each frame's time in `durations`. + - **Onion pictures.** `pixel sheet` writes `..onion.png`: a panel per step of playback, each frame over the one before in red and the one after in blue, so an agent that cannot watch an animation can see a jump, a drift, or a change of size. + - **Tile seams.** `pixel lint tile.png --tile both` (or `x`, `y`) warns of a `seam` when a tile's edges do not meet, and `pixel preview tile.png --tile both` writes it repeated. + - The `ritmo-pixel-art` skill says when to use each, and that a game plays each frame for its `duration`. + + Breaking: the sprite sheet JSON of `pixel sheet` and `fx render` no longer has `meta.fps`, since one rate would lose a frame's hold. Replace `meta.fps[name]` with each frame's time, `frames[" 0"].duration`, in milliseconds. With Pixi, give `AnimatedSprite` `{ texture, time }` objects built from those durations. +- 215ba15: `ritmo-tools pixel`: one-pixel outlines, bent grids, and a shrink that keeps strokes whole, after [ToPixel](https://github.com/daisuqe/ToPixel). + + - **Outlines** (`outlines` in a recipe, `--outlines auto|on|off`, **Outlines** on the page; `"auto"` by default): when the picture's dark strokes are about one cell wide, a stroke a grid line split between two pixels goes back to one, never breaking the outline or a corner, and followed lines keep off dark strokes. The report's `grid.outlines` says whether it ran. + - **Follow lines** is now a choice: `"off"`, `"straight"` (every line placed at once, each on a strong edge with every gap near the cell size, instead of one after another), or `"bend"` (lines placed in each row and column of cells on their own, for a grid the generator warped). The page draws bent lines bent. + - **Keep outlines** when shrinking (`--shrink-to`, **Size**) carries outlines over as strokes: thinned back to one pixel without breaking, one-pixel steps turned into diagonals. A one-pixel ring shrunk from 16 to 11 pixels stays whole, where it used to lose its sides. + + Breaking: a recipe's `follow` is `"off"`, `"straight"`, or `"bend"`, no longer `true` or `false`, and `--follow` takes one of them. Replace `"follow": true` with `"follow": "straight"` (`--follow straight`), and `"follow": false` with `"follow": "off"` (or leave it out). +- d989c0b: `ritmo-tools pixel`: review all of a game's pixel art with a person on one page, and read back what they said. + + - **`pixel review `** serves a local page (port 5193) with every PNG and text sprite under the folders named, by folder, each with its status. A person looks at each asset at whole-number zooms, opening as large as fits with 1× a key away, over a dark, light, checkerboard, or custom backdrop, plays sheets by animation with their durations, sees a tile repeated, lines the whole set up at one scale on a grid by size (picking several to compare or approve at once), approves or asks for changes, and drags over the art to pin a comment to an area of pixels on one frame. Each asset's lint against its nearest `pixel.config.json` shows beside it. What the tools write beside the art (previews, project PNGs, onion pictures, scaled copies, frame folders) is left out, and a fix shows as one row with what it was made from. + - **`pixel notes `** prints a `summary` of what is left, then each asset's status, note, open comments (numbered, with their area and frame), how many are resolved, its lint, and `next`, changes asked first. **`pixel resolve --reply "..."`** marks comments done; the open page shows them with the reply. + - **Approval keeps the pixels the person saw**: a hash of each frame as decoded, and its duration. A re-export that looks the same is no change, and neither is a color hidden under a transparent pixel. When an approved asset later looks different or plays at other timings, `pixel notes` and the page say which frames under `changedSinceApproved`, and the page offers **Approve it as it is now**. + - The review lives in one `pixel.review.json` beside the game's `package.json`, keyed by path, out of the folder that ships. + - `review`, `notes`, and `resolve` exit 0 or 2 only. + - `pixel lint` no longer runs out of memory on a picture of thousands of colors (a smooth app icon took gigabytes comparing every pair): past 512 colors it says the picture is not pixel art yet instead. On the review page and in `pixel notes`, a sheet frame with nothing drawn in it is an `empty-frame` error. + + The `ritmo-pixel-art` skill says when to open the review and how to act on it, and that approving on the page does not replace looking at the art in the game at display size. + +### Patch Changes + +- ea008a4: `ritmo-tools` depends on Effect 4.1.1 (`effect`, `@effect/platform-node`, and `@effect/platform-node-shared`, still pinned exactly and together). +- f715940: `pixel review` and `pixel notes` fixes, from trying them on real games: + + - The page keeps saving after the agent renames or replaces art it holds a review of (`pixel fix` does, putting `X.fixed.png` in place of `X.png`). Saves used to fail until a reload, which lost what was unsaved. The old review stays in `pixel.review.json` under its old name, and `pixel notes` lists it under `gone`. + - A page that has not heard an asset came back can no longer write over that asset's review. + - A comment being typed survives a click on a status or a reply from the agent, and is added when the person moves to another asset, animation, or view. Cancel or Esc drops it. + - `pixel notes` no longer says "Leave it as it is" about an approved asset with open comments. An asset with a note or comments but no status comes ahead of untouched ones, and the summary counts it apart. + - The server answers while it reads a large folder, and the page says how far it is. Past 500 assets, `pixel review` and `pixel notes` warn to name narrower folders. The page gets a picture's file as it is, rather than a copy written for it up front, so a large folder takes about a third less time and far less memory. + - Holding an arrow key down in Safari no longer stops the page moving through assets. + ## 0.20.0 ### Minor Changes diff --git a/packages/tools/package.json b/packages/tools/package.json index 09c886ef..57033238 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@typeonce/ritmo-tools", - "version": "0.20.0", + "version": "0.21.0", "description": "Asset tools for games made with Ritmo, for agents: fixes AI-made pixel art and animation sheets onto a true pixel grid and a palette, keeps game sounds as JSON recipes, writes game music as JSON songs built around a hum, and draws pixel art effects as code, each with a review page for a person, pixel art included", "author": "Sandro Maglione", "license": "MIT", diff --git a/packages/tools/src/internal/version.ts b/packages/tools/src/internal/version.ts index b19e188e..cc1c79af 100644 --- a/packages/tools/src/internal/version.ts +++ b/packages/tools/src/internal/version.ts @@ -3,4 +3,4 @@ * `package.json` by `scripts/sync-version.ts` (run by `pnpm version-packages`) * and checked by a test. */ -export const toolsVersion = "0.20.0" +export const toolsVersion = "0.21.0"