Files
feuerwerk/src/editor/PlaySession.ts
T
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00

145 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Owns everything that only exists while the project is playing: the world, its systems, the
* keyboard and the per-frame step. Play never touches the saved model – the world is built
* from the runtime copy `state.enterPlay()` swaps in and `exitPlay()` throws away.
*/
import { worldPositionOf } from '../core/graph';
import { BehaviorSystem, type Trace } from '../runtime/systems/BehaviorSystem';
import { CollisionSystem } from '../runtime/systems/CollisionSystem';
import { Input } from '../runtime/systems/Input';
import { World } from '../runtime/World';
import { ui } from '../ui';
import type { GameViewport } from '../viewport/GameViewport';
import type { GraphEditor } from './graph/GraphEditor';
import type { EditorState } from './state';
/** Facts about the running world for the check scripts. */
export interface WorldSnapshot {
entities: {
objectId: string;
assetId: string;
name: string;
x: number;
y: number;
worldX: number;
worldY: number;
parent: string;
props: Record<string, unknown>;
}[];
log: string[];
}
export interface PlayHost {
gameView: GameViewport;
graphEditor: GraphEditor;
showGame(): void;
/** Leave the game view if it is the one on screen. */
leaveGame(): void;
}
export class PlaySession {
private world: World | null = null;
private behaviors: BehaviorSystem | null = null;
private readonly input = new Input();
constructor(
private readonly state: EditorState,
private readonly host: PlayHost,
) {}
get isPlaying(): boolean {
return this.world !== null;
}
traceFor(assetId: string): Trace | null {
return this.behaviors?.traceFor(assetId) ?? null;
}
get time(): number {
return this.world?.time ?? 0;
}
syncFromModel(): void {
this.world?.syncFromModel();
}
play(): void {
const scene = this.state.activeScene;
if (!scene) return;
ui().focus.blur();
const runtime = this.state.enterPlay();
const world = World.instantiate(runtime, scene.id);
this.world = world;
this.input.attach();
// Collisions are detected first so behaviors see this frame's enter/stay/exit events.
const collisions = new CollisionSystem();
this.behaviors = new BehaviorSystem(runtime, this.input, collisions);
world.addSystem(collisions);
world.addSystem(this.behaviors);
this.host.gameView.setWorld(world);
this.host.showGame();
this.state.setNotice(
world.firstOfType('camera')
? 'Playing · edits are not saved · Esc stops'
: 'This scene has no camera – nothing is rendered · Esc stops',
);
if (world.stopRequested) this.stop();
}
stop(): void {
const world = this.world;
this.world = null;
this.behaviors = null;
this.input.detach();
ui().focus.blur();
world?.destroy();
this.host.gameView.setWorld(null);
this.state.exitPlay();
this.host.leaveGame();
const lastError = world?.log.find((line) => line.startsWith('Error in behavior'));
this.state.setNotice(lastError ?? '');
}
tick(dt: number): void {
const world = this.world;
if (!world) return;
world.tick(Math.min(dt, 0.1));
this.input.endFrame();
if (world.stopRequested) {
this.stop();
return;
}
if (world.restartRequested) {
this.stop();
this.play();
return;
}
// Mirror what the systems did into the runtime copy so the panels show live values.
if (world.syncToModel()) this.state.preview();
this.state.runtimeTick();
if (this.host.graphEditor.visible) this.host.graphEditor.canvas.redraw();
}
snapshot(): WorldSnapshot | null {
const world = this.world;
if (!world) return null;
return {
entities: world.entities.map((e) => {
const w = worldPositionOf(e);
return {
objectId: e.objectId,
assetId: e.assetId,
name: e.name,
x: e.x,
y: e.y,
worldX: w.x,
worldY: w.y,
parent: e.parent?.objectId ?? '',
props: { ...e.props },
};
}),
log: [...world.log],
};
}
}