Which of two events ran first depended on hidden lists: the order of the behavior instances and the order in which nodes were created. A graph had no way to say that one On update must run before another, and an On update of one behavior could run before a key event of another. - A frame now runs event kind by event kind over all behaviors: On disable and On enable, key down, key up, key held, timer, collision enter, stay and exit, animation finished, click, mouse wheel and at last On update. - All 17 event nodes have a field Order, a whole number, default 0, negative allowed. Within one kind a lower Order runs earlier, no matter which behavior holds the node. - On a tie the current hierarchy decides from top to bottom, then the order in which the nodes were created. The old rule that a behavior enabled later goes to the end is gone. - The node shows Order: 5 only when the value is not 0, so existing graphs look the same. - The rule lives as pure functions in core/graph/eventOrder.ts. The interpreter can now fire a single event node. Changes in existing projects: - An object destroyed in a frame gets no further event in that frame. On disable and On destroy still come at the end of the frame. - New instances get all their On start first, then all On enable. Teardown runs all On disable first, then all On destroy. - State events follow right after the event kind that caused them. - Four unit tests had fixed the old order and are adjusted: two in behavior-events, one in physics-system, one in key-repeat. Tests: - ./check.sh: 191 files, 2784 tests, CHECK EXIT 0. - GM_STEPS=1-6: ALL PASSED, 280 checks. - Full e2e run (local): ALL PASSED, 1551 checks, measured. - tests/core/event-order.test.ts: 11 tests for the rule, the field on all 17 nodes, order across behaviors, key before update, two nodes in one graph, On start, hierarchy after late enable and reorder at runtime, state events, save and load. - e2e 6z-viii, 2 checks with real mouse and keyboard: Order -1 set in the inspector shows on the node, and a run logs B, A although B is lower in the hierarchy. - Counter-checks: 5 in unit tests and 1 in the browser, all fail as expected. The originals were restored and checked with cmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
/**
|
||
* The repeat clock behind `On key down`, checked without a world: the cases that are awkward to
|
||
* stage in one (a key already held when tracking starts, a hostile interval) live here.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { nodeDef } from '../../src/core/graph';
|
||
import { MIN_REPEAT_INTERVAL, stepKeyRepeat } from '../../src/core/graph/keyRepeat';
|
||
import type { PropertyBag } from '../../src/core/model/Properties';
|
||
import { formatNumber } from '../../src/ui/math';
|
||
|
||
const held = { pressed: false, down: true, dt: 0.1, after: 0.2, interval: 0.2 };
|
||
|
||
describe('key repeat', () => {
|
||
it('starts the clock on the press, whatever the frame was worth', () => {
|
||
const step = stepKeyRepeat(null, { ...held, pressed: true, dt: 5 });
|
||
expect(step.fires).toBe(1);
|
||
expect(step.state).toEqual({ held: 0, repeats: 0 });
|
||
});
|
||
|
||
it('forgets the clock when the key comes up', () => {
|
||
expect(stepKeyRepeat({ held: 9, repeats: 3 }, { ...held, down: false })).toEqual({
|
||
state: null,
|
||
fires: 0,
|
||
});
|
||
});
|
||
|
||
it('stays quiet for a key that was already held when it started watching', () => {
|
||
// No state means the press was never seen – repeating without a first fire would be a lie.
|
||
expect(stepKeyRepeat(null, held)).toEqual({ state: null, fires: 0 });
|
||
});
|
||
|
||
it('never repeats while the delay is zero, however long the key is held', () => {
|
||
let state = { held: 0, repeats: 0 };
|
||
for (let n = 0; n < 100; n++) {
|
||
const step = stepKeyRepeat(state, { ...held, after: 0, dt: 1 });
|
||
expect(step.fires).toBe(0);
|
||
state = step.state!;
|
||
}
|
||
expect(state.held).toBe(100);
|
||
});
|
||
|
||
it('treats a negative delay as no delay at all', () => {
|
||
expect(stepKeyRepeat({ held: 5, repeats: 0 }, { ...held, after: -1 }).fires).toBe(0);
|
||
});
|
||
|
||
it('holds an absurdly small interval to the floor instead of freezing the frame', () => {
|
||
// Without the floor a 1 ns interval over a 1 s frame would ask for a billion fires.
|
||
const step = stepKeyRepeat({ held: 0.2, repeats: 0 }, { ...held, dt: 1, interval: 1e-9 });
|
||
expect(step.fires).toBeLessThanOrEqual(1 / MIN_REPEAT_INTERVAL + 1);
|
||
expect(step.fires).toBeGreaterThan(1 / MIN_REPEAT_INTERVAL - 2);
|
||
});
|
||
|
||
it('counts every due repeat when one frame swallows several', () => {
|
||
const step = stepKeyRepeat({ held: 0, repeats: 0 }, { ...held, dt: 1, after: 0.5, interval: 0.5 });
|
||
expect(step).toEqual({ state: { held: 1, repeats: 2 }, fires: 2 });
|
||
});
|
||
});
|
||
|
||
describe('the repeat rows on the node body', () => {
|
||
// With an order set, so the order row (#264) stays out of what these look at.
|
||
const hidden = (params: PropertyBag) => nodeDef('OnKeyDown')?.hiddenParams?.({ order: 1, ...params });
|
||
|
||
it('leaves both off while the repeat is switched off', () => {
|
||
expect(hidden({ key: 'a' })).toEqual(['repeatAfter', 'repeatInterval']);
|
||
expect(hidden({ key: 'a', repeatAfter: 0, repeatInterval: 50 })).toEqual([
|
||
'repeatAfter',
|
||
'repeatInterval',
|
||
]);
|
||
});
|
||
|
||
it('shows both once a delay is set, an interval of zero included', () => {
|
||
// Zero is a setting here ("every frame"), not an absence – hiding that row would lie.
|
||
expect(hidden({ key: 'a', repeatAfter: 300, repeatInterval: 0 })).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('the repeat fields in the inspector', () => {
|
||
const param = (key: string) => {
|
||
const def = nodeDef('OnKeyDown')?.params.find((p) => p.key === key);
|
||
return def?.kind === 'number' ? def : null;
|
||
};
|
||
|
||
it('types both as whole milliseconds, so the field shows 300 and not 300.000', () => {
|
||
for (const key of ['repeatAfter', 'repeatInterval']) {
|
||
expect(param(key)?.decimals).toBe(0);
|
||
expect(formatNumber(300, param(key)?.decimals)).toBe('300');
|
||
}
|
||
});
|
||
});
|