Refactoring: one file per node, consistent comments, shared helpers, split large modules
- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry separated, writeTyped() replaces duplicated typed writes - Comments: one style (JSDoc on non-obvious exports, // for decisions and traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars - Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts, commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe, Input.axis, setMode, titleText, World.worldPosition, unused ctor params) - Editor: Inspector split into inspector/ sections, App.ts into Editor.ts + debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared "Add collider" action, unified type labels/swatches - UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers, Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes - Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections - Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3234eb480f
commit
1eb28314cf
189 files changed
+6231
-5543
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { isHandleEnabled } from '../../core/graph';
|
||||
import { type Axis, axisCorrection, colliderShape, colliderSize, isTrigger, shapesOverlap } from '../../core/properties/collider';
|
||||
import { isHandleEnabled, worldPositionOf } from '../../core/graph';
|
||||
import { axisCorrection, colliderShape, colliderSize, isTrigger, type PlaneAxis, shapesOverlap } from '../../core/properties/collider';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
import type { System } from './System';
|
||||
@@ -36,7 +36,7 @@ export class CollisionSystem implements System {
|
||||
const a = colliders[i]!;
|
||||
const b = colliders[j]!;
|
||||
if (bodyOf(a) === bodyOf(b)) continue;
|
||||
if (this.overlapping(world, a, b)) current.set(`${a.id}|${b.id}`, { a, b });
|
||||
if (this.overlapping(a, b)) current.set(`${a.id}|${b.id}`, { a, b });
|
||||
}
|
||||
}
|
||||
this.enters = [];
|
||||
@@ -55,15 +55,15 @@ export class CollisionSystem implements System {
|
||||
}
|
||||
|
||||
/** Live test of two collider entities (world positions applied). */
|
||||
overlapping(world: World, a: Entity, b: Entity): boolean {
|
||||
return shapesOverlap(colliderShape(a.props, world.worldPosition(a)), colliderShape(b.props, world.worldPosition(b)));
|
||||
overlapping(a: Entity, b: Entity): boolean {
|
||||
return shapesOverlap(shapeOf(a), shapeOf(b));
|
||||
}
|
||||
|
||||
/** Whether any collider of `a` overlaps any collider of `b` (see `collidersOf`). */
|
||||
touching(world: World, a: Entity, b: Entity): boolean {
|
||||
for (const ca of collidersOf(world, a)) {
|
||||
for (const cb of collidersOf(world, b)) {
|
||||
if (ca !== cb && this.overlapping(world, ca, cb)) return true;
|
||||
if (ca !== cb && this.overlapping(ca, cb)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -80,20 +80,20 @@ export class CollisionSystem implements System {
|
||||
*/
|
||||
moveAndSlide(world: World, entity: Entity, dx: number, dy: number): { blockedX: boolean; blockedY: boolean } {
|
||||
const own = collidersOf(world, entity).filter((c) => !isTrigger(c.props) && isHandleEnabled(c));
|
||||
const obstacles = own.length === 0 ? [] : world.entities.filter((c) => c.type === 'collider' && !isTrigger(c.props) && isHandleEnabled(c) && !this.travelsWith(c, entity));
|
||||
return { blockedX: this.slideAxis(world, entity, own, obstacles, 'x', dx), blockedY: this.slideAxis(world, entity, own, obstacles, 'y', dy) };
|
||||
const obstacles = own.length === 0 ? [] : world.entities.filter((c) => c.type === 'collider' && !isTrigger(c.props) && isHandleEnabled(c) && !travelsWith(c, entity));
|
||||
return { blockedX: this.slideAxis(entity, own, obstacles, 'x', dx), blockedY: this.slideAxis(entity, own, obstacles, 'y', dy) };
|
||||
}
|
||||
|
||||
private slideAxis(world: World, entity: Entity, own: Entity[], obstacles: Entity[], axis: Axis, delta: number): boolean {
|
||||
private slideAxis(entity: Entity, own: Entity[], obstacles: Entity[], axis: PlaneAxis, delta: number): boolean {
|
||||
if (delta === 0 || own.length === 0) return false;
|
||||
// Long steps are split so a fast mover cannot jump across a thin obstacle in one frame.
|
||||
const steps = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.abs(delta) / stepLimit(own, obstacles, axis))));
|
||||
for (let i = 0; i < steps; i++) if (this.stepAxis(world, entity, own, obstacles, axis, delta / steps)) return true;
|
||||
for (let i = 0; i < steps; i++) if (this.stepAxis(entity, own, obstacles, axis, delta / steps)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** One indivisible move along `axis`; returns whether an obstacle stopped it. */
|
||||
private stepAxis(world: World, entity: Entity, own: Entity[], obstacles: Entity[], axis: Axis, delta: number): boolean {
|
||||
private stepAxis(entity: Entity, own: Entity[], obstacles: Entity[], axis: PlaneAxis, delta: number): boolean {
|
||||
const dir: 1 | -1 = delta > 0 ? 1 : -1;
|
||||
// How deep each pair already sits in the other before the step. A step may never deepen
|
||||
// that, but resolving it is not our job (no jolt, and leaving a wall is always possible).
|
||||
@@ -102,49 +102,53 @@ export class CollisionSystem implements System {
|
||||
const before: [Entity, Entity, number][] = [];
|
||||
for (const a of own) {
|
||||
for (const b of obstacles) {
|
||||
const p = this.penetration(world, a, b, axis, dir);
|
||||
const p = penetration(a, b, axis, dir);
|
||||
before.push([a, b, p > SKIN ? p : 0]);
|
||||
}
|
||||
}
|
||||
entity[axis] += delta;
|
||||
let correction = 0;
|
||||
for (const [a, b, was] of before) {
|
||||
const t = this.penetration(world, a, b, axis, dir) - was;
|
||||
const t = penetration(a, b, axis, dir) - was;
|
||||
if (t > correction) correction = t;
|
||||
}
|
||||
if (correction <= 0) return false;
|
||||
entity[axis] -= dir * Math.min(correction, Math.abs(delta));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How far `a` would have to be pushed back against `dir` to just clear `b` (0 when they are
|
||||
* apart). Once a's centre has passed b's centre along `dir` the shorter way out is forwards,
|
||||
* so nothing blocks – that keeps an object that ended up inside something able to leave.
|
||||
*/
|
||||
private penetration(world: World, a: Entity, b: Entity, axis: Axis, dir: 1 | -1): number {
|
||||
const mover = colliderShape(a.props, world.worldPosition(a));
|
||||
const obstacle = colliderShape(b.props, world.worldPosition(b));
|
||||
if (dir * (mover[axis] - obstacle[axis]) >= 0) return 0;
|
||||
return axisCorrection(mover, obstacle, axis, dir);
|
||||
}
|
||||
function shapeOf(collider: Entity) {
|
||||
return colliderShape(collider.props, worldPositionOf(collider));
|
||||
}
|
||||
|
||||
/** Whether `collider` belongs to `entity` or to something parented (at any depth) under it. */
|
||||
private travelsWith(collider: Entity, entity: Entity): boolean {
|
||||
for (let e: Entity | null = collider; e; e = e.parent) if (e === entity) return true;
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* How far `a` would have to be pushed back against `dir` to just clear `b` (0 when they are
|
||||
* apart). Once a's centre has passed b's centre along `dir` the shorter way out is forwards,
|
||||
* so nothing blocks – that keeps an object that ended up inside something able to leave.
|
||||
*/
|
||||
function penetration(a: Entity, b: Entity, axis: PlaneAxis, dir: 1 | -1): number {
|
||||
const mover = shapeOf(a);
|
||||
const obstacle = shapeOf(b);
|
||||
if (dir * (mover[axis] - obstacle[axis]) >= 0) return 0;
|
||||
return axisCorrection(mover, obstacle, axis, dir);
|
||||
}
|
||||
|
||||
/** Whether `collider` belongs to `entity` or to something parented (at any depth) under it. */
|
||||
function travelsWith(collider: Entity, entity: Entity): boolean {
|
||||
for (let e: Entity | null = collider; e; e = e.parent) if (e === entity) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Longest safe step along `axis`: half of the thinnest collider involved (never below 1). */
|
||||
function stepLimit(own: Entity[], obstacles: Entity[], axis: Axis): number {
|
||||
function stepLimit(own: Entity[], obstacles: Entity[], axis: PlaneAxis): number {
|
||||
let min = Infinity;
|
||||
for (const e of own) min = Math.min(min, extentOf(e, axis));
|
||||
for (const e of obstacles) min = Math.min(min, extentOf(e, axis));
|
||||
return Math.max(1, min / 2);
|
||||
}
|
||||
|
||||
function extentOf(collider: Entity, axis: Axis): number {
|
||||
function extentOf(collider: Entity, axis: PlaneAxis): number {
|
||||
const size = colliderSize(collider.props);
|
||||
return axis === 'x' ? size.w : size.h;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user