Add ESLint and Prettier, and close the node test gaps
Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f140c2fd77
commit
d54a620ebd
113 files changed
+5049
-666
No files matched your search
@@ -1,4 +1,11 @@
|
||||
import { type EntityHandle, GraphInterpreter, isHandleEnabled, type NodeContext, type TraceListener } from '../../core/graph';
|
||||
import {
|
||||
type EntityHandle,
|
||||
GraphInterpreter,
|
||||
isHandleEnabled,
|
||||
type NodeContext,
|
||||
type TraceListener,
|
||||
} from '../../core/graph';
|
||||
import type { PropertyBag } from '../../core/model/Properties';
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
@@ -56,7 +63,12 @@ export class BehaviorSystem implements System {
|
||||
private syncInstances(world: World): void {
|
||||
this.instances = this.instances.filter((i) => world.entities.includes(i.entity));
|
||||
for (const entity of world.entities) {
|
||||
if (entity.type !== 'behavior' || !isHandleEnabled(entity) || this.instances.some((i) => i.entity === entity)) continue;
|
||||
if (
|
||||
entity.type !== 'behavior' ||
|
||||
!isHandleEnabled(entity) ||
|
||||
this.instances.some((i) => i.entity === entity)
|
||||
)
|
||||
continue;
|
||||
const asset = findAsset(this.project, entity.assetId);
|
||||
if (!asset || asset.type !== 'behavior') continue;
|
||||
const trace: Trace = { edges: new Map(), nodes: new Map() };
|
||||
@@ -73,34 +85,38 @@ export class BehaviorSystem implements System {
|
||||
|
||||
update(world: World, dt: number): void {
|
||||
this.syncInstances(world);
|
||||
const keyOf = (node: { params: Record<string, unknown> }) => String(node.params.key ?? '');
|
||||
const keyOf = (node: { params: PropertyBag }) => String(node.params.key ?? '');
|
||||
for (const instance of this.instances) {
|
||||
if (!world.entities.includes(instance.entity)) continue; // destroyed during play
|
||||
if (!isHandleEnabled(instance.entity)) continue; // switched off (itself or a parent)
|
||||
this.run(instance, (i, ctx) => {
|
||||
for (const key of this.input.justPressed) i.fire('OnKeyDown', ctx, {}, (n) => keyOf(n) === key);
|
||||
for (const key of this.input.justReleased) i.fire('OnKeyUp', ctx, {}, (n) => keyOf(n) === key);
|
||||
for (const key of this.input.heldKeys) i.fire('OnKeyHeld', ctx, { dt }, (n) => keyOf(n) === key);
|
||||
for (const node of i.eventNodes('OnTimer')) {
|
||||
const state = i.nodeState.get(node.id) ?? { elapsed: 0 };
|
||||
i.nodeState.set(node.id, state);
|
||||
const interval = Math.max(0.01, Number(node.params.interval ?? 1));
|
||||
state.elapsed = (state.elapsed ?? 0) + dt;
|
||||
while (state.elapsed >= interval) {
|
||||
state.elapsed -= interval;
|
||||
i.fire('OnTimer', ctx, {}, (n) => n.id === node.id);
|
||||
this.run(
|
||||
instance,
|
||||
(i, ctx) => {
|
||||
for (const key of this.input.justPressed) i.fire('OnKeyDown', ctx, {}, (n) => keyOf(n) === key);
|
||||
for (const key of this.input.justReleased) i.fire('OnKeyUp', ctx, {}, (n) => keyOf(n) === key);
|
||||
for (const key of this.input.heldKeys) i.fire('OnKeyHeld', ctx, { dt }, (n) => keyOf(n) === key);
|
||||
for (const node of i.eventNodes('OnTimer')) {
|
||||
const state = i.nodeState.get(node.id) ?? { elapsed: 0 };
|
||||
i.nodeState.set(node.id, state);
|
||||
const interval = Math.max(0.01, Number(node.params.interval ?? 1));
|
||||
state.elapsed = (state.elapsed ?? 0) + dt;
|
||||
while (state.elapsed >= interval) {
|
||||
state.elapsed -= interval;
|
||||
i.fire('OnTimer', ctx, {}, (n) => n.id === node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.fireCollisions(instance, i, ctx);
|
||||
// With an owner the click has to land on it; a behavior without one has no shape to
|
||||
// be hit, so it listens to the whole game view (anywhere inside the camera frame).
|
||||
const pointer = world.pointer;
|
||||
const owner = instance.entity.parent;
|
||||
if (pointer.pressed && (owner ? world.hitTest(owner, pointer.x, pointer.y) : pointer.inside)) {
|
||||
i.fire('OnClick', ctx, { x: pointer.x, y: pointer.y });
|
||||
}
|
||||
i.fire('OnUpdate', ctx, { dt });
|
||||
}, dt);
|
||||
this.fireCollisions(instance, i, ctx);
|
||||
// With an owner the click has to land on it; a behavior without one has no shape to
|
||||
// be hit, so it listens to the whole game view (anywhere inside the camera frame).
|
||||
const pointer = world.pointer;
|
||||
const owner = instance.entity.parent;
|
||||
if (pointer.pressed && (owner ? world.hitTest(owner, pointer.x, pointer.y) : pointer.inside)) {
|
||||
i.fire('OnClick', ctx, { x: pointer.x, y: pointer.y });
|
||||
}
|
||||
i.fire('OnUpdate', ctx, { dt });
|
||||
},
|
||||
dt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,14 +156,20 @@ export class BehaviorSystem implements System {
|
||||
}
|
||||
|
||||
/** Runs graph code for one instance; an exception stops play and is logged, never thrown. */
|
||||
private run(instance: Instance, body: (interpreter: GraphInterpreter, ctx: NodeContext) => void, dt: number): void {
|
||||
private run(
|
||||
instance: Instance,
|
||||
body: (interpreter: GraphInterpreter, ctx: NodeContext) => void,
|
||||
dt: number,
|
||||
): void {
|
||||
const world = this.world;
|
||||
if (!world) return;
|
||||
const ctx = this.context(instance.entity, world, dt);
|
||||
try {
|
||||
body(instance.interpreter, ctx);
|
||||
} catch (error) {
|
||||
world.log.push(`Error in behavior "${instance.entity.name}": ${error instanceof Error ? error.message : String(error)}`);
|
||||
world.log.push(
|
||||
`Error in behavior "${instance.entity.name}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
world.stopRequested = true;
|
||||
}
|
||||
}
|
||||
@@ -184,6 +206,8 @@ export class BehaviorSystem implements System {
|
||||
},
|
||||
log: (message) => {
|
||||
world.log.push(message);
|
||||
// The `Log` node's whole purpose: the author wants this in the browser console.
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[behavior ${entity.name}]`, message);
|
||||
},
|
||||
spawn: (assetId, position, parent) => world.spawn(assetId, position, resolve(parent) ?? null),
|
||||
|
||||
Reference in new issue
Block a user