Commit Graph
290 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 fb4d65a2ab Keep both sounds full at the middle of a Mix with a curve #331
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Successful in 16m2s
A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds
in an instrument now goes through a Mix (076c564), so the user asked
for a second curve where each side stays full up to the middle and
only fades on the other half. At 0.25 a is full and b half, at 0.5
both are full, at 0.75 a is half and b full.

- The Mix has a setting Curve with Crossfade (default) and Both at
  center. A plan and a chain only say `curve` where it is `both`, so
  old projects and their compiled plans stay as they were.
- A share is not a straight line of `mix` with that curve. Two ramps
  of their own would dip to half each when `mix` glides across the
  middle. So a ConstantSourceNode carries `mix` on the audio thread,
  and two WaveShaperNodes make the shares from it. The knob of such a
  Mix is that one source.
- The source is started with the voice and stopped when it is cut or
  ends by itself, so it does not run on after the voice.
- A sound FX node in an instrument keeps the curve of a Mix inside it.
- At 0.5 both sounds are added in full, up to +6 dB where both are
  loud. That is what several edges into the Output did before. The
  wiki says so.

Found on the way: the suite of 23bdc3f (task 529) failed the Step
check in e2e 34. It timed the jump from the start of the voice, but a
timed change runs from when it arrives. In CI that was 80 ms later,
and the pitch rightly held to 2.08 s.

- stats() says `tunedAt`, the second of the audio clock the last
  change moved a voice. A change that moves nothing, as On update
  sends every frame, leaves it.
- The Step check times its readings from there.

Tests:
- sound-library.test.ts: both shares shaped from one source, a glide
  moves only the source, the source stops with a cut and with an end
  by itself (50 tests, 2 new)
- soundfx.test.ts: curve only where set, one sound at 0 (41 tests)
- instrument.test.ts: the curve through a sound FX node (34 tests)
- Counter-checks: dropping the curve in throughFx and not stopping
  the source in done() each turn their new test red
- e2e 34 on melektaus-atx: ALL PASSED, 48 checks (+1). Rendered
  offline in Chrome, a and b come to 1/0, 1/0.5, 1/1, 0.5/1 and 0/1
  at mix 0 to 1, and a glide from 0 to 1 reads the same at 0.25, 0.5
  and 0.75 s. Step jumped 2.04 s after the change.
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3793 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:12:56 +02:00
adminandClaude Opus 5.5 076c564415 Take one sound at the Output of an instrument, mixed by a Mix #331
The Output summed any number of edges as layers. That made it the only
sound input of its kind, and it read as if a note could be several
voices. A note was always one voice, but the graph did not say so.
Now the Output takes one sound like every other sound input, and
several are put together with a Mix in front of it (decided by the
user after #331).

- The Output's input is no longer `many`. A second edge replaces the
  first, as at any data input.
- With only one drum sounding for a note, a Mix is its other side. A
  drum kit keeps the volume of each drum.
- An instrument saved with several edges into the Output keeps them
  but does not compile. The Output says "Takes one sound: put a Mix in
  front of it to play several". No saved project has one: the only
  instrument there (cbbf160bb7e8) has a single edge.
- An Output with nothing in it is a problem now, not silence.
- The compiler gathers only triggers from several edges. The player
  still takes a list of layers, which an instrument fills with one.

Tests:
- instrument.test.ts: drum kit through a Mix, a second edge replacing
  the first, an old Output with two edges, an empty Output (33 tests)
- Counter-check: `many` back on the Output turns 15 tests red, the
  new one about the second edge among them
- e2e 34 on melektaus-atx: ALL PASSED, 47 checks
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3789 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:59:52 +02:00
adminandClaude Opus 5.5 23bdc3f775 Play notes in time on the audio clock with a scheduler #332
game-maker / gate (push) Successful in 3m34s
game-maker / suite (push) Failing after 16m7s
Patterns and songs (#333, #334) have to play sample-accurate in time,
whatever the frame rate does. What is counted in game time runs away
from the sound. This lays the ground: musical time, a scheduler that
plans notes ahead on the audio clock, and a player that starts and ends
a note at a given second. Nothing in the editor plays a sequence yet,
the check scripts drive it through `__gm.music`.

- Ticks, 480 to the quarter as in MIDI files (decided in #332).
  Triplets and quintuplets land on whole ticks. Any meter, a beat being
  the note value under the line. The tempo always counts quarters.
- `core/music/sequence.ts` is a pure rule: the notes starting in a
  window of playing time. Across the end of a loop it looks into the
  next round, so the seam has no gap. A note past the loop end ends
  there. Windows are whole ticks, and the seam is stepped exactly, so
  no rounding can drop the first note of a round.
- `core/music/Scheduler.ts` plans the next 100 ms every 25 ms through a
  host, so it is tested with a clock of its own. The first note sounds
  50 ms after the start. Changes to the sequence are heard from the
  next window on. Stop and seek fade what it started. A new tempo or
  loop counts from what is planned, and the playhead keeps its old
  pace until the clock gets there.
- The position is the audio clock less `outputLatency`, as bar, beat
  and tick.
- The beat comes from a Web Worker (`viewport/musicTimer.ts`), which
  keeps its pace in a hidden tab.
- The sound library starts a note at `timing.at`: a buffer source by
  `start(when)`, the pitch worklet by counting down inside the render
  block it falls in. A note's end is set on the clock at once, its
  fade to the sample. Leaving a loop still waits for the page.
- Delay synced to beats (decided in #332, noted from #327): a setting
  `Sync` (Seconds / Beats), `time` then in quarter notes, held to the
  2 s of the line. Play sound and Play note have an input `tempo`,
  120 BPM where nothing is connected. A scheduled note carries the
  tempo of its sequence.
- Swing is left for #333/#334, as decided.

Also fixes the red suite of 6f1f315 (task 526). Three checks in e2e 34
waited on the page's clock for things that run on the audio clock. In
CI the voice was already 0.7 s old when first seen, and the 0.3 s note
of Play note was over. `stats().last` now says when a voice started on
the audio clock, and the Quad, Step and Play note checks hold every
reading against it.

Tests:
- music.test.ts (new): 17 tests, time, windows, loop seam, scheduler
  with a jittery timer, stop, seek, tempo change
- sound-library +4, soundfx +1, node-catalog +2 (tempo inputs)
- Counter-checks: an unclamped note length, a window that never moves
  on, a buffer source ignoring its start time and a Delay ignoring
  Beats each turn 1, 6, 1 and 2 tests red
- e2e 34 on melektaus-atx: ALL PASSED, 47 checks (+2). Nine notes of a
  looped bar at 150 BPM exactly 0.4 s apart, each planned 89 to 91 ms
  ahead. The position matched the clock less 32 ms latency to the
  tick. Offline the worklet started on samples 590, 12000 and 15200.
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3786 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:29:31 +02:00
adminandClaude Opus 5.5 144dee0a97 Let a looped sample play out past its loop when the note ends #331
With "Mode" and "Loop" as two fields, One-shot plus Loop ran until
Stop was pressed: letting go of the key did nothing. The two fields
are now one, "Play", with four entries that each say what happens
while the note is held and when it is let go.

- Gate and One-shot play the sound once, as before. Loop repeats it
  and fades out with the note.
- "Loop, then release" is new: the sampler's sustain loop. Let go,
  it finishes the round it is in and plays what comes after "Loop
  end" to the end of the sound.
- A buffer source stops looping and plays on by itself. The pitch
  worklet gets a "leave" message. The phase vocoder then lets the
  round finish that its newest frame has read into, so nothing read
  already changes under the frames still sounding.
- Samples saved with "Mode" and "Loop" are turned into "Play" on
  load. Gate and One-shot stay, either with Loop becomes Loop, since
  playing until stopped is gone.
- The keys of the Instrument tab stay half lit while a note let go
  is still heard. Esc on the keys stops everything, like Stop.
- Striking a ringing key again plays a new note beside it rather
  than stopping the old one, so a drum can be hit quickly. This
  differs from what was proposed in the issue discussion.

Tests:
- instrument + pitch-player + sound-library + node-catalog:
  477 tests, green (12 new).
- e2e 34 (melektaus-atx): ALL PASSED, 45 checks (+1). Let go after
  a loop of 0.05 s, the note played out for 0.31 s on the audio
  clock, the key half lit meanwhile.
- Counter-check: a release that does not leave the loop fails the
  2 new player tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:34:30 +02:00
adminandClaude Opus 5.5 8a4e4a6762 Play sounds as notes of an instrument asset #331
game-maker / suite (push) Failing after 2m40s
game-maker / gate (push) Successful in 3m14s
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.

- New asset type instrument, under the heading Music, not placeable.
  A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
  only, the lowest C-1. Notes has From and To, outputs any, note,
  frequency and velocity, and with Single notes a trigger per note,
  each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
  can take any number of edges now (PortDef.many): a trigger, and
  the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
  through the pitch worklet of #317, so it keeps its length. Gate
  fades it out at the end of the note, One-shot plays through. Loop
  runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
  default. Sound FX sends the sound through a sound FX asset, its
  parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
  only for the notes it stands for, a silent source silences what it
  alone feeds, a Mix with a silent side is the other side. velocity
  and the parameters stay open for the voice. Builds are cached per
  note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
  with its own file and volume. release fades the gated ways out,
  one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
  detached, and hands out voice for Set sound FX parameters and
  Stop sound. Velocity is always loudness. Instrument parameters
  picks values for the instrument's parameters like Sound FX
  parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
  128 keys below. Keys without sound are grey. A key plays while it
  is held. Focused, the computer keyboard plays as in a tracker by
  KeyboardEvent.code, the arrows move the octave.

Findings:
- A label of the keys set fill on the shared small text style and
  darkened every small label of the editor. Caught in a screenshot
  before the commit, the keys have a style of their own now.
- Suite task 523 on cf6799a failed two checks of e2e 34 that timed
  audio by the page's clock: a 2.95 s echo tail was gone after 1 s,
  and Quad in was further along than 0.7 s allow. Both time on the
  audio clock now through a new hook __gm.sound.clock().

Tests:
- instrument.test.ts (new, 22): notes, asset, palette, round trip,
  compile per note, drum kit, zones, Mix, velocity and parameters,
  Sound FX node with rename, problems, cache, Play note, the keys
- sound-library +7 (layers, release, duration, early release, loop
  region in buffer and worklet), pitch-player +2 (loop region),
  node-catalog +4, graph adjusted. 582 tests in the touched files.
- e2e 4 and 34 on melektaus-atx: ALL PASSED, 34 with 44 checks (+7)
- Gate on melektaus-atx: CHECK EXIT 0, 243 test files, 3750 tests
- Counter-checks: any always on, release ending one-shots, Sound FX
  inputs ignored, loop region ignored. 3, 2, 1 and 1 tests failed.
- Browser probe on stein2: keys, held key, Single notes names and
  the header looked at in screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:07:40 +02:00
adminandClaude Opus 5.5 cf6799a784 Let echoes ring on with a Delay node in sound FX #324 #327
game-maker / gate (push) Successful in 3m20s
game-maker / suite (push) Failing after 15m4s
A voice used to end the moment its sources did, and Stop sound cut it
dead. An echo has to outlive its sound, and a stop must not click.

- Delay node: a delay line with a damped feedback loop, added at mix
  next to the untouched sound. Time 0.01 to 2 s, feedback up to 0.95,
  damping 20 kHz to 500 Hz evenly in octaves, all fed by parameters
  and moved by Set sound FX parameters like any other number.
- Ping-pong is a setting of the node. It takes the sound into the left
  of the line and swaps the sides every round.
- A voice with an echo rings on for its tail: until the last echo is
  60 dB below the sound, from time, feedback and mix (fxTailOf). It
  stays reachable meanwhile, and a change counts the tail again.
- The end of a tail is timed on the audio clock with a silent
  ConstantSourceNode, so a paused run holds it. Then the voice is
  disconnected, since an echo is a loop of nodes.
- Stop sound fades a voice out in 10 ms instead of cutting it. Let
  ring fades only its sources and keeps the voice for its echoes.
  Where the context is suspended it cuts at once.
- Stop sound takes a voice. Connected, it stops only that play, and a
  0 from a Play sound that played nothing stops nothing.
- Play sound hands out tail: length plus the echoes at its start
  values.
- Destroying a behavior and ending a run still cut, echoes included.
- No limit on voices. The emit of a sound FX node now also gets the
  node's settings.

Finding: in Chrome every round of a feedback loop comes one render
quantum late (128 samples, 2.7 ms at 48 kHz), measured as 0, 128, 256,
384 for the first four echoes. Left so, since a browser without it
would then hear the echoes early. Written in the wiki.

Tests:
- sound-library, soundfx, node-catalog, sounds, keep-on-scene-load:
  489 tests green. New: fade on stop, cut while suspended, echo and
  ping-pong wiring, ringing until the tail, tail counted again, let
  ring, stop by voice, echo knobs, delay compile, tail sums.
- Counter-checks: no tail, a stop without fade, a connected voice 0
  read as unconnected. Each fails its tests, files back by cmp.
- e2e 34 (melektaus-atx): ALL PASSED, 37 checks. A blip through a
  ping-pong Delay (0.3 s, 0.5, 0.5) rang 2.97 s, 2.95 s worked out.
  Let ring kept the voice, a stop after it cut it. Rendered offline
  with the player's own stageNodes the echoes are 0.25 left, 0.125
  right, 0.063 left. 64 stereo voices of it took about 0.3 s for 2 s,
  so about 400 play at once.
- Gate (melektaus-atx): CHECK EXIT 0, 242 test files, 3713 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:05:05 +02:00
adminandClaude Opus 5.5 6e70421b92 Keep the length of a sound whatever its pitch does #317
game-maker / gate (push) Successful in 3m13s
game-maker / suite (push) Successful in 15m36s
A Pitch used to keep the length only for the pitch a voice started at.
Set sound FX parameters changed it by speed, so a sweep made the sound
longer or shorter, and length into time no longer spanned the sound.
Now a way with a Pitch is played by an AudioWorklet, a phase vocoder
whose pitch is an AudioParam in semitones. The length never changes,
and a pitch glides, ramps and eases like a Gain.

- PitchPlayer (core/soundfx) is the vocoder as a plain class, so it
  runs in a unit test. Frames of about 43 ms, four overlapping, a real
  FFT done as a complex one of half the size.
- Peaks are moved with the bins around them (Laroche and Dolson's
  phase locking), and frames are turned by half their size so a phase
  is the one at the middle. Without both, a tone beat against itself
  and dipped to 0.54 of its loudness while the pitch moved.
- It reads the sound itself rather than being fed it. It looks ahead
  as far as a frame reaches, so a voice starts with its first sample
  and every way of a Mix stays in step. A change of pitch is heard
  about half a frame (20 ms) late.
- At exactly 0 a frame is the sound itself and costs next to nothing.
  Coming back to 0, the phases are pulled round to the sound's for
  about half a second before it switches, so the switch does not dip.
- pitchWorklet (viewport) is the processor, bundled by Vite through
  ?worker&url. It is loaded with the first play that has a Pitch, which
  waits for it like for its bytes. A file's samples are sent with its
  first voice only and kept in the worklet's scope.
- A way has the worklet when its plan has a Pitch, a typed-in 0
  excepted (FxChain.shifted). A way without one stays a plain buffer
  source. Without AudioWorklet a Pitch is not heard, and the console
  says so.
- The pitched copies (WSOLA, pitchShift.ts) and their cache are gone.
- Decisions of the user: the player at the source rather than an
  effect in the chain, a phase vocoder rather than WSOLA, no limit on
  voices, no range tighter than the node's -64 to +64.
- stats() says whether the worklet is there, levels() gives a pitch in
  semitones, and __gm.sound.pitchWorkletUrl() lets a check run it.

Tests:
- pitch-player.test.ts (new, replaces pitch-shift.test.ts): 11 tests.
  Pitches from -30.5 to +12 within 2 %, the length to the sample, 0
  exact to 1e-5, a sweep half a frame behind, 8 kHz sounds, loops,
  ramps there and back without a dip.
- sound-library.test.ts: 23 tests (+4), soundfx.test.ts: 31 (+1).
- Counter-checks: no worklet in the player, shifted taken from the
  value, no pull back to 0. 15 tests fail, files restored by cmp.
- e2e 34 on melektaus-atx: ALL PASSED, 34 checks (+1). In real Chrome
  the worklet is there and the pitch param glides to 3.0007.
  Offline: 880.0 Hz for 440 an octave up, 48000 of 48000 samples, a
  sweep 0 to -12 at 392 then 248 Hz, still 48000 samples.
- Gate on melektaus-atx: 242 test files, 3690 tests, CHECK EXIT 0.
- Cost: 53 ms per second of a stereo voice at +5 in Node on stein2,
  5 ms at 0. In headless Chrome 16 voices for 2 s took 1.3 s, about 24
  fill the audio thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:00:13 +02:00
adminandClaude Opus 5.5 804a97b0a9 Hand out the length of a played sound from Play sound
game-maker / gate (push) Successful in 3m12s
game-maker / suite (push) Failing after 26m24s
Wired into the time of Set sound FX parameters, a change then spans
exactly one run of the clip, so a fade or a filter sweep ends with its
last sample.

- Play sound has a second output, length, in seconds. It is 0 where
  nothing was played or the sound has not loaded yet.
- The AudioSink gives the length of a file, the SoundLibrary takes it
  from the decoded buffer. A pitch the voice starts on keeps it (#312).
- A pitch changed while the voice plays still changes its length, so a
  pitch sweep over length does not end with the sound. A pitch that
  keeps the length in real time is #317.

Tests:
- soundfx, sounds, keep-on-scene-load, node-catalog, sound-library:
  462 tests green.
- Counter-check: with length always 0, the catalog case and the new
  soundfx case fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:27:44 +02:00
adminandClaude Opus 5.5 297fca3e94 Show Direction and Curve of a node only where they matter
An animation key shows Direction only for one of the ten curves and
the Curve row only for Curve. Lerp and Set sound FX parameters hid
them on the node body, but the inspector still offered both rows for
every easing, so a linear Lerp had a Direction to pick that did
nothing.

- A node definition can name `idleParams`: settings that do nothing
  at the current values. The node body and the inspector both leave
  them out.
- `hiddenParams` stays as it was. It only trims the node body, the
  inspector keeps those rows (the Order of an event, the mode of
  Find), so it could not take this job.
- Lerp and Set sound FX parameters use `idleEasing` (formerly
  `hiddenEasing`) for it.
- The idle settings are part of the node signature of the inspector.
  Picking an easing that needs Direction or Curve rebuilds the
  section, and the row appears.

Tests:
- easing-curves: 14 tests (+1), node-catalog: 381, green
- e2e 06 (149 checks, green) checks that a new linear Lerp has no
  Direction and no Curve row in the inspector. Picking Cubic brings
  the Direction row, and Out is picked there as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:15:15 +02:00
adminandClaude Opus 5.5 e7a360db73 Offer the easings of an animation key on Lerp and sound FX ramps
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Successful in 15m36s
A timed Set sound FX parameters ran only linearly, and a Lerp had four
quadratic eases of its own. An animation key already had the full set.
Now all three offer the same choice: Linear, Step, the ten curves from
Sine to Bounce with In, Out or In-out, and a drawn Curve asset.

- easingParams in src/core/graph/nodes/easing.ts gives a node the
  fields Easing, Direction and Curve from the key's own lists, and
  easingOfParams reads them back as a key interpolation.
- There is no migration. A Lerp saved with the old in, out or inOut
  runs linear until its easing is picked again.
- Set sound FX parameters hands the easing to the player. Linear and
  Step stay ramps of the audio clock, every other easing goes to
  setValueCurveAtTime as 200 samples a second, in semitones and dB.
- Step now reaches its end at t 1. A key never asked for that, it
  reads the next key itself, so animations play as before.
- The curve-in-use count and the warning for a gone curve cover the
  sound node too.

Tests:
- node-catalog: Lerp with cubic in, quad out and in-out, a curve
  without a direction, step, an unknown easing as linear. The sound
  node hands on a curve, step and a gone curve as linear.
- sound-library: an eased change becomes a curve in semitones and
  decibels, a step a jump at the end.
- node-catalog and curve after dropping the old values: 399, green.
- e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a
  2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the
  end, a 1 s Step held 1 and then jumped to 0.5.
- Counter-check: with curves sent as linear ramps, the curve test of
  the sound library fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:05:00 +02:00
adminandClaude Opus 5.5 501a1524ed Ramp sound FX parameters over a time on the audio clock
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Successful in 15m26s
A pitch sweep counted down in On update or On fixed update runs in
game time, while the sound runs on the clock of the audio device. A
short sound magnifies any offset between the two, so the sweep came
out different on every play and most of all on the first.

- Set sound FX parameters has a new input time in seconds. The values
  run evenly to their target in that time, scheduled on the audio
  clock with linearRampToValueAtTime or exponentialRampToValueAtTime.
- Evenly in their own unit: a pitch in semitones and a gain in
  decibels, an EQ band and a Mix share linearly in their value.
- A time of 0 or below keeps the quick glide of about 0.1 s.
- Each change starts from where the voice is now. Whatever was still
  planned is dropped, so a change never queues behind an older one.
  Firefox lacks cancelAndHoldAtTime, there the value is held by hand.
- A voice still waiting on its bytes starts a timed change with the
  sound, so the whole sweep is heard.

Tests:
- node-catalog: time is handed on, a time below 0 is a quick glide.
- sound-library: semitones and decibels ramp in their own steps, a
  timed change on a waiting voice starts with the sound.
- sound-library + node-catalog + soundfx: 419 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 31 checks. A 2 s ramp from
  depth 7 to -5 read speed 0.979, 0.926 and 0.778 half a second on.
- Counter-check: folding a timed change on a waiting voice into its
  start values fails the new test.
- The gate did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:13 +02:00
adminandClaude Opus 5.5 ed2b58b3c0 Load the sounds of a project ahead of their first play
A sound was fetched and decoded on its first Play sound. That play
started late, its beginning was not heard, and a graph that tuned the
voice in the meantime had moved on by the time it started. Images and
fonts were already loaded when a project opens. Sounds now are too.

- projectFiles lists the sounds of a project, and preloadProjectFiles
  hands them to the new soundLibrary.preload. That covers the editor
  and the published game.
- Preloading opens no audio device. It decodes with an
  OfflineAudioContext at 48 kHz. The real AudioContext is still made
  on the first play, and a source resamples where the device runs at
  another rate.
- A run started right after opening a project can still find a sound
  loading. It then starts late as before.

Tests:
- sound-library: a preload makes only the offline context, and a
  play after it starts at once, opening the device only then.
- sound-library + project-files: 18 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 30 checks.
- Counter-check: decoding with the real AudioContext again fails the
  new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:00 +02:00
adminandClaude Opus 5.5 ded0022903 Run behaviors on a fixed step with an On fixed update node
Adding up the frame's dt in On update makes anything that counts in
steps depend on the frame rate. A pitch sweep sounds a little
different on every run. On fixed update runs 60 times per second of
game time with dt exactly 1/60, so the same graph gives the same
result every time.

- fixedSteps in src/core/time.ts is the one rule for the fixed step:
  spend the frame in steps of 1/60, keep the rest, catch up 8 steps
  at most after a hitch.
- The physics now counts its steps with fixedSteps too. It behaves
  as before, and On fixed update runs exactly as often as the engine
  steps.
- The steps run after On mouse wheel and before On update, each as a
  kind of its own with the usual Order setting.
- Each step gets its own context, so a node inside that reads dt
  (Time, Move, Apply force) gets 1/60 rather than the frame.
- A paused game has no steps, also for a behavior that runs while
  paused.
- The engine still takes all of a frame's steps before the behaviors
  run. A force set in On fixed update acts from the next frame on.

Tests:
- fixed-update.test.ts (new): 5 tests. Step counting, uneven frames
  adding up to 60 steps a second, the hitch cap, two steps at 30 Hz
  before On update with dt 1/60 for the event and for Time, one step
  every other frame at 120 Hz, none while paused.
- event-order, node-catalog, physics-system, game-pause: 430 tests
  together with the new file, all green.
- Counter-check: with the frame's dt inside a step and without the
  pause guard both world tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:46:15 +02:00
adminandClaude Opus 5.5 c7e9769f90 Change the sound FX parameters of a sound while it plays #315
game-maker / gate (push) Successful in 3m14s
game-maker / suite (push) Successful in 15m39s
Until now a value handed to a sound FX counted from the start of the
voice only. An engine whose pitch follows the speed, or a loop that
gets quieter, could not be built. Every play of Play sound now gives
out the number of its voice, and a new node lets the knobs of that one
voice glide to new values while it plays.

- Play sound has a new output voice: the number of this one play, or 0
  where nothing was played. Kept in a Number variable it reaches the
  voice from another event. Two plays of one sound are two voices.
- New action node Set sound FX parameters (exec, voice, fx params).
  It reaches exactly the voice it is given, a detached one too. A
  voice that has ended, or voice 0, is not reached.
- AudioSink.play returns the voice number and takes the plan and the
  values (FxVoice) instead of a finished chain. AudioSink.tune takes a
  voice number and new values. A name the voice's FX does not have is
  left out, one not given keeps its value.
- SoundLibrary hands out the number at once, before the bytes are
  there, and never twice. A voice still waiting for its bytes starts
  with the values handed in meanwhile.
- Every knob of a voice moves with setTargetAtTime (30 ms time
  constant). A knob whose value did not change is not sent again, so a
  tune every frame costs next to nothing.
- A pitch on a playing voice glides by playbackRate. A new WSOLA copy
  in the middle of a sound is not worked out, so the voice gets shorter
  or longer while it is moved. On a loop that is not heard.
- fxChainOf now gives a chain whose shape depends on the plan alone.
  A stage fed by a parameter stays at 0 dB, and a Mix fed by a
  parameter keeps both ways. A typed-in 0 or Mix at 0/1 is still left
  out by the compiler.
- Every way of a voice has a source of its own, also at the same
  pitch, so each can glide on its own. The pitched copy is shared.
- The tab keeps the numbers of what it plays, so moving a parameter
  field is heard at once.
- Test hook __gm.sound.levels(file) reads the knobs of every voice of
  a file, oldest first, and stats() counts the voices tuned.

Tests:
- soundfx.test.ts: 29 tests (+2 for the node in a running world: the
  voice of the first of two plays of one sound, and a play that played
  nothing)
- sound-library.test.ts: 15 tests (+3: glide, reach by number,
  pending voice)
- node-catalog.test.ts: 370 tests (+2, Play sound checks its voice)
- sounds + keep-on-scene-load: 31 tests, unchanged
- Counter-test: a tune that reaches every voice of the same file makes
  the reach test fail.
- e2e 34 (melektaus-atx): ALL PASSED - 30 checks (+1). In Chrome two
  looping voices of one sound start at depth 7. The one whose number
  sits in a variable glides to 0.7938 (2^(-4/12) = 0.7937), the other
  stays at 1. Both gains stay at 0.5012.
- The gate ran before voices got a number (CHECK EXIT 0, 241 files,
  3658 tests), not after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:53:26 +02:00
adminandClaude Opus 5.5 072537b2e8 Blend two ways of a sound FX with a Mix node #316
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Successful in 15m30s
A sound FX was a single line from Input to Output, so two versions
of one sound could not be heard together. The new Mix node joins two
ways again, and each way can carry its own effects, its own Pitch
included.

- Mix has the audio inputs a and b and a number input mix (0 to 1,
  default 0.5). At 0 only a is heard, at 1 only b. Like every number
  it can come from a parameter of the Input and is held to its range.
- The compiled plan and the chain a voice gets are now a tree. A
  source carries a pitch and stages, a mix carries a, b and the
  stages after it. A Mix that ends at exactly 0 or 1 collapses to the
  one way heard, so the silent way is never worked out.
- A Pitch is still worked out on the sound as it starts (#312). Behind
  a Mix it goes to both of its sounds. An effect before a Pitch
  therefore acts as if it came after it, the node now says so.
- The player starts one source per pitch, all at once and all of the
  same length, and two ways at the same pitch share one. A mix becomes
  two gain nodes with 1 - mix and mix into a sum. A voice leaves the
  count when all of its sources have ended.
- stats().last reports pitches (one per source) instead of pitch.

Tests:
- soundfx.test.ts: 27 tests (+5), sound-library.test.ts: 12 (+2),
  node-catalog.test.ts: 368, all green. Typecheck, ESLint, Prettier
  clean.
- Counter-check: a Pitch behind the Mix given to a only, and b mixed
  at 1 - mix, each fails its new test.
- e2e 34 on melektaus-atx: ALL PASSED, 29 checks (+1). The new check
  plays the pitched way mixed with the dry Input in the browser: two
  sources at 5 and 0 semitones, as long as the sound itself.
- The full gate did not run before this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:09:30 +02:00
adminandClaude Opus 5.5 790e483198 Pitch a sound FX without changing its length #312 #313 #314
game-maker / gate (push) Successful in 3m9s
game-maker / suite (push) Successful in 15m19s
The Pitch node changed the playback speed, so a sound pitched up was
also shorter and one pitched down longer. A pitch now keeps the
length, takes fractions and reaches further. Gain and the EQ bands
reach further down, and the EQ is renamed so a parametric one can
come later without a mix-up.

- Pitch shifting is the pure function pitchShift in
  core/soundfx/pitchShift.ts. It reads the sound faster or slower and
  stretches it back by WSOLA: 40 ms frames under a Hann window, each
  moved within 12.5 ms to where it continues the frame before it.
  A looping sound reads on across its end.
- SoundLibrary plays a pitched copy of the decoded buffer instead of
  setting playbackRate. Copies are kept per file, pitch and loop, the
  last 32 of them.
- stats().last tells the duration of what was played, so a check can
  see that the length is kept.
- Pitch: -64 to +64 semitones, scrubbed in steps of 0.1 (#312).
- Gain: -120 to +24 dB (#313).
- EQ is now labelled "5-Band-EQ", its bands go -60 to +24 dB (#314).
  The node type stays SoundFxEq, so saved graphs load unchanged.

Findings:
- A plain granular shifter was tried first and missed the pitch: at
  +0.5 semitones it measured 438 Hz instead of 452.9 Hz, because every
  grain restarts at the phase of the original. WSOLA measures 452 Hz.
- Far ends: +64 semitones from 440 Hz lands above half the sample rate
  and folds back, -64 is below what a 40 ms frame can follow. Nobody
  has listened to either.
- Speed: 3 s of a 48 kHz channel take 47 ms at +3 and +64, 171 ms
  at -64 (measured in Node).

Tests:
- pitch-shift (new): 7 tests. A 440 Hz sine at +12, -12, +7, +0.5 and
  -30.5 semitones is within 2 % of the target and keeps its length.
  Level stays at most 1, a loop does not fade at its seam.
- sound-library: 10 tests (+1), soundfx 22, node-catalog 368.
- Counter-check: with the pitched copy switched off the new
  sound-library test fails.
- e2e 34 (melektaus-atx): ALL PASSED - 28 checks. Its octave-down
  voice is now also checked for the length of the sound.
- Typecheck, ESLint and Prettier clean. The gate did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:32:00 +02:00
adminandClaude Opus 5.5 183f356c99 Let a game hand values to the parameters of a sound FX #311
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 14m15s
A sound FX played the same every time: its numbers were settings of
the nodes. Now the Input declares parameters with a default, every
number of Gain, Pitch and EQ is an input they can feed, and a Play
sound hands in values for them.

- Input declares number parameters in the inspector, each with a
  Default (DeclaredPort.default, SetPortDefaultCommand). They are
  outputs of the Input. Input and Output count as terminals now, one
  of each per graph.
- Gain, Pitch and EQ take their numbers at inputs under the keys the
  settings had, so saved graphs sound as before without a migration.
  Each value is held to its range however it arrives.
- The compiler gives an FxPlan with the parameters still open.
  fxChainOf fills them in when a voice starts. The player still only
  sees an FxChain.
- New behavior node Sound FX parameters: names an FX and picks its
  parameters like a Variables node picks variables. Each picked one is
  a number input that starts at the FX's default. Its output has the
  new port type fxparams, which only fits Play sound's fx params.
- Play sound warns when the parameters name another FX than the one
  it plays. Names the FX does not know are left out.
- Renaming a parameter at the Input renames it in every Sound FX
  parameters node naming that FX, edges and values included.
- The Sound FX tab has a number field per parameter in its header.
  Play uses those values, they are never written into the asset.
- A value counts from the start of a voice. Changing a playing voice
  is #315, a Mix node is #316.

Tests:
- soundfx.test.ts 22 (+11): parameters in the plan, held values, a
  0-by-default stage, EQ band, save with defaults, wrong port types,
  default command with undo, Sound FX parameters into Play sound,
  defaults of picked ones, node problems, mismatch, rename with undo
- node-catalog 368 (+3): Play sound fx params, Sound FX parameters
- e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5,
  a Sound FX parameters node gives the game's voice depth 7)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test
  files, 3636 tests green
- Counter-check: with fxChainOf ignoring the values and the rename
  not followed, 5 of the new tests fail.
- Browser probe: header fields, inspector Default and the picked
  parameter looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:26:20 +02:00
adminandClaude Opus 5.5 de14f0f9e2 Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.

- New asset type soundfx ("Sound FX"), not placeable, with a graph
  and its own tab. The heading in the asset panel is "Sound effects",
  so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
  that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
  EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
  emitter. An input takes one edge, so the result is one line: a
  pitch and a list of gain and filter stages. A node without input
  or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
  sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
  An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
  order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
  through the graph as it stands. Stop there ends only its own voice.

Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
  node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
  2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
  test files green. The red one was sound-library (its fake
  AudioContext had no playbackRate), fixed afterwards, gate not
  re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
  test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:45:42 +02:00
adminandClaude Opus 5.5 7b1796baac Let a container animation reach alike-named objects in another one
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Successful in 15m43s
An animation over a container aims its rows at template ids. Played on
another container, those ids are not there, so every row below the
root was skipped without a word. "Enemy - Hit" built over the Slime
flashed the slime's Sprite but never the skeleton's.

- When the template id is not found below the object, the runtime
  takes the names on the way to the row's target in the root and
  walks the same names below the object, level by level.
- The id still comes first, so the root's own instances bind as
  before.
- A name missing at some level, or taken twice, skips the row, as
  a gone target always did.
- The rule is the pure function targetNamePath in animation.ts. The
  AnimationSystem only walks the children.

Tests:
- animation.test.ts: 84 tests, all green (+2). One plays over a
  second container built alike, renamed and with the name twice.
  The other names a row two containers deep.
- Counter-check: with the name fallback off, the new runtime test
  fails (1 of 84).
- Project cbbf160bb7e8 (revision 14407), Demo scene: "Enemy - Hit"
  and "Enemy - Die" change Flash and scale on the skeleton's Sprite.
  Without the fallback the skeleton stays unchanged.
- The gate did not run before this commit. CI covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:16:42 +02:00
adminandClaude Opus 5.5 2da07599d8 Let Get position give only the local x and y
game-maker / gate (push) Successful in 3m6s
game-maker / suite (push) Successful in 16m6s
Get world position now covers the world coordinates. Two nodes that
both offer world x and y made it unclear which one to take. No project
uses the world outputs of Get position any more.

- Get position drops its world x and world y outputs and its
  description points to Get world position.
- Old snapshots with edges on those outputs still load. The edges hang
  on ghost ports and the node warns about them.
- The unit tests in hierarchy and raycast and the graphs in e2e 43 use
  Get world position where they read the world coordinates.

Tests:
- node-catalog, hierarchy, raycast: 3 files, 417 tests, all green
- e2e 43 on melektaus-atx: ALL PASSED, 17 checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:13:58 +02:00
adminandClaude Opus 5.5 526b1da7f4 Add a Get world position node with just the world x and y
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 16m27s
Get position hands out four numbers, local and world. A graph that
only needs where an object is in the world had to pick the two world
ports out of four and leave the local ones dangling.

- Get world position has one object input and the outputs x and y.
  It reads the same worldPositionOf as Get position, so a child adds
  every parent's position in.
- Without an object it reports the origin, like Get position.

Tests:
- Node catalog: a top-level object, a child under a parent and no
  object (3 cases).
- node-catalog and hierarchy: 2 files, 378 tests, all green. The
  full gate was not run for this commit, CI covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:44:04 +02:00
adminandClaude Opus 5.5 ed7872c893 Let a called behavior share the caller's local state machines
A local state machine started over in every Run behavior call, so a
state set in a sub-behavior was gone by the next call. A character
controller split into sub-behaviors could therefore never remember
that it was attacking. Scene or global scope would keep the state, but
then every character shares it.

- A call now runs on the states of the instance that runs it, however
  deep. A state one call sets is still set in the next one, and the
  call begins in the start state like the instance does.
- A move made inside a call is reported like one the caller made. The
  caller's On state entered and On state exited hear it, the event
  nodes of the called graph stay silent as before.
- Two objects running the same behavior still hold a state each.
- This reverses the rule from #229 that local machines of a called
  behavior are not kept. Keep stays a switch for variables only.

Tests:
- Two unit tests that pinned the old rule now expect the shared state.
- New world test: a sub-behavior closes a door per object, the
  caller's events hear it, and the next call finds it closed.
- Counter-check: with the old call context the 3 tests fail.
- Gate on melektaus-atx: 239 files, 3600 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:38:06 +02:00
adminandClaude Opus 5.5 d80d5cce26 Let a Start input or a variable name the state machine it carries
A sub-behavior that gets its machine through a Start input had no
state ports on Is in state, Set state and the other state nodes.
Their ports came only from an inline pick or a State machine literal,
so a machine handed in from outside left them empty.

- A statemachine port of a Start node can now name a machine
  (DeclaredPort.machine). The inspector shows the pick next to the
  port name. SetPortMachineCommand sets it with undo.
- machineIdOf follows an edge from such a port to that machine, so
  the state nodes it feeds offer its states.
- A Run behavior node has the machine picked in advance on that input.
- A different pick there is allowed, but the node warns, because the
  called graph asks for the states of its own machine.
- A statemachine variable of a Variables node can name a machine
  the same way, picked next to the variable's row.
- The inspector signature of a node now includes the picked machine.
  Before, the pick changed the ports but the dropdown kept its text
  until the node was selected again.
- A local machine handed in still starts in no state every call.
  That rule from #229 is unchanged.

Tests:
- tests/core/start-machine.test.ts (7): ports with and without a
  machine and undo, save and load, the preset value moving a shared
  machine through the call, the warning, a port of another kind,
  the inspector signature, a Variables node.
- Counter checks: without the new branch in machineIdOf 2 tests
  fail, without the machine in the signature 1 test fails.
- Gate on melektaus-atx: 239 files, 3599 tests, CHECK EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:15:56 +02:00
adminandClaude Opus 5.5 2c19c89878 Let variables, fields, ports and list items be dragged into order (#310)
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 19m34s
Variables of a behavior, the fields of a struct, the ports of a start
or end node and the items of a list could only be added and removed.
Changing their order meant removing and adding them again. Now each of
them is dragged by the mark in front of its row.

- sortBlocks (ui/blockDrag.ts) makes the rows of one column sortable.
  A block is every row it covers, so a struct moves with its fields and
  a list item with its nested list. The block is dimmed while it is
  carried, an accent line shows where it lands, and a scrolled panel
  follows the pointer at its edge. Less than 4 px stays a click.
- The arithmetic is pure and lives in ui/sortable.ts: blockGap,
  gapLineY, dropSlot and edgeScroll. dropIndex of the tile strip moved
  there too, so the reorder maths sits in one file.
- MoveVariableCommand, MoveStructFieldCommand and MovePortCommand
  build on the existing MoveCommand. Undo moves back.
- Lists need no command. InlineList replaces the list as a whole, as
  it does for every other edit of its items.
- The grip is the kind mark, for a list item its number. Moving only
  happens among siblings: a field stays in its struct.
- On a Struct asset the move runs through WithStructTypes. Typed
  variables, Pack/Expand ports and split outputs follow the new order.
- Values and edges hang on names, so only the order changes. Nodes
  running a behavior read their ports from the start node, the new
  order reaches them without a copy.
- Not sortable by design: the variables of a placed object, the fields
  of a typed struct (moved on the Struct asset) and the picked ports
  of Variables/Properties nodes.
- Test hook __gm.inspectorMark(label) finds the mark of a row.

Tests:
- Gate on melektaus-atx: 235 files, 3572 tests, CHECK EXIT 0 (+9).
- tests/core/reorder.test.ts: a variable, a field two levels deep, a
  Struct asset reaching a typed variable and Pack, and start ports
  reaching the calling node with edges and result kept.
- e2e 22: ALL PASSED, 39 checks (+4, sum 1739). 22f-iii drags a
  variable past a struct of several rows, then a field inside it.
- Counter-probes: dropSlot by top edge instead of middle and a field
  move that ignores its path fail 3 unit tests. With sortBlocks
  switched off, the three order checks of 22f-iii fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:45:04 +02:00
adminandClaude Opus 5.5 8452a99c64 Let a graph read and change the z of an object
Until now a behavior could not move an object in front of another
while the game runs. Set position writes only x and y, and z lives on
the running object rather than in its props, so Properties cannot
reach it either. Drawing already reads the z every frame, so only the
nodes were missing.

- Get Z (data): object in, z and world z out. World z is summed up
  with the parents, the way drawing does it (worldTransformOf).
- Set Z (action): object and z in. It sets the object's own z, local
  to its parent like Set position. Higher is drawn in front from the
  next frame on.
- Two nodes of their own rather than a z port on Set position and
  Get position, as asked.

Tests:
- node-catalog: Get Z for a child (2 below 5 is world z 7) and for no
  object, Set Z writes only z and leaves x/y.
- set-z.test.ts: in a running world a key sets the lower of two
  sprites to z 2, drawSlots then draws it in front, Get Z logs 2 and 2.
- Two counter-checks bite: Set Z without the write and Get Z without
  the parent fail 3 tests.
- Gate: 233 files, 3548 tests (3542 + 6), CHECK EXIT 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:08:35 +02:00
adminandClaude Opus 5.5 3e05cbbed5 Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing
and sorting already read both off the TextureRef every picture hands
out, but the generated pictures always said "no slice, no line". So
the feature moves from the frame to the picture, without a second
copy of any rule.

- PictureCut (border, fill, sortLine) is one type. TextureFrame
  extends it, PixelArtAsset and VectorArtAsset carry it as a whole.
  The texture file format does not change, and no migration is needed.
- The rules in texture.ts take a cut and a size: cutSortLine,
  setCutSlice, rawCutSlice and the rest replace the frame* versions.
  serialize.ts reads all three through one readCut.
- cutTarget.ts resolves a target to cut and size: a texture frame, a
  pixel frame's size or the vector canvas. It lives apart from
  texture.ts because vectorart.ts reaches texture.ts itself.
- SetSliceCommand and SetSortLineCommand take that target instead of
  (assetId, frameId). They refuse an asset that draws no picture.
- generatedRef fills slice and sortLine from the drawing. The whole
  strip of a pixel art asset with several frames gets none, it is no
  picture of that size. With one frame asset and frame are the same.
- Nine-slice scale is one shared schema row, now also on pixel art and
  vector art.
- cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame
  inspector and the drawing inspector both build them.
- SheetCanvas draws a drawing's lines that are set, dashed as in the
  Texture tab. They are not dragged there, the painting tools own the
  canvas (as proposed in the issue).
- drawOrder falls back to the picture's height when a ref has no
  rect, or a sort line on vector art would have been ignored.

Tests:
- Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector
  art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a
  drawing, a cut on the whole strip, no height fallback.
- e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where
  12-pixel borders at scale 2 put them. Without borders both spots
  read green.
- Looked at: the Pixel art tab with the lines, and the sliced sprite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:53:51 +02:00
adminandClaude Opus 5.5 d9c6bbfa4d Add a node that names the render layer of an object (#305)
game-maker / gate (push) Successful in 3m14s
game-maker / suite (push) Failing after 14m18s
The issue asked for Set render layer, Set sees and Render layer of. As
with #211, a probe showed that writing already works. A Properties
node of the object's type writes Render layer during a run, Inherit
included, and one of type Camera writes Sees. The next picture follows
both. As agreed, only Render layer of is built, and writing stays with
Properties.

- Render layer of is a data node with the input object and the outputs
  name and number. It answers the layer that holds, so an object
  without a choice of its own names its parent's.
- NodeContext.renderLayerOf answers the number with the same rule the
  drawing uses (renderLayerAlong). The new field renderLayers on
  GraphLookup brings the names to the node.
- hasRenderLayer in PropertySchema says which types are drawn.
- Decided here: for an object that is not drawn (a camera, a
  behavior, a collider) the name is empty and the number -1, as Layer
  of does without a collider.

Changing the render layer or Sees during a run had no test so far.
Three unit tests now hold the proof from the issue.

Tests:
- Gate: 231 files, 3534 tests (3526 + 8), exit 0
- e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1)
- Unit: the coin moves into the overlay with Set and back with
  Inherit, a HUD container takes its sprite along, the main camera sees
  a second layer once its Sees is set, and Render layer of names the
  inherited layer, and none for a behavior
- Node catalog: four cases for Render layer of
- 63c3 moves a sprite onto UI in a Play run: the main camera no longer
  draws it, and the log reads "UI" and "1"
- Three counter-probes fail 1, 1 and 2 tests: without inheriting in
  the node, without the check for a drawn type, and without
  inheriting in the drawing
- "layer of" in the palette still finds Layer of first (6z-x)
- The e2e sum is 1731 (1730 plus 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:25:47 +02:00
adminandClaude Opus 5.5 70f3608733 Let Mouse position and Is hovered see through any camera (#304)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 16m23s
An overlay camera from #302 can stand elsewhere and zoom differently.
A HUD button only it shows lay somewhere else in the world than the
mouse of the main camera, so Is hovered never found it. Both nodes now
take a camera and see the mouse through that camera's picture.

- New input `camera` on Mouse position and Is hovered. Left open, both
  read the main camera exactly as before.
- `pointerThrough` in core/properties/camera is the rule as a pure
  function. It fits the camera's picture into the main one and centres
  it, the same way overlayView draws it.
- The game view reports where the mouse lies on the main picture, as a
  part of its width and height. The world turns that into a camera's
  view, so a main camera on the move does not drag the mouse across
  the HUD while the mouse stands still.
- `in view` is true only on the camera's own picture. A camera that is
  not drawn (switched off, a second plain one, not a camera) never has
  the mouse on it.
- On click and Mouse button stay with the main camera, as agreed in
  the issue.

Also fixes a unit test that 1d0fa7b broke. play-settings expected 8
to be no game view scale, but the steps go up to 20 since then. It now
checks 20 as the top and 21 as no choice.

Tests:
- Unit: the rule lands on the same world point as overlayView plus
  screenToWorld at four screen points for two cameras. One mouse gives
  (100, 50) through the main camera and (1050, 25) through an overlay
  at (1000, 0) with zoom 2.
- e2e 63c2: the real mouse 30 units right of the picture centre hovers
  the overlay's square through it, through the main camera not.
- Counter-checks: Math.max instead of Math.min in the rule, and no
  check for "drawn", each fail one unit test. The view reporting the
  centre of the picture always fails 63c2 with x 2000 instead of 2015.
- Gate: 231 files, 3526 tests (+6). e2e 63: 12 checks (+1), sum 1730.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:09:53 +02:00
adminandClaude Opus 5.5 a526e1b40f Use a shader graph as a node in other shaders (#303)
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Failing after 14m37s
A calculation needed in several shaders (a greyscale, a noise, an
outline) had to be built again in every one of them. A shader
function keeps it as an asset of its own, called like a behavior is
since #16.

- New asset type `shaderfunction` ("Shader function"), edited in the
  Shader tab. The panel beside the graph shows no picture, the
  readout counts its inputs and outputs.
- Two new nodes: `Function input` and `Function output`. Their
  declared ports, numbers or colours only, are the ports of every
  call. One of each per function, like Start and End.
- The call is node `ShaderCall`, named after the function. The
  palette lists every function, it can be dragged in from the Assets
  panel, and a double-click opens it.
- The compiler sets the function in where it is called
  (`inlineFunctions`). It renames the copied nodes `<call>/<node>`
  and turns an unconnected port into a constant. Passes, Time, the
  Edge cut and the corner walk then see a plain graph.
- A problem inside a function is reported at the call, in the
  function's words. The function's own tab checks it on its own
  (`checkShaderFunction`).
- `ShaderLibrary` keys a shader on its graph and on every function it
  calls, however deep, so a change reaches every caller.
- The port commands mirror a new, renamed or removed port into every
  call, as for a behavior's Start and End. `NodeDef.portKinds` holds
  the two ends to Number and Color.
- As agreed in the issue: no `Property`, `Color property`, Output or
  Vertex output in a function, and `Time` is the caller's clock.
- Own decision: a `Pass` may stand in a function. It becomes a pass
  of the calling shader, so a wide blur can be a function.
- A function calling itself, also through another, says so at the
  node and is not set in. The palette does not offer such a call.
- The Shader tab showed only the compiler's problems. A call nothing
  reaches was never compiled and looked healthy, so a call now also
  shows its own problem there, as in a behavior.

Tests:
- Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in
  tests/core/shader-functions.test.ts, 2 for the new icon)
- e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in
  place both #606060/#a2a2a2, halved in the function #303030/#515151.
- Counter-check: a fingerprint without the called functions fails 1
  unit test, and in the browser the change check (#606060 stays).
- The first e2e runs found two faults, both fixed: a double-click
  only selected the function, and an unreached call of itself
  showed no problem.
- e2e total 1728 is computed (1725 + 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:09:53 +02:00
adminandClaude Opus 5.5 b2326085ed Show each camera only its layers and lay overlay cameras on top (#302)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m26s
A camera could only show the whole scene, so a HUD drawn in the world
would run through a camera shader (#299) with everything else. Render
layers say which cameras see an object, and overlay cameras draw their
own picture over the main one.

- The project has a list of up to 16 render layers, edited in Project
  settings next to the collision layers. It is a list of its own, read
  by the same rules (`layerNamesOf`).
- Sprites, texts, tilemaps, particle sources and containers carry a row
  `Render layer` under their switches. Its default `Inherit` takes the
  parent's layer, at the top the first one.
- A camera has `Sees` (all layers by default) and `Overlay`. The main
  camera is the first one switched on without Overlay, so two plain
  cameras still draw through the first one only.
- Every view through a camera keeps only what that camera sees and lays
  one see-through view per overlay camera over itself. That view is a
  `WorldViewport` of its own, handed its view and frame from below.
- An overlay is fitted into the main camera's whole picture without
  distortion. One of the same size and zoom covers it exactly.
- `z` sorts within one camera only. The pictures lie on top of each
  other as a whole, which is what a shader per camera (#299) needs.
- The Game tab's preview now collects the scene once per redraw. Before
  it collected it twice, with the cameras it would have been five times.
- `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which
  takes the list it sets.

Follow-ups: #304 (the mouse through a chosen camera) and #305 (change
the render layer at run time).

Tests:
- Gate: 226 files, 3487 tests (3480 + 7)
- e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a
  red main sprite at z 1000, in the preview and in the running game.
  A `UI` sprite in front of the main camera and a `Default` one in
  front of the overlay stay away. Without Overlay the second camera
  draws nothing. The settings dialog adds a render layer at the end.
- Counter-checks: inheriting from parents switched off fails 1 unit
  test. Overlays never drawn fails the two overlay checks in e2e 63.
- e2e total 1720 (1716 + 4), computed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:32:52 +02:00
adminandClaude Opus 5.5 f03ca0d382 Draw a shader in passes with a Pass node (#301)
game-maker / gate (push) Successful in 3m3s
game-maker / suite (push) Successful in 14m43s
A Blur stops at radius 8 in one pass, and bloom needs a softened copy
of the picture beside the original. A Pass node cuts the graph: what
feeds it is drawn into a picture of its own first, and the nodes that
read a picture read that one when it feeds them.

- Pass has color and alpha like the Output, a Resolution setting
  (Full, Half, Quarter) and a picture output. Texture color, Texture
  alpha, Sample texture, Blur and Texture size get a picture input.
  Unconnected it is the wearer's own picture, so the original stays
  readable beside any pass.
- picture is a port type of its own. It plugs only into a picture
  input, not even into an any port.
- The walk shared by the compilers can stop at a node (cut). The
  shader compiler draws each Pass it meets as a fragment of its own,
  earlier passes first. A ring through a picture says Feeds itself,
  and more than 8 passes say so at the node.
- A pass draws untinted. The tint comes on when the last part reads a
  picture, so a particle source draws its passes once for all of its
  particles (user decision).
- A pass's picture is the wearer's picture and its Padding, at the
  pixels of the picture times Resolution, at most 2048 per side. It is
  read with the picture's own filter and continues past its frame as
  the Output's Edge says.
- PassPictures keeps one render texture per pass and wearer. It draws
  again only when picture, size, Padding or a uniform changed. A
  field's picture is a render texture that changes under the same id,
  so a field draws its passes every time.
- Sprite, sliced sprite, text, particle source, field shader and the
  Shader tab's preview all draw passes. syncShader takes a fixed
  picture for a 9-slice, which holds its corners but keeps the margin.
- texelAt and blur take the sampler they read. Existing shaders
  compile to the same program apart from that argument.
- A field shader's Blur and Texture size count a pixel as a unit of
  the frame (uPixelScale), not of the screen. Before, a field's blur
  grew when zooming out and shrank when zooming in. This came with
  #170 and #300 and is fixed here because passes inherit it.
- tools/perf-shaders.mjs and tools/perf-field-shaders.mjs measure two
  passes with GM_PERF_PASSES=1 and GM_PERF_PASS_RESOLUTION. Below Full
  an open Pass shrinks the picture first, as the wiki's recipe does.

Found on the way:
- Destroying the pictures before the shader made Pixi warn that a
  texture was still bound. The shader goes first now, and a picture of
  a new size is resized rather than replaced.
- Two passes in a row could draw plain white the first time. Pixi
  builds the code that binds a program's resources once and finds them
  by position. A pass of one graph is often the same program as a pass
  of another with fewer slots, so it bound a placeholder. Every shader
  now declares all 8 pass slots and its uniform group.

Tests:
- Gate on melektaus-atx: 225 files, 3480 tests (3467 + 13), exit 0.
- e2e 28 and 31 on melektaus-atx: 97 checks (94 + 3) and 44 checks
  (43 + 1), all passed, no Pixi warning in the console.
- 28l-vii: a Blur across, a Pass and a Blur down draw the 5x5 spot of
  a Blur of Both (difference 0.000). On a grey sprite a pass's picture
  added to the original is 231/60/60, each tinted once. Two Passes in
  a row after a graph with one show red and green, not white.
- 31v-iii: a Blur of 8 fades a field's frame edge over 8.0 units at
  50 % and at 200 % zoom.
- Counter-checks: a tinted pass fails 1 unit test. Pictures never
  bound fail both e2e checks (spot 0.20 instead of 0.04, 243/158/158).
  Pass slots by the number of passes draw #ffffff, and a field without
  its pixel scale ramps over 16 and 4 units: exactly the two new
  checks fail.
- e2e sum 1716 is counted, not yet measured (1712 + 4).
- Measured on stein2 (HD 630, one run per row): 50 field shaders at
  radius 8 go from 11.4 to 41.3 fps in two passes, 44.4 at radius 16
  (three passes at Half). 800 sprites at radius 8 go from 13.9 to
  24.5 fps. At radius 4 one pass stays faster from 400 sprites on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:51:18 +02:00
adminandClaude Opus 5.5 df61b08bd1 Soften a shader's picture in one pass with a Blur node (#300)
game-maker / gate (push) Successful in 3m3s
game-maker / suite (push) Successful in 14m26s
A blur already worked with Sample texture, but only by wiring every
sample by hand: 25 nodes for a 5x5. Averaging their colours also gave
a dark seam wherever a neighbour was transparent. The Blur node writes
the loop itself and averages the right way.

- Blur takes radius (pixels of the picture, 0 to 8, fractions allowed)
  and u/v like Sample texture, and gives color and alpha.
- Kind is Box (default) or Gaussian, Direction is Both, Horizontal or
  Vertical. Both are arguments of one prelude helper, blur().
- The samples are averaged premultiplied and only then made straight,
  so a transparent neighbour lowers the opacity and adds no black.
- The outermost ring counts by the part of the radius that reaches
  it, so a radius from a Property changes smoothly and compiles
  nothing again.
- A node may now declare prepare, a vec4 the compiler works out once
  into a local. Color and alpha read that one sum instead of blurring
  twice.
- A reached Blur counts as a shifted sample, so the picture comes
  with the Output's Edge. It cannot feed the Vertex output.
- Both perf tools take GM_PERF_BLUR=<radius>.

Wider blurs and bloom need several passes. That is #301.

Measured on stein2 (HD 630, one run per value, fps), Box blur:
- 48x48 sprites at 100/400/800: radius 2 59.8/58.4/55.6,
  radius 4 53.1/51.4/36.3, radius 8 46.6/24.4/14.4.
- 256x256 field shaders at 1/10/50: radius 2 60.2/56.0/49.0,
  radius 4 60.1/49.0/27.1, radius 8 52.4/34.9/11.4.

Tests:
- Gate: 224 files, 3467 tests, CHECK EXIT 0 (+7).
- e2e 28: ALL PASSED, 94 checks (+5 in 28l-vi). Pixels match the
  arithmetic to 1: the last red texel at radius 0.5/1/4 reads
  187/173/154 (expected 188/173/154).
- Counter-checks: Blur not counted as shifted fails 1 unit test.
  Blur output left premultiplied fails the seam and spot checks.
- e2e total 1712 is computed (1707 + 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:11:31 +02:00
adminandClaude Opus 5.5 d6d38a6422 Lay a shader over a particle source's whole field (#170)
game-maker / gate (push) Successful in 3m2s
game-maker / suite (push) Successful in 14m6s
A shader on a source runs per particle and stops at each quad, so a
glow or blur across the whole cloud was not possible. A source now has
a second shader row that is laid over everything it draws at once.

- New rows Field shader, Field width and Field height (256) on a
  particle source. The size rows show only while a field shader is
  picked (new `shownWhenSet` on a row definition).
- The particles are drawn into a RenderTexture of the frame, and a
  sprite mesh wearing the field shader draws that picture. The frame
  is centred on the source and turns and grows with it.
- UV run 0 to 1 across the frame. Texture color, Texture alpha and
  Sample texture read the picture of the particles.
- The picture has as many device pixels as the frame covers on
  screen, at most 2048 per side. A frame off screen draws no pass.
- The field shader's Padding and Vertex output do nothing: the frame
  is the edge, and its corners stay put as on a nine-sliced sprite.
- A name both shaders declare is one row, and both uniforms read it.
- The scene view draws the frame dashed round a selected source. The
  Particles tab preview wears the field shader too.
- `tools/perf-field-shaders.mjs` measures 1, 10 and 50 sources. On
  the HD 630 they ran at 60.0, 60.0 and 57.6 fps against 60.1, 60.0
  and 59.2 without, one run per row.

Tests:
- Gate: 223 files, 3460 tests (3452 + 8), CHECK EXIT 0.
- e2e 31: 43 checks, 3 of them new. Sum 1707 is computed (1704 + 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:24:51 +02:00
adminandClaude Opus 5.5 bf27e42e8a Let a shader declare a color the sprite sets (#298)
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Successful in 14m11s
A shader could only ask its sprites for numbers. A tint, an outline
colour or a glow colour had to be built from three Property nodes.

- New node Color property next to Property: Name and Default (a
  colour, white unless picked), output `color`. It becomes a colour
  row below the Shader dropdown and a vec3 uniform in the program.
- Number and colour properties share one set of names. A clash is
  the existing "Another property is named" problem.
- Like Color it cannot feed the Vertex output.
- Placed from the palette it is called tint, then tint2.
- The preview in the Shader tab shows a colour field for it. Like
  the sliders it starts at the sprite's value and never writes into
  the project.
- `declaredRows` takes the row builder, so the particle graph keeps
  its number rows unchanged.

Tests:
- Gate: 222 files, 3452 tests (3445 + 7)
- e2e 28: 89 checks (28l-v: default colour drawn, then the colour
  the sprite sets)
- e2e total 1704, counted (1703 + 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:43:54 +02:00
adminandClaude Opus 5.5 4f4c149211 Duplicate an asset from its right-click menu (#297)
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Successful in 14m16s
A new asset that starts out like an existing one (a death animation
next to "Break", a second slime) had to be built again by hand. Now
the asset panel's right-click menu has Duplicate.

- The copy lands right below the original and is named the way a new
  asset would be ("Crate 2"). It is selected and its tab opens where
  it has one. A copied scene does not become the active one.
- One undo step takes it out again. A redo brings back the same id.
- The copy is a deep copy: values, graph, tracks, layers and objects
  are its own, and editing it leaves the original alone.
- Frames of a texture or of pixel art get fresh ids, because a sprite
  names a frame across the whole document.
- Objects of a scene or container get fresh ids the way pasted ones
  do. Parents and object references among them follow the copy.
- Whatever pointed at the original keeps pointing at the original: a
  sprite wearing it, an animation's root, a placed object.

Tests:
- 3 new unit tests (DuplicateAssetCommand). One counter-check run:
  without renaming the references the scene test fails.
- e2e 04: 2 new checks with the real menu (copy named "Crate 2" right
  behind the Crate and selected, one undo removes it).
- Gate: 221 files, 3445 tests, CHECK EXIT 0.
- e2e total 1703 is computed (1701 + 2). Full suite left to CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:17:02 +02:00
adminandClaude Opus 5.5 0dda6a012a Walk an object to a point, and measure distance in the world (#296)
game-maker / gate (push) Successful in 2m55s
game-maker / suite (push) Successful in 14m10s
The slime of #296 walks to three kinds of point: a spot in its home
area, home again, and the player. Each of those took about eight
nodes, and Distance measured between local positions, so it was wrong
for the player, whose sprite sits inside a container.

- New node Move toward: steps an object toward a world point by at
  most speed times dt and stops exactly on it. Below a moved or turned
  parent the step is turned into the axes Move counts in.
- With Collide with solids it moves the way Move does. Outputs are
  arrived, the distance still left and hit.
- Distance now measures in world units, however deep each object is
  nested.

The slime itself, its state machine, the player's lives and the
change to the Break animation live in the project, not in the repo.

Tests:
- Gate on melektaus-atx: 220 files, 3442 tests (3436 + 6),
  CHECK EXIT 0.
- Six new catalog cases. Both counter-checks bite: Distance back to
  local positions fails one, Move toward without the parent's axes
  fails one.
- No e2e module touches these nodes. The e2e sum stays at 1701.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:21 +02:00
adminandClaude Opus 5.5 f1e3bd8378 Let a joint glide on a rail and drive it with a motor (#154)
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Successful in 14m2s
Step 2 of the joints: a door that opens by itself, a lift and a
piston need a joint that slides along one line and a motor that turns
or pushes. A graph also needs to read how far such a joint stands.

- Slider: a fifth joint type on Rapier's prismatic joint. Its rail is
  the joint's own x axis, so turning the joint turns the rail. Joints
  now carry Scale, Rotation and Mirror like a sprite.
- Limit distance with Min and Max, in world units from where the
  object was placed.
- Motor on hinge and slider: Off, Speed or Target, with Motor speed,
  Motor target and Max force (0 = no limit). Target goes there at no
  more than the speed and slows down for the last stretch.
- Changing speed, target or force keeps the engine joint. Switching
  the motor on or off builds it again.
- New node Joint position: value and speed of a hinge (degrees,
  counting past a full round) or a slider (world units).
- Scene view draws a slider's rail dashed, with a tick at each limit.
- shownFor in the schema takes a list of choices that must all hold.

Findings, measured with a probe against Rapier 0.20:
- A prismatic joint holds both bodies at one angle. Held to the scene
  or to an object without a rigidbody, the joint's own anchor body is
  turned by the placed difference, so nothing jumps. Between two
  rigidbodies the held one is turned to match, and the console says
  so once.
- Rapier reads no joint angle back. The position is worked out from
  the two bodies.
- A hinge motor is weak on a small body at the end of a long arm
  (box 20 on an arm of 100 does not lift itself). More solver
  iterations would help but cost every scene. A door of 100 x 20
  reaches -90 and holds it. This is written in the wiki.

Tests:
- Unit: 16 new in joints.test.ts, 1 in joint-links, 3 catalog cases.
- e2e 41: 27 checks (25 + 2), block 41f with a real mouse in the
  inspector and the motor in play (59.1 of 60 units in 1 s).
- 9 of 9 unit counter-probes fail as they should.
- Gate: 220 files, 3436 tests (3416 + 20), CHECK EXIT 0.
- e2e total 1701 is counted (1699 + 2), the full suite did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:09:45 +02:00
adminandClaude Opus 5.5 4716726b6b Let a shader move the corners of the picture it wears (#79)
A shader could only say what colour a pixel has. Where the picture
is drawn was fixed to the four corners of its rectangle, so nothing
could wave, bend or wobble.

- The new node Vertex output takes x and y, in pixels of the
  picture, and moves every corner of the wearer's grid by them. The
  UVs stay with the corners, so the picture bends along.
- Subdivisions on that node says how fine the grid is: cells per
  axis, 1 to 64, 8 by default. 1 is the plain quad. A shader without
  the node keeps the quad every sprite shares.
- A graph holds one Vertex output at most. UV, Time, Property,
  Number and the math nodes may feed it, and one node may feed both
  outputs.
- Texture color, Texture alpha, Sample texture, Texture size and
  Color cannot feed it. The node says "Not available per corner"
  and the shader does not compile.
- Sprites, texts and the preview in the shader tab wear the grid.
  A nine-slice sprite and particles draw as if the node were not
  there.
- Picking, the selection frame, the size and colliders stay with
  the rectangle. A wearer with a Vertex output is never culled, as
  with Padding, and with Padding the grid covers the room around
  the picture too.
- tools/perf-shaders.mjs takes GM_PERF_SUBDIVISIONS to measure a
  grid against the plain quad.

Measured on stein2 (Intel HD 630), frames per second, one run per
row: at 800 sprites 58 with a shader on the quad, 46 with 8 cells,
41 with 32. Up to 400 sprites a grid hardly shows. Every sprite
holds its own grid.

Tests:
- Gate on melektaus-atx: 220 files, 3416 tests, CHECK EXIT 0.
- 15 new unit tests for the compiler and the grid.
- Four new e2e checks in modules 28 and 30: a fixed offset moves
  the picture, 8 cells bend only the lower half where 1 cell shears
  it all, Texture alpha is refused at the node, a text moves.
- e2e 28,30 on melektaus-atx: 110 checks, all pass.
- The e2e total of 1699 is counted (1695 + 4), the full suite did
  not run.
- No counter-probe, no screenshot. Not looked at: the preview, Time
  in the browser, flipped sprites, Padding together with the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:14:00 +02:00
adminandClaude Opus 5.5 963f8a152a Open a scene framed, and thin out two inspector sections (#295)
Three small things asked for in one issue. A scene came up wherever
the view happened to stand, and the inspector carried a button and a
list that are better reached somewhere else.

- A scene comes up framed as if Fit had been clicked: after a
  project is loaded and when another scene is opened. From then on
  the view belongs to the user.
- An empty scene has nothing to frame and stands at the origin, one
  pixel per unit. A scene the running game loads is not framed anew.
- The Container tab is left out. It keeps the view of each container
  as before (#171).
- The Unpack button and the two lines under it are gone from the
  inspector of a placed container. Unpack container stays in the
  right-click menu of the hierarchy and of the scene view.
- The corners of a polygon collider are no row in the inspector any
  more. They are edited on the outline in the scene view. The new
  schema option noRow keeps a property out of the inspector while it
  stays a property.

Tests:
- Gate on melektaus-atx: 219 files, 3401 tests, CHECK EXIT 0.
- One new unit test in tests/core/polygon.test.ts: the corners are
  a property of the collider and no row.
- Three new e2e checks: a loaded project is framed like Fit (module
  03), an opened scene is (04), and after a real reload a click on
  Fit moves nothing (09).
- Module 09 unpacks through the right-click menu now and checks that
  the inspector offers no button.
- Full suite on melektaus-atx: 1695 checks, all pass. Smokes 24, 19
  and 21, all pass.
- No counter-probe evaluated, and nothing looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:52:49 +02:00
adminandClaude Opus 5.5 4407c02078 Keep who played an animation beside its player, not on it
d6fd000 wrote the behaviors that played an animation on the player
itself. A player is plain data, and those behaviors are entities of
the run with their parents and children. The actor smoke reads the
world as JSON and threw "Converting circular structure to JSON" as
soon as its graph had played a walk animation.

- The starters live in a WeakMap next to the player and are read
  with startersOf. The player carries no field for them any more.
- The animation system reads them from there when a pass ends. What
  a behavior hears does not change.
- Only the hook the check scripts read the world through was hit.
  The editor and a running game never serialise a player.

Tests:
- Gate on melektaus-atx: 219 files, 3400 tests, CHECK EXIT 0. It
  ran on the working tree, which also held the fix to e2e 47 and
  the kept panel sizes that are not committed yet. Seven of the
  tests belong to those.
- One new unit test: a player played by an asker that reaches back
  to its holder stays plain data, through JSON and structuredClone,
  and names the asker once. Its counter-probe bites.
- Smokes on melektaus-atx: 24, 19 and 21 checks, all pass. Before
  the fix the third stopped after 9 checks.
- e2e 6,17-26 on melektaus-atx: 480 checks, all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:15:21 +02:00
adminandClaude Opus 5.5 d6fd000160 Let a behavior hear the end of an animation it played itself
game-maker / gate (push) Successful in 3m11s
game-maker / suite (push) Failing after 12m9s
A behavior that played an animation on the parent of its owner never
heard it finish. On animation finished listened to the owner and to
what stands below it, and the parent stands above.

- Play animation notes on the player which behavior asked for it.
- On animation finished hears, as before, every animation on the
  owner or below it, and now also every animation the behavior
  played itself, on whatever object. Its output object names that
  object.
- Two behaviors that both played the same animation on the same
  object each hear its end once.
- A behavior that is called by Run behavior or heard by an On node
  counts as the behavior it sits in.
- The description of the node and the engine prompt say so.

Tests:
- Gate on melektaus-atx: 218 files, 3392 tests, CHECK EXIT 0.
- Two new unit tests: the behavior hears the animation on the parent
  of its owner while a bystander does not, and two starters hear it
  once each.
- Both counter-probes bite.
- e2e 6,17-26 on melektaus-atx: 480 checks, all pass.
- No new e2e check, and nothing was looked at in the browser. The
  rule for called and heard behaviors has no test of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:37:04 +02:00
adminandClaude Opus 5.5 7dd3ae1131 Tie two objects with a hinge, a spring, a rope or for good (#154)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Failing after 11m41s
Two bodies could not be connected: no door on a hinge, no lamp on
a rope, no wheel on an axle. This is the first step of the issue,
the four joints without a motor. The slider and the motors follow
in a second step under the same issue.

- Joint is a new asset type, placed as a child of the object it
  holds, like a collider or a rigidbody. "Add joint…" in the
  object menu makes one or places one the project has. An object
  takes as many as it needs.
- Where the joint sits is the point it holds its object at. A
  hinge and a fixed joint have that one point only. A spring and a
  rope end at a connected anchor on the other object, counted in
  that object's own units, or in the scene's without one.
- The rows follow the type: length, stiffness and damping for a
  spring, length for a rope, an angle limit for a hinge. Length 0
  takes the distance the two ends were placed at. The limit counts
  from how the two were placed.
- The connected object is an object reference in the schema of the
  type. Pasting, containers and removing treat it as they treat
  every reference since #258.
- shownFor is a new schema option: a row is shown while an enum of
  the same bag holds one of the named values.
- The object needs a rigidbody, its own or one further up. Without
  one the joint holds nothing and the console says so once.
- A connected object without a rigidbody is a moving anchor: the
  joint has a kinematic body of its own that follows it.
- A connected object destroyed while playing makes the joint let go
  with a hint. Removed in the editor the reference empties, so the
  joint holds on to the scene. The issue asked for "inactive" there.
- "Add joint" ties two objects while the game runs, from a joint
  asset as the template. Destroy on the joint lets go, there is no
  Break joint node. Destroy with keep children takes the joints
  along with the object.
- The scene view draws a line from each joint to what it holds on
  to and rings the held point. A selected spring or rope has a
  round handle on its far end, and dragging it is one undo step.
- Joints hold in Play only. The editor run simulates nothing
  (#148), and the inspector of a placed joint says so.
- Rapier counts a revolute joint's limits absolutely, not from the
  start, so the placed angle is added. Measured with a probe.
- The + menu has 25 types now and no longer fits under its button
  on a canvas 800 high. It scrolls there, and e2e 04 scrolls to
  the last entry instead of expecting it in view.

Tests:
- Gate on melektaus-atx for this state: 218 files, 3390 tests
  (3336 + 54), exit 0.
- e2e 1-16,24,39-46,51,52,57,58 on melektaus-atx: 831 checks, all
  pass. Module 41 alone: 25 checks (21 + 4).
- 54 new unit tests: 40 in joints.test.ts (rope, spring, pendulum,
  limit, fixed, the other end, the node, containers, paste), 6 for
  the line the scene view draws, 4 catalog cases of the node. The
  remaining 4 come from tests that make a case per asset type.
- Four new e2e checks in module 41: Add joint from the menu, the
  rope to the hook with its line and handle, dragging the handle
  with one undo, the bob hanging in Play next to one that falls.
- e2e 04, 40 and 46 count the new type in their lists.
- 21 of 22 unit counter-probes fail their tests. The other one is
  equivalent: Rapier ignores removing a joint that left with its
  body, the guard stays.
- Three browser counter-probes fail their checks (2, 1 and 2).
- One screenshot looked at: icon, line, handle and the rope rows.
- The e2e sum of 1691 is counted (1687 + 4, 1687 measured by CI
  on e066613). The full suite and the smokes did not run.
- Not seen in the browser: spring, hinge with limit and fixed
  joint, the node Add joint, a connected object that moves, joints
  inside a container, the released game. Unit tests cover them
  but the last. Limits under a mirrored parent are not handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:43:12 +02:00
adminandClaude Opus 5.5 e933fad446 Give a shader room to draw around the picture it wears (#69)
A shader ran only inside the rectangle of what wears it: the quad
has the size of the sprite, and outside of it no fragment runs. An
outline or a glow built with Sample texture was cut off at the
border. The issue still spoke of Filter.padding, but since #77 the
shader is no filter any more, the limit is the geometry.

- The Output node has a number Padding next to Edge: so many pixels
  of the picture per side the wearer draws beyond it. 0 is the
  default and draws as before, 256 is the most.
- The quad grows and its UVs grow with it, so the picture keeps its
  place and size. In the margin the UVs run on below 0 and above 1.
- Transparent is a fourth Edge mode: outside of the picture there
  is nothing. It is cut in the fragment (texelAt), so it needs no
  copy of the picture with an address mode of its own.
- Edge holds everywhere alike, also for Texture color, Texture
  alpha and unconnected inputs.
- Padding above 0 with Edge on Clamp puts the hint "Padding with
  Clamp smears the border" on the Output. The shader still
  compiles. A chosen Clamp and the default cannot be told apart.
- All four wearers draw the margin. A sprite counts in pixels of
  its frame, a text in its units, a particle source grows every
  particle about its centre.
- A nine-slice sprite gets a ring around its nine fields, as wide
  as so many border pixels. The fields themselves stay.
- Without a picture a pixel is one unit of the wearer's own size,
  or of the particle size.
- Particles now take the Edge mode of their shader. It had no
  effect on them before.
- An object wearing a padded shader is never culled, or its margin
  would vanish at the edge of the screen.
- The preview in the shader tab shows the margin and leaves room
  for it.
- Picking, the selection frame, the size and the collider stay with
  the rectangle without the margin.

Tests:
- Gate on melektaus-atx right before this commit: 216 files,
  3336 tests (3319 + 17), exit 0.
- e2e 28,30,31,37,41 on melektaus-atx: 178 checks, all pass.
  e2e 1-37,47,48,58 ran before the last change (no culling with
  padding): 1281 checks, all pass.
- 17 new unit tests: the compiler (padding, Transparent, the hint)
  and the geometry (quad, nine-slice ring).
- Seven new e2e checks: four at the sprite in module 28, one each
  for text, particles and nine-slice.
- 17 unit counter-probes all fail their tests. Three browser
  counter-probes fail their checks (5, 3 and 1 of them).
- Three screenshots looked at: the sprite with Clamp and margin in
  the scene, the preview in the shader tab, the text.
- The e2e sum of 1687 is counted (1680 + 7), the full suite and
  the smokes did not run.
- Not seen: the hint at the node in a picture, particles and
  nine-slice in a picture, the game view while playing, sprites
  without a picture, vector and pixel art pictures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:33:20 +02:00
adminandClaude Opus 5.5 46c620ae3b Give a behavior more ways in and out, and hear it as an event (#294)
game-maker / gate (push) Successful in 2m48s
game-maker / suite (push) Failing after 13m34s
A behavior run by another one had a single flow port at its Start
and at its End, so it could not branch for its caller. And it could
not be used as an event: a call needs a pulse at its flow input,
and the events of a called graph never fire. An input like
"Confirm" (Space or Enter) had to be built again in every graph.

- A declared port of a Start or an End node can be of the kind
  Flow. It sits above the values and shares their names, exec stays
  reserved. Only these two nodes declare one, a struct does not.
- Run behavior shows a flow port of the Start as a further flow
  input. A call arriving through it leaves the Start through the
  output of that name and not through exec.
- Run behavior shows a flow port of the End as a further flow
  output. The call goes on through the one the End was reached by
  last, and then through exec. exec fires always, as before.
- On behavior is a new event node without a flow input. The
  behavior it names runs at the node for as long as the behavior
  around it runs, with all of its own events and one set of
  variables per node.
- Its outputs are those of the End. It fires once the inner pulse
  is through, by the output the End was reached by last, exec only
  for End.exec. A pulse that never reaches the End is not seen.
- The value ports of the Start are its inputs, read anew for each
  inner pulse. The Start itself never fires there.
- It nests to any depth. Under a Run behavior it stays silent. A
  behavior hearing itself, however far round, is not started and
  the node says "A behavior cannot listen to itself".
- The palette offers every other behavior with an End once more as
  "On <name>". A node naming one without End says so.
- An On node removed while the game runs ends its behavior with On
  disable and On destroy. The sounds of the entity end only with
  the behavior placed on it.
- Renaming and deleting a declared port follows the edges at On
  behavior nodes as it does at Run behavior nodes.
- The flow outputs of the Start of a behavior placed on an object
  never fire, like its exec.
- The engine prompt of the benchmark names flow ports and On
  behavior.

Tests:
- Gate on melektaus-atx right before this commit: 215 files, 3319
  tests (3278 + 41), exit 0.
- e2e 19 on melektaus-atx: 39 checks (+4), all pass. e2e
  1-6,17-24,26,36,51: 628 checks, all pass.
- 19 unit tests in flow-ports.test.ts, 19 in on-behavior.test.ts
  on a real world, the rest in the node catalog.
- Four new e2e checks in module 19: a flow port declared by mouse
  at the End, On Confirm in the graph, the call leaving through the
  further output while playing, Enter and Space on a real keyboard.
- 19 unit counter-probes all fail their tests. One did not at first
  (a removed node kept its instance), a test was added. Two browser
  counter-probes fail their checks.
- One screenshot looked at: On Confirm with exec and pressed, the
  second flow pin at the calling node.
- The e2e sum of 1680 is counted (1676 + 4), the full suite and
  the smokes did not run.
- Not seen in the browser: flow inputs at the Start, nesting, the
  ring, values through On behavior, the palette entry. No test
  holds that the sounds of the entity go on when a heard behavior
  ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:33:34 +02:00
adminandClaude Opus 5.5 b5339b96c2 Tell a graph the frame a key went down or came up
game-maker / gate (push) Successful in 2m49s
game-maker / suite (push) Successful in 13m49s
Key is down says whether a key is held. To act once on a press, a
graph had only the events On key down and On key up. A behavior
that is called by another one and reads its input as data had to
keep the state of the last frame in a variable of its own.

- Key was pressed is true for the one frame in which the key goes
  down, the frame On key down fires in. Key was released is the
  same for the frame the key comes up.
- Both sit next to Key is down in the palette and have the key as
  their only setting.
- NodeContext gains wasKeyPressed and wasKeyReleased. The behavior
  context answers them from justPressed and justReleased of the
  input, through keyName, so a key saved as "A" meets the key "a".
- No repeat. On key down may fire again while a key stays held, a
  held key is pressed once here.
- When the window loses the keyboard, every held key counts as
  released in that frame, as On key up has it.
- The engine prompt of the benchmark names the two nodes.

Tests:
- Gate on melektaus-atx right before this commit: 213 files, 3278
  tests (3264 + 14), exit 0.
- e2e 18,20,43 on melektaus-atx: 57 checks, all pass.
- Ten unit tests in key-edges.test.ts on a real world, four cases
  in the node catalog.
- Eight counter-probes all fail their tests. One did not at first
  (the case of the key saved on the node), a test was added.
- No e2e check of its own, the sum stays 1676. The nodes were not
  looked at in the browser, neither in the palette nor with a real
  keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:59:22 +02:00
adminandClaude Opus 5.5 f02adabcf9 Show the type an arithmetic shader node really puts out (#219)
The result of Add, Multiply, Mix and the other arithmetic nodes of
a shader graph was drawn as any, whatever went in. The compiler
already lifts a colour on any side to a colour result, so the pin
said less than the code knew. A colour result could also be wired
into a number input, and the shader then failed to compile.

- liftedType in core/shader/nodes.ts is the one rule. The compiler
  reads it when it lifts two operands, and the editor reads it
  through liftedPortType for the ports it draws.
- A SharedPortGroup may bring its own rule as agree. Without one
  it is agreedType, as before. The thirteen arithmetic nodes share
  their type by liftedPortType.
- Only the any inputs decide. An input nothing feeds counts as the
  number typed into it. t, edge, exponent, min and max have no say.
- The type travels through a chain of such nodes without code of
  its own, because #210 hands a shared type on.
- In a ring the result stays any, unless a colour has settled it.
- A number may feed a colour input. Without that a number result
  no longer fitted Output.color, although the compiler spreads it
  over three channels. The rule sits in portsCompatible, but only
  the shader graph has colour ports.
- A colour result no longer fits a number input. An edge laid
  before the colour came in stays and is drawn in the danger
  colour, and the node at its end names the problem.
- The nine shaders in the stored projects (108 edges) all stay
  valid under the new rule.

Tests:
- Gate on melektaus-atx: 212 files, 3260 tests (3240 + 20), exit 0.
  The gate right before this commit ran on the tree of this and
  the next commit together, with the same numbers.
- e2e 28 on melektaus-atx: 81 checks, all pass.
- e2e 1-31: 1059 pass, 1 fail in module 18 (the editor clock read
  1.97 s after a stop). A rerun of 1-18 passed with 602 checks and
  0.18 s. 1-31 was not run in full again.
- No full run and no smokes. The sum of 1676 (1673 + 3) is reckoned.
- Twenty unit tests in shader-port-types.test.ts, one of them runs
  all thirteen nodes through every mix of number and colour and
  compares what is drawn with the code made.
- Two tests in shaders.test.ts now lay the edge first and bring the
  colour in after, the only order that still wires such a graph.
- Three e2e checks in module 28 with the real mouse: a colour into
  Multiply, the red edge with its message, and the colour result
  refused at alpha and taken at color.
- Fifteen unit counter-probes and two browser counter-probes all
  fail their tests.
- Not checked in the browser: the one-input nodes, Mix and the
  ring, and a number wired straight into a colour input. Nothing
  was looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:50:50 +02:00
adminandClaude Opus 5.5 9ca220a285 Hand a shared port type on through a chain of nodes (#210)
game-maker / gate (push) Successful in 2m51s
game-maker / suite (push) Successful in 14m44s
Since #207 the output of First not empty or Select takes the type
its inputs agree on. A second node of that kind behind the first
stayed any, because the first one answered with the any it was
registered with. That was the guard against a circle of such nodes
asking each other forever. The issue asks for the type to travel
through the chain. Answered there: measure with 50 nodes and build
no cache, and the painted input pins follow along.

- shapedDef in core/graph/nested.ts takes a Trail in place of the
  flag inherit. Its set open holds the nodes still being asked. A
  node reached a second time reports the any it was registered
  with, which ends a circle.
- The map done of the Trail keeps each answer of one question. A
  node feeding both inputs of the next is asked once, so the work
  does not double with every link of a chain.
- ownShape is the part of shapedDef that builds the ports before a
  shared group has its say. It is split off, nothing in it changed.
- Inputs stay any and only outputs inherit, as in #207. An edge
  that turns invalid further down is left in place and drawn in the
  danger colour.
- paintedInputs uses one Trail for all pins of a node, so the pins
  show the type handed on through the chain.
- Nothing is cached between two changes. A chain of 50 resolved in
  about 10 ms where it took 2 ms before, measured earlier in this
  round and not again for this commit.

Tests:
- Gate on melektaus-atx: 211 files, 3240 tests (3227 + 13), exit 0
- e2e 1-6 on melektaus-atx: 298 checks, all pass
- e2e 4,6,18,19,20,23,24,26,28,31,36,43,53: 558 checks, all pass
- No full run and no smokes. The sum of 1673 (1671 + 2) is reckoned,
  1671 is what CI counted for c88f57c.
- Fourteen unit tests in shared-port-type.test.ts: a chain of two
  and of three, mixed kinds, a mixed and an open input, undo at the
  front, a circle of two, a node feeding itself, behind a circle, an
  invalid edge further down, the chain of 40 asked once per node,
  the chain of 50, and two for the pins. The test saying the type
  does not pass through a second node is gone, hence 13 more.
- Two e2e checks in module 06: the second node takes number, and a
  text at the front opens the whole chain again.
- Four unit counter-probes and one browser counter-probe all fail
  their tests. With the old behaviour only the chain check fails.
- Not checked in the browser: the circle and the chain of three,
  which have unit tests only. Nothing was looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:41:30 +02:00
adminandClaude Opus 5.5 c88f57cf0a Let a project choose how its game link is zoomed (#279)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 14m31s
The page of a released game always drew the camera at Auto, the
largest whole step that fits. A camera of 300x180 in a window of
1280x800 stands at step 4 and leaves a border left and right. The
issue asks for Fit next to it. Answered there: a setting of its own
rather than the scale of the Game tab, and no switch for the player.

- The project has a field gameLinkZoom, 'auto' or 'fit'. A document
  without it reads as 'auto', so nothing changes for a project that
  is there already. No format bump.
- Project settings has a row "Link zoom" with a menu of Auto and
  Fit. It is an undo step of its own, SetGameLinkZoomCommand.
- GameScreen hands the field to cameraView where it passed a fixed
  'auto'.
- gameScale, the scale of the Game tab, is not touched and does not
  reach the link.
- The released copy is frozen, so the change reaches the link only
  with "Update release".

Decided here:

- The row is called "Link zoom", not "Game link zoom" as proposed.
  The longer label is wider than the label column of the dialog and
  pushed its button 12 px out of line with the others.
- The undo step keeps the name "Game link zoom", which is what the
  tooltip of the undo button shows.
- A note under the row says "The released game at the largest whole
  step that fits, or filling the window."

Tests:
- Gate on melektaus-atx: 211 files, 3227 tests (3219 + 8), exit 0
- e2e 60 on melektaus-atx: 19 checks, all pass
- e2e 35,46,59,60: 74 checks, all pass
- No full run and no smokes. The sum of 1671 (1668 + 3) is reckoned,
  1668 is what CI counted for 9e869df.
- Eight unit tests: the default, saving and loading, values that
  are not a choice, undo, gameScale left alone, the factor of Fit
  for a window larger and smaller than the camera, and the rule
  behind Apply.
- Three e2e checks in block 60i of module 60, with the real mouse:
  Auto to Fit in the dialog, update the release, open the link
  without a session and measure scale, frame and pixels. Fit reads
  4.267 where Auto reads 4.
- Nine unit counter-probes and three browser counter-probes all fail
  their tests.
- Not checked in the browser: Fit in a window smaller than the
  camera, which has a unit test, and Fit under a display scale above
  100 %. Nothing was looked at after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:53:10 +02:00
adminandClaude Opus 5.5 345289982b Scale, turn and mirror the shapes of a vector drawing (#291)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 14m25s
A shape could only be moved and reshaped point by point. Whoever
wanted it larger or turned pulled every point alone, and for several
shapes or a group there was no way at all. The answers in the issue
settle the rest: the change is baked into the points, nothing of it
is remembered, and the line width stays.

- With Select the selection stands in a frame of eight squares and a
  knob, drawn like the one of the scene view. It holds one shape,
  several shapes or a group, the same selection that is dragged as
  one.
- A corner square scales from the opposite corner, a square on an
  edge in its own direction. Shift keeps the proportions, Alt scales
  about the middle.
- The knob turns round the middle of the frame, with Shift in steps
  of 15 degrees. After the pull the frame stands upright again.
- The inspector has a band TRANSFORM with X, Y, Width, Height and
  Rotate by, and the buttons Flip horizontal and Flip vertical.
- Points, handles, gradients and texture tiles are taken through the
  same matrix. The rule is a pure function in vectorTransform.ts,
  transformShapesCommand makes one undo step of any number of
  shapes.
- A press goes to a point or a handle first, then to the frame, then
  to a line. shapeFrame.ts holds where the handles are and what a
  pull of each means.

Decided here:

- The frame stands 15 pixels off the shapes. The points of a shape
  lie on its bounds, so a square on the bounds would cover them.
- The knob only turns. In the scene view it scales as well.
- A side cannot be pulled through the opposite one, the frame keeps
  at least 1 pixel.
- Coordinates are rounded to a thousandth of a pixel, so a quarter
  turn is exact.
- A texture tile stays upright under a turn and grows to the bounds
  of the turned tile. The bands of a gradient stay square to its
  axis under an uneven scale.
- The switch Scale stroke of the proposal is not built.

Found on the way:

- The inspector of a shape is now taller than the window. Two checks
  of module 47 no longer found their row or button on the screen and
  use the new hook inspectorButton, which scrolls there first.
- Not solved: with a shape at the top edge of the view the knob lies
  outside the tab. The wiki names it under the limits.

Taken over from #217: the e2e check for two selected shapes without
a group, and unit tests for the snap of the common offset and for
the gradients of the shapes dragged along.

Tests:
- Gate on melektaus-atx: 209 files, 3176 tests (3121 + 55), exit 0
- e2e 47,48 on melektaus-atx: 112 checks, all pass
- No full run and no smokes. The sum of 1664 (1660 + 4) is reckoned,
  1660 is what CI counted for 6d79cf5.
- 55 unit tests: 25 in tests/core/vector-transform.test.ts and 30 in
  tests/editor/shape-frame.test.ts
- Four e2e checks in block 47k of module 47: pull a corner, turn
  with Shift, type width and rotation, drag two shapes without a
  group
- 47 unit counter-probes and four browser counter-probes all fail
  their tests. Four of the unit ones did not at first: two showed
  dead code, which is gone, two got tests.
- Not checked in the browser: the edge squares, Alt, the cursors,
  the flip buttons and the rows X, Y and Height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:20:55 +02:00
adminandClaude Opus 5.5 275e50af01 Let a tile report a touch instead of blocking it (#160)
A tile could be solid, one-way and scaled, but not report without
blocking. Water, lava, spikes or a goal zone needed a trigger object
of their own laid over the map. Now a tile has a switch Trigger, and
the collision events name the tile and the cell they met.

- A tile has the switch "Trigger (reports, never blocks)" under
  Solid. Its cells hold nothing up: no Move, no falling body, no
  ray. They raise On collision enter, stay and exit with the tilemap
  as other.
- The three events have the outputs tile, cell x and cell y. Without
  a tile, tile is empty and the cell is -1, -1.
- The three events have the input per cell. Off, the default, gives
  one event per area: cells of the same tile that touch side by
  side. On gives one event per cell.
- The scene view outlines trigger cells in yellow like a trigger
  collider, merged to rectangles but never with solid cells.
- Is touching with the map counts trigger tiles, and Set tile works
  while the game runs.
- Both systems find the touches with the same overlap check in
  runtime/systems/tileTriggers.ts, not through the physics engine.

Decided here:

- Trigger wins over Solid. A tile with both does not block, as with
  a collider.
- Water beside lava is two areas, and so are two ponds that touch
  only at a corner.
- An area names the cell nearest to the centre of the collider, and
  on exit the cell it named last.
- The solid cells of a map still do not name their tile.
- Normal, impulse and point are 0, as with a trigger collider.
- Collision scale and outlines shape a trigger cell like a solid
  one. One-way, friction and bounciness have no effect.

Tests:
- Gate on stein2: 205 files, 3104 tests (3067 + 37), exit 0
- e2e 39,40: 106 checks. e2e 1-8,39-43: 490 checks. Both on
  melektaus-atx. No full run, the sum of 1648 (1645 + 3) is
  reckoned and CI measures it.
- 37 unit tests in tests/core/tile-triggers.test.ts, nearly all
  against both systems
- Three e2e checks with a real mouse in block 40l-iv
- 24 unit counter-probes fail tests. Two did not at first, tests
  were added for them.
- Two browser counter-probes fail their checks
- Two screenshots looked at (yellow outline, the node with per cell)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:28:39 +02:00