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
+52
-12
@@ -33,7 +33,14 @@ export class World {
|
||||
* Mouse state in world units, fed by the game view (the only place with the camera
|
||||
* transform). `pressed`/`released` last for one tick, like the keyboard edges.
|
||||
*/
|
||||
readonly pointer: PointerState = { x: 0, y: 0, inside: false, down: false, pressed: false, released: false };
|
||||
readonly pointer: PointerState = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
inside: false,
|
||||
down: false,
|
||||
pressed: false,
|
||||
released: false,
|
||||
};
|
||||
/** Messages logged by behaviors (Log node), newest last. */
|
||||
readonly log: string[] = [];
|
||||
private readonly systems: System[] = [];
|
||||
@@ -41,7 +48,10 @@ export class World {
|
||||
/** Set when `spawn` added an object; reported (and cleared) by the next `syncToModel`. */
|
||||
private structureChanged = false;
|
||||
|
||||
private constructor(readonly project: Project, readonly sceneId: string) {}
|
||||
private constructor(
|
||||
readonly project: Project,
|
||||
readonly sceneId: string,
|
||||
) {}
|
||||
|
||||
/** Builds a world from the given scene. Throws when the scene does not exist. */
|
||||
static instantiate(project: Project, sceneId: string): World {
|
||||
@@ -75,11 +85,12 @@ export class World {
|
||||
if (!asset || !isPlaceableType(asset.type)) continue;
|
||||
keep.add(this.materialize(object, asset));
|
||||
}
|
||||
for (let i = this.entities.length - 1; i >= 0; i--) if (!keep.has(this.entities[i]!)) this.entities.splice(i, 1);
|
||||
for (let i = this.entities.length - 1; i >= 0; i--)
|
||||
if (!keep.has(this.entities[i]!)) this.entities.splice(i, 1);
|
||||
// Link parents once every entity exists (storage order is not guaranteed to be parent-first).
|
||||
for (const entity of this.entities) {
|
||||
const object = findObject(scene, entity.objectId);
|
||||
entity.parent = object?.parentId ? this.findByObjectId(object.parentId) ?? null : null;
|
||||
entity.parent = object?.parentId ? (this.findByObjectId(object.parentId) ?? null) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +98,18 @@ export class World {
|
||||
private materialize(object: SceneObject, asset: Asset): Entity {
|
||||
let entity = this.findByObjectId(object.id);
|
||||
if (!entity) {
|
||||
entity = { id: `e-${object.id}`, objectId: object.id, assetId: asset.id, type: asset.type, name: '', x: 0, y: 0, z: 0, props: {}, parent: null };
|
||||
entity = {
|
||||
id: `e-${object.id}`,
|
||||
objectId: object.id,
|
||||
assetId: asset.id,
|
||||
type: asset.type,
|
||||
name: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
props: {},
|
||||
parent: null,
|
||||
};
|
||||
this.entities.push(entity);
|
||||
}
|
||||
entity.assetId = asset.id;
|
||||
@@ -109,14 +131,21 @@ export class World {
|
||||
* during this tick already). Position is local to `parent`, world when there is none.
|
||||
* Returns null for unknown or unplaceable assets.
|
||||
*/
|
||||
spawn(assetId: string, position: { x: number; y: number; z: number }, parent: Entity | null): Entity | null {
|
||||
spawn(
|
||||
assetId: string,
|
||||
position: { x: number; y: number; z: number },
|
||||
parent: Entity | null,
|
||||
): Entity | null {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
const asset = findAsset(this.project, assetId);
|
||||
if (!scene || !asset || !isPlaceableType(asset.type)) return null;
|
||||
const parentEntity = parent && this.entities.includes(parent) ? parent : null;
|
||||
const object = createSceneObject(newId(), asset.id, position, parentEntity?.objectId ?? '');
|
||||
// A container brings its content along (parented below it, in template order).
|
||||
const created = [object, ...(asset.type === 'container' ? instantiateContainer(this.project, asset, object.id) : [])];
|
||||
const created = [
|
||||
object,
|
||||
...(asset.type === 'container' ? instantiateContainer(this.project, asset, object.id) : []),
|
||||
];
|
||||
scene.objects.push(...created);
|
||||
const entity = this.materialize(object, asset);
|
||||
entity.parent = parentEntity;
|
||||
@@ -152,9 +181,11 @@ export class World {
|
||||
const asset = object && findAsset(this.project, entity.assetId);
|
||||
if (!object || !asset) continue;
|
||||
const p = object.position;
|
||||
if (p.x !== entity.x || p.y !== entity.y || p.z !== entity.z) object.position = { x: entity.x, y: entity.y, z: entity.z };
|
||||
if (p.x !== entity.x || p.y !== entity.y || p.z !== entity.z)
|
||||
object.position = { x: entity.x, y: entity.y, z: entity.z };
|
||||
const base = resolveSceneObjectProps(asset, object);
|
||||
for (const [key, value] of Object.entries(entity.props)) if (key in base && base[key] !== value) object.overrides[key] = value;
|
||||
for (const [key, value] of Object.entries(entity.props))
|
||||
if (key in base && base[key] !== value) object.overrides[key] = value;
|
||||
}
|
||||
return spawned || scene.objects.length !== before;
|
||||
}
|
||||
@@ -210,8 +241,13 @@ export class World {
|
||||
const point = { x, y };
|
||||
const parts = [entity, ...this.descendantsOf(entity)].filter((e) => isHandleEnabled(e));
|
||||
const colliders = parts.filter((e) => e.type === 'collider');
|
||||
if (colliders.length > 0) return colliders.some((c) => shapeContains(colliderShape(c.props, worldPositionOf(c)), point));
|
||||
return parts.some((e) => (e.type === 'sprite' || e.type === 'text') && footprintContains(e.type, e.props, worldPositionOf(e), point));
|
||||
if (colliders.length > 0)
|
||||
return colliders.some((c) => shapeContains(colliderShape(c.props, worldPositionOf(c)), point));
|
||||
return parts.some(
|
||||
(e) =>
|
||||
(e.type === 'sprite' || e.type === 'text') &&
|
||||
footprintContains(e.type, e.props, worldPositionOf(e), point),
|
||||
);
|
||||
}
|
||||
|
||||
childrenOf(entity: Entity): Entity[] {
|
||||
@@ -220,7 +256,11 @@ export class World {
|
||||
|
||||
/** Everything parented below an entity, at any depth. */
|
||||
descendantsOf(entity: Entity): Entity[] {
|
||||
return descendantsBelow(entity, (parent) => this.childrenOf(parent), (child) => child);
|
||||
return descendantsBelow(
|
||||
entity,
|
||||
(parent) => this.childrenOf(parent),
|
||||
(child) => child,
|
||||
);
|
||||
}
|
||||
|
||||
findByObjectId(objectId: string): Entity | undefined {
|
||||
|
||||
Reference in new issue
Block a user