ed2b58b3c04d1c97ff673d27076abc5ae0414a3c
495
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
c7e9769f90 |
Change the sound FX parameters of a sound while it plays #315
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> |
||
|
|
072537b2e8 |
Blend two ways of a sound FX with a Mix node #316
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> |
||
|
|
790e483198 |
Pitch a sound FX without changing its length #312 #313 #314
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> |
||
|
|
45e45fb1df |
Count the Sound FX type in the + menu check
The suite run on |
||
|
|
183f356c99 |
Let a game hand values to the parameters of a sound FX #311
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> |
||
|
|
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>
|
||
|
|
93c0a80b7d |
Keep a grip from laying out before it has a size
The gate of
|
||
|
|
a5184b3368 |
Let the rows of a Variables node be dragged into a new order
The Variables and Properties nodes listed their picked rows in the inspector, but unlike Start, End and struct rows they could not be reordered. The drag dots also sat right on the edge of the row and touched the kind mark. - buildPickedPortSection gathers each row with its warning line as one block and hands the blocks to sortBlocks. A move reorders the ports on the node and is one undo step. Properties uses the same section and gains the same reordering. - Grip draws the dots 3 px in from the left edge and moves the kind mark 3 px clear of them (DOTS_PAD). The column keeps its width, so the name fields do not shift. Rows without dots are unchanged. Tests: - block-drag, reorder, variable-choices: 3 files, 13 tests, all green. block-drag now expects the mark 11 px in with dots, 0 without. - e2e 23 on melektaus-atx: ALL PASSED, 32 checks (+3 for dragging vy before vx and undoing it). It ran before the padding change, which touches no checks there. - The gate did not run before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b1796baac |
Let a container animation reach alike-named objects in another one
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> |
||
|
|
2da07599d8 |
Let Get position give only the local x and y
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> |
||
|
|
526b1da7f4 |
Add a Get world position node with just the world x and y
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
3f12a77aea |
Preview a sprite at the end of its inspector, placed or as an asset
A sprite's inspector listed its rows but never showed what they add up to. The picture, its frame, nine-slice, tint and shader could only be judged in the scene, often far from the object being edited. - SpritePreview draws the sprite through syncSpriteMesh, the function the scene draws it with. Slices, padding, tint and shader come out the same without a second path. syncSpriteMesh now takes only the three fields of a DrawItem it reads. - The subject is resolved by previewPicture from the Shader tab, so a placed sprite shows its own overrides. - previewFit is the rule for size and place: whole device pixels per unit from 1 on, at most 8, and the corner on a whole device pixel. Pixel art stays sharp. A sprite larger than the group shrinks freely. - A shader with a Time node keeps the preview redrawing once a frame while it is on screen. A folded group or a hidden panel stops it. - New test hook __gm.inspectorSpritePreview(). Finding: the preview redrew on a model change in a microtask at first. A drawn picture whose revision moved on is retried once, in a microtask queued by the scene's synchronous redraw. That retry ran before the preview had let go, and the old pixel art sheet stayed waiting (e2e 48l-iii read waiting [1,1,1]). The preview now redraws at once on a model change, as the scene does. Tests: - unit: 238 files, 3592 tests (+5 in preview-fit.test.ts), and the group list of a placed sprite in sections.test.ts ends in PREVIEW - e2e 48: ALL PASSED, 47 checks (+2: 48l-i asset preview with the frame, 48l-iii placed preview with its shader values) - counter-checks: without whole-pixel zoom 2 unit tests fail; with the microtask redraw 48l-iii fails with waiting [1,1,1] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1665576fb9 |
Let a pixel perfect camera draw on the game view's pixel grid
A camera that follows smoothly stands between pixels most of the time, and pixel art then wanders between them. The new camera switch draws what the camera shows on the game view's own grid, while positions in the model keep their fractions. - `Pixel perfect` is a camera row, off by default. It works at every fixed game scale and at Auto, but not at Fit and not in the Scene tab, where one works with the fractions. - The grid has one step per device pixel of a world unit. At 100% that is whole units, at 500% fifths. The view carries it (`View.grid`), so no view draws with the grid of another. - The view looks from a point that puts the world's origin on a whole device pixel (`lookFrom`). This also covers odd camera sizes and views that cut the picture to an odd size. - Sprites, texts and tilemaps snap their top left corner when they are unturned, so an odd size lands on whole pixels too. A turned object snaps its pivot (`snapPlacement` in `pixelGrid.ts`). - Under a camera shader the picture is taken the same way, and the objects in it use the picture's grid. With Camera pixels that is one step per camera pixel. - An overlay camera has its own switch and snaps only while the main picture is at a fixed scale. - Particles are drawn where they are. - New test hook `__gm.gameDrawnRect(id)` reads where a sprite's picture is drawn, which `gameRect` (the model's) cannot tell. Found: the e2e fixture at 300% drew the crate at y 205.5 device pixels without moving the camera, because the view cut to an odd height puts its centre on half a pixel. With the switch on it is at 206. Tests: - Gate: 237 files, 3587 tests (+10 in pixel-grid.test.ts) - e2e 2,3: ALL PASSED, 75 checks (+1, 3j) - Counter-checks: without the origin snap 3 unit tests fail, with the centre snapped instead of the corner 2 fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
92395f6773 |
Find the render layer log pair in e2e 63c3 instead of the tail (#305)
The suite went red three times in a row (tasks 488, 491, 492) on one check only: 63c3 read `named: ["",""]` while the picture half passed. Alone the module was green. In the full run objects left by earlier modules are still in the scene, and their behaviors log two empty lines after the OnStart of the check. `log.slice(-2)` took those. - The check now searches the log for the pair "UI", "1". Both lines come from one exec chain, so they stand next to each other. - Without the pair it shows the last 6 lines for diagnosis. - Not found out: which earlier module writes the empty lines. Tests: - e2e 63 on melektaus-atx with a temporary behavior that logs two empty lines after the switch: ALL PASSED, 13 checks. - The same with the old `slice(-2)`: 1 FAILED, `named: ["",""]`, the picture from CI. The disturbing behavior is removed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f57cf731d |
Keep a pixel frame worn through a shader alive while it is drawn
Changing a palette colour of pixel art flooded the console with Pixi warnings: "[BindGroup] a 'textureSource' was destroyed while still bound to a shader", and the same for its sampler. #293 had stopped this for a drawing, but a frame of pixel art is worn as a cut of the sheet. The cut has a source of its own, so the sheet looked unbound, was freed at once and took the cut with it under the shader. - The check whether a replaced picture is still in use gets its key too. TextureLibrary then asks the picture and every cut taken from it. - A sheet with a bound cut waits like a bound picture did since #293, and both go together once the shader has taken the new frame. Found on the way: - The project the report came from has a sprite wearing a frame of an eleven-frame pixel art through a shader asset. - A nine-sliced sprite wearing the frame showed no warning, even without the change. The first try of the e2e check used it. Tests: - Gate on melektaus-atx: 236 files, 3577 tests (+1) - e2e 48: 45 checks (+1, sum 1741). 48l-iii puts a shader on the object wearing the second frame, changes a palette colour three times and counts the warnings. - Counter-checks bite: without the cuts in the check the unit test fails, and 48l-iii counts 6 warnings for 3 changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c74b6b1cab |
Show where an inspector row can be dragged and answer the pointer
After #310 rows could be dragged by their mark, but nothing said which marks take a drag, and the pointer on one changed nothing but the cursor. - A mark that drags is now a Grip: the same widget wrapped, the same size, so the value column stays where it was. Once sortBlocks takes it, it carries six faint dots at its left edge. - Marks that cannot drag stay plain: the variables of a placed object and the fields of a typed struct show no dots. - With the pointer on a grip its dots light up and the whole block that would move is washed in the accent colour, a struct with its fields. While the block is carried the wash gives way to the dimming, and the grip stays lit. - While a block is carried the other grips let the pointer pass: no lit dots, no wash. Every grip of the column shows grabbing instead of grab until the drop. - The wash first used controlHover like list rows. In the inspector it is darker than the panel and vanished next to the dark fields, so it became the accent at 15 percent, the colour of the drop line. - __gm.inspectorMark also reports the dots: none, idle or lit. Tests: - Gate on melektaus-atx: 236 files, 3576 tests (+4 in block-drag.test.ts, one of them a drag passing over another grip). - e2e 22: 40 checks (+1, sum 1740). 22f-iii reads idle dots, moves the mouse onto the mark and reads them lit. - Two counter-checks bite: no wash on hover fails 1 test, grips never made grabbable fail 3. A third: hover ignoring the drag fails 1. - Screenshots of a lit single row and a lit struct block looked at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2c19c89878 |
Let variables, fields, ports and list items be dragged into order (#310)
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> |
||
|
|
cf6043a066 |
Show a wheel step or Fit as a short zoom transition (#309)
The wheel and the Fit button used to jump to the new view. Now the view takes a short way there, 200 ms by default, everywhere the editor zooms. The zoom steps stay the same, only the change is shown. - viewTween: tweenView is the way between two views as a pure function. The scale moves by equal ratios, and the world point that sits at the same place on screen in both views stays there. After a wheel step that is the point under the pointer. - ViewTween drives it on the ticker. A surface hands it reading, writing and drawing its own view and wraps its wheel and Fit code in change(). A step during a transition starts from where that one was heading, so quick steps add up to exactly as many steps. - Anything else that moves the view ends the transition. A pan jumps to the target first, and saving a view saves the target. - Wired into FreeViewport (Scene, Container, animation preview), SheetCanvas (Texture, Curve, Pixel art, Vector art), GraphCanvas, panAndZoom (Tile board, plugin node board) and the timeline. - Menu > General settings... sets the time in ms, 0 jumps at once. It is kept in this browser like the panel sizes, not in the project, and it is not an undo step. - A document that opens still comes up framed at once. The animation preview frames a new subject itself instead of calling fit(). - The pixel art readout names the step the view is heading for. Measured in the scene view at 200 ms on melektaus-atx, where every frame of the transition is a full redraw: - 500 sprites: redraw 6.7 ms, 16 frames, mean 18 ms - 1000 sprites: redraw 13.0 ms, 9 frames, mean 27 ms - 2000 sprites: redraw 32.2 ms, 5 frames, mean 58 ms Tests: - The harness sets 0 ms on every load, so the existing modules read the step and not the way. - e2e 46i samples one wheel step at 1000 ms a frame later: zoom 1.033 between 1 and 2, the point under the pointer kept at (134, -40). It lands on the view a 0 ms jump reaches. - e2e 46 and 49 expect the new menu entry. - Gate (atx): 234 files, 3563 tests, CHECK EXIT 0 (+15) - e2e 46,49 (atx): ALL PASSED, 43 checks (+3, sum 1735) - Three counter-checks fail view-tween.test.ts as expected. e2e 46i fails when the scene view does not carry its centre along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d9c6bbfa4d |
Add a node that names the render layer of an object (#305)
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> |
||
|
|
70f3608733 |
Let Mouse position and Is hovered see through any camera (#304)
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
|
||
|
|
1d0fa7b947 |
Let the game view zoom up to 20x
A small camera on a large screen stopped at 6x. The steps now go on in whole numbers up to 20, both in the zoom menu of the Game tab and for Auto, which takes the largest step that fits whole. - GAME_SCALE_STEPS in playSettings.ts runs 0.25, 0.5, 1 to 20. Prettier wraps the longer line. - e2e 60 keeps its own copy of the steps to work out what Auto should pick. The copy follows the new list. Tests: - view.test.ts: 23 passed (every step is used as its own factor) - e2e 60: ALL PASSED - 19 checks (Auto still picks 4 at 1280x800) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a18966bfe5 |
Show the current editor in the README screenshot
The picture from
|
||
|
|
b895ae99c0 |
Show the graph in the e2e 6 screenshot of the long name (#306)
The check for the long animation name ran after a play and stop, which leave the editor on the scene tab. Its screenshot showed the scene, not the node. The check itself read the row through the test hook and was not affected. - The step switches back to the graph tab before it renames the animation. Tests: - e2e 6: all passed, 149 checks. The screenshot shows the node, with "animation Swi…" ending before a whole "progress". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2aa635f453 |
Keep an output's name readable beside a long inline value (#306)
An input and an output in the same port row were cut to fit each on its own. The input text could reach x = 123 and the output label could start at x = 83, so a long inline value ran under the output's name. At Animation playing a long animation name covered "progress". - The output label is placed first and keeps its room. The input text is cut with an ellipsis 6 px before it, as decided in the issue. - The rule is a pure function in src/editor/graph/portRow.ts. A row without an output keeps the input's old limit of 108 px. - The value a run shows beside an output uses the same rule. Its left edge can no longer lie right of its right edge. - New test hook __gm.graph.rowSpans(nodeId) gives each row's input and output text as cut and placed, in node units. - NODE_WIDTH stays as it is, as decided in the issue. Tests: - Gate: 230 files, 3520 tests (6 new in port-row.test.ts). - e2e 6: all passed, 149 checks (148 + 1). - Counter-check: with the rule switched off, the input text reaches x = 121.4 under "progress" at x = 114.1, and the new check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a40e629fde |
Make e2e 56 pass on its own, not only after module 55 (#307)
Check 56a-ii clicks the read-only value of a private variable and types into it. A read-only field takes no focus, so typeText found no input and threw when the module ran alone. In the full run the keystroke went into whatever input an earlier module had left focused, and the check was green by luck. - New harness helper typeBlind sends the keystrokes without checking where they land. It is meant for keystrokes that should be refused. - 56a-ii now also checks that no field holds the focus after the click (__gm.focusedRect() is null), the same way module 28 checks its read-only Asset row. - No other module types into a field that is meant to be locked. Tests: - e2e 56 alone: ALL PASSED, 10 checks (was E2E EXIT 1 before) - e2e 55,56: ALL PASSED, 27 checks - Counter-check with the second lock click left out: 56a-ii fails with a focused field and timer 9, three later checks follow - Gate: 229 files, 3514 tests, CHECK EXIT 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
808e1eefc2 |
Clean up duplicates and dead code, and fix the + menu check (#85)
Refactoring round 9 covers everything since round 8 ( |
||
|
|
a526e1b40f |
Use a shader graph as a node in other shaders (#303)
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> |
||
|
|
2e55071fa5 |
Lay a shader over a camera's whole picture (#299)
A shader so far worked only on its own wearer. Effects over the whole game picture, such as a vignette, grey, scanlines or a red flash on a hit, had no place to live. A camera now wears one, drawn the way a field shader of a particle source is (#170). - The camera gets a Shader row and, while one is picked, a Shader resolution row. The shader's properties follow as camera rows. They load, animate and appear in a Properties node under Type Camera. - Every view keeps masks and objects in one container. CameraShade renders it into the camera's picture, cleared to the background, and draws the shader over the frame. Game tab and play.html take the container off the stage while the picture stands in for it. - Screen pixels gives the picture as many pixels as it covers on screen. Camera pixels uses View width x View height and scales up without smoothing. - A pixel counts as a camera pixel in both (the user's choice). The uniform uPixelScale makes Texture size read View width x View height, so a Blur keeps its reach at any game scale. - Each camera wears its own shader, overlay cameras included. A red flash on the game camera leaves the HUD of an overlay camera alone. - The scene view's eye has a Shader switch on the Camera row, off by default. With it on, the main camera's shader lies over its frame, below symbols and gizmos, and the world around it stays drawn. - New tool tools/perf-camera-shader.mjs measures the cost. Measured on stein2 (HD 630, Game tab at Fit 1310x679): a camera shader that hands the picture through stays inside the scene's own spread of 3 to 5 fps between runs. A Blur of 4 cost 4 to 8 fps. Tests: - Gate on melektaus-atx: 227 files, 3497 tests (3487 + 10). - e2e 63 alone: 11 checks (6 + 5), e2e sum 1725 counted. - 63e-63i: pass-through is byte-equal and upright at 100%, flat red reaches the corners while the overlay keeps its green, Texture size reads 640 x 360 in both resolutions at Fit, no shader draws as before, and the scene view shows it only with the switch on. - Counter-checks: a camera left out of the shader wearers fails 1 unit test. Counting screen pixels fails 63g with #ab6000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2326085ed |
Show each camera only its layers and lay overlay cameras on top (#302)
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> |
||
|
|
f03ca0d382 |
Draw a shader in passes with a Pass node (#301)
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> |
||
|
|
df61b08bd1 |
Soften a shader's picture in one pass with a Blur node (#300)
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> |
||
|
|
d6d38a6422 |
Lay a shader over a particle source's whole field (#170)
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> |
||
|
|
bf27e42e8a |
Let a shader declare a color the sprite sets (#298)
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> |
||
|
|
4f4c149211 |
Duplicate an asset from its right-click menu (#297)
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>
|
||
|
|
0dda6a012a |
Walk an object to a point, and measure distance in the world (#296)
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> |
||
|
|
f1e3bd8378 |
Let a joint glide on a rail and drive it with a motor (#154)
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ca5b2db449 |
Keep where each divider was dragged to across a reload
A panel dragged wider was back at its first size after every reload. The sizes are now kept in the browser, next to the folded groups. - editor/panelSizes.ts holds one entry in localStorage, game-maker.panel-sizes: a name per divider and its width, height or share. - A panel reads its size once when it is built, held to the bounds its divider keeps today. Every drag writes the new size. - Kept are the columns left and right of the centre, the console, the split of the left column, the tilemap panel, the preview of the Shader, Particles and Pixel art tabs, each under its own name, and the split of the user management. - The sizes belong to the browser, not to the project. They hold for every project opened there. - A browser that refuses the storage, or holds rubbish under the entry, starts with the sizes the editor comes with. Tests: - Gate on melektaus-atx: 219 files, 3400 tests, CHECK EXIT 0. - Seven new unit tests in tests/editor/panel-sizes.test.ts. All five counter-probes bite. - Smokes on melektaus-atx: 24, 19 and 21, all pass. - One new e2e check in module 48: the left column dragged 40 px wider is as wide after a reload, and dragged back after. - Full suite on melektaus-atx: 1692 checks, all pass. It ran before the fix to the animation players of the commit before last. - Browser counter-probe: without the write to the browser exactly that check fails. - Only the left column is measured across a reload. The other dividers go the same way and have no check of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8f9d373d7b |
Scroll the + menu to Vector art before picking it in e2e 47
The suite to
|
||
|
|
4407c02078 |
Keep who played an animation beside its player, not on it
|
||
|
|
d6fd000160 |
Let a behavior hear the end of an animation it played itself
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> |
||
|
|
7dd3ae1131 |
Tie two objects with a hinge, a spring, a rope or for good (#154)
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
|
||
|
|
e066613583 |
Watch a held body for a time, not for a count of frames
The suite to |