Files
feuerwerk/tests/editor/beads.test.ts
T
rootandClaude Opus 5 656d61f187 Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The
running game reads the graph by reference each frame, so a new node, edge,
changed value, disconnect or undo takes effect from the next frame. Stop
discards these edits like everything else changed in play. A new On start
node does not fire for a behavior that has already started.

During play every data output shows the value it last produced, in the
highlight colour and refreshed ten times a second. Values are kept per
graph, because node ids are only unique within one; with several
instances the most recent value wins. The Variables node reads the live
variable bag, so only its own outputs are kept, as a snapshot.

Each time an exec edge fires, a bead runs along it in 0.3 s, at most one
every 0.15 s per edge, following the same curve the edge is drawn with.
Beads run on game time and freeze while paused; the game logic is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:19:07 +02:00

66 lines
2.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
BEAD_SPACING,
BEAD_TRAVEL,
beadsInFlight,
curvePoint,
launchBead,
} from '../../src/editor/graph/beads';
describe('beads on a firing edge', () => {
it('a single fire sends one bead from the source to the target and then it is gone', () => {
const times: number[] = [];
launchBead(times, 2);
expect(beadsInFlight(times, 2)).toEqual([0]);
expect(beadsInFlight(times, 2 + BEAD_TRAVEL / 2)[0]).toBeCloseTo(0.5);
expect(beadsInFlight(times, 2 + BEAD_TRAVEL)).toHaveLength(1);
expect(beadsInFlight(times, 2 + BEAD_TRAVEL + 0.01)).toEqual([]);
});
it('a fire closer than the spacing to the last bead sends none', () => {
const times: number[] = [];
launchBead(times, 1);
launchBead(times, 1 + BEAD_SPACING / 2);
expect(times).toEqual([1]);
launchBead(times, 1 + BEAD_SPACING);
expect(times).toEqual([1, 1 + BEAD_SPACING]);
});
it('an edge firing every frame shows an evenly spaced chain that does not pile up', () => {
const times: number[] = [];
let now = 0;
for (let frame = 0; frame < 600; frame++) {
now = frame / 60;
launchBead(times, now);
}
const flying = beadsInFlight(times, now);
expect(flying.length).toBeGreaterThanOrEqual(2);
expect(times.length).toBeLessThanOrEqual(Math.ceil(BEAD_TRAVEL / BEAD_SPACING) + 1);
for (let i = 1; i < flying.length; i++)
expect(flying[i - 1]! - flying[i]!).toBeGreaterThanOrEqual(BEAD_SPACING / BEAD_TRAVEL - 1e-9);
});
});
describe('the curve a bead follows', () => {
it('starts in the source port and ends in the target port', () => {
expect(curvePoint(10, 20, 300, 80, 0)).toEqual({ x: 10, y: 20 });
expect(curvePoint(10, 20, 300, 80, 1)).toEqual({ x: 300, y: 80 });
});
it('passes the middle of a curve half way, since the curve is symmetric', () => {
const mid = curvePoint(0, 0, 200, 100, 0.5);
expect(mid.x).toBeCloseTo(100);
expect(mid.y).toBeCloseTo(50);
});
it('bows out to the right between two ports stacked above each other, as the drawn curve does', () => {
expect(curvePoint(0, 0, 0, 100, 0.25).x).toBeCloseTo(11.25);
});
it('leaves its port horizontally, as the drawn curve does', () => {
const near = curvePoint(0, 0, 200, 100, 0.01);
expect(near.x).toBeGreaterThan(0);
expect(Math.abs(near.y / near.x)).toBeLessThan(0.05);
});
});