castle-web-cli 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +1 -1
  3. package/kits/basic-2d/CLAUDE.md +3 -3
  4. package/package.json +1 -1
  5. package/kits/basic-2d-frozen/.prettierrc +0 -8
  6. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  7. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  8. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  9. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  10. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  11. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  12. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  13. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  14. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  15. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  16. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  17. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  18. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  19. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  20. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  21. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  22. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  23. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  24. package/kits/basic-2d-frozen/engine/files.js +0 -62
  25. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  26. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  27. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  28. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  29. package/kits/basic-2d-frozen/index.html +0 -11
  30. package/kits/basic-2d-frozen/main.jsx +0 -10
  31. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  32. package/kits/basic-2d-frozen/package.json +0 -41
  33. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  34. package/kits/basic-2d-frozen/vite.config.js +0 -1
  35. package/kits/rpg-2d/.prettierrc +0 -8
  36. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  37. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  38. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  39. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  40. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  41. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  42. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  43. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  44. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  45. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  46. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  47. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  48. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  49. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  50. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  51. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  52. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  53. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  55. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  56. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  57. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  58. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  59. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  60. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  61. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  62. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  63. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  67. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  70. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  71. package/kits/rpg-2d/editors/App.tsx +0 -163
  72. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  73. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  74. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  75. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  76. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  77. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  78. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  79. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  80. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  81. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  82. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  83. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  84. package/kits/rpg-2d/engine/drawing.ts +0 -81
  85. package/kits/rpg-2d/engine/files.ts +0 -215
  86. package/kits/rpg-2d/engine/scene.ts +0 -484
  87. package/kits/rpg-2d/engine/ui.module.css +0 -928
  88. package/kits/rpg-2d/engine/ui.tsx +0 -483
  89. package/kits/rpg-2d/eslint.config.js +0 -46
  90. package/kits/rpg-2d/index.html +0 -11
  91. package/kits/rpg-2d/main.tsx +0 -14
  92. package/kits/rpg-2d/package-lock.json +0 -3149
  93. package/kits/rpg-2d/package.json +0 -46
  94. package/kits/rpg-2d/scenes/main.scene +0 -203
  95. package/kits/rpg-2d/tsconfig.json +0 -17
  96. package/kits/rpg-2d/vite-env.d.ts +0 -7
  97. package/kits/rpg-2d/vite.config.js +0 -1
@@ -1,215 +0,0 @@
1
- export type FileMap = Record<string, string>;
2
- export type FileKind = 'scene' | 'drawing' | 'code' | 'text';
3
-
4
- // Fully transparent pixel -- the empty value in a pixel grid.
5
- export const TRANSPARENT = '#00000000';
6
-
7
- export type DrawingPlayMode = 'still' | 'once' | 'loop';
8
-
9
- // One layer of a drawing: an ordered stack of frames, each frame a flat
10
- // row-major pixel grid of `width * height` hex-RGBA strings.
11
- export interface DrawingLayer {
12
- id: string;
13
- name: string;
14
- visible: boolean;
15
- frames: string[][];
16
- }
17
-
18
- // A drawing is a stack of layers, each animated across a shared frame count.
19
- // Layers composite bottom-to-top; animation advances all layers in lockstep.
20
- // `initialFrame` is 1-based.
21
- export interface DrawingData {
22
- width: number;
23
- height: number;
24
- fps: number;
25
- playMode: DrawingPlayMode;
26
- initialFrame: number;
27
- palette: string[];
28
- layers: DrawingLayer[];
29
- }
30
-
31
- // Number of animation frames -- layers stay frame-count-synced, so layer 0
32
- // is authoritative.
33
- export function frameCount(drawing: DrawingData): number {
34
- return drawing.layers[0]?.frames.length ?? 1;
35
- }
36
-
37
- let layerIdCounter = 0;
38
-
39
- export function newLayerId(): string {
40
- layerIdCounter += 1;
41
- return `layer_${Date.now().toString(36)}_${layerIdCounter}`;
42
- }
43
-
44
- export function blankPixelGrid(width: number, height: number): string[] {
45
- return Array<string>(Math.max(1, width * height)).fill(TRANSPARENT);
46
- }
47
-
48
- // Default palette for fresh drawings; user-editable per file.
49
- const DEFAULT_PALETTE: string[] = [
50
- '#00000000',
51
- '#000000FF',
52
- '#444444FF',
53
- '#888888FF',
54
- '#ffffffFF',
55
- '#8db7ffFF',
56
- '#3fae5dFF',
57
- '#ffe17aFF',
58
- '#ff5d5dFF',
59
- ];
60
-
61
- // Coerce arbitrary parsed JSON into a valid DrawingData. Accepts the legacy
62
- // flat `{width,height,pixels}` shape and lifts it into a single layer/frame
63
- // so old `.drawing` files keep loading.
64
- export function normalizeDrawing(raw: unknown): DrawingData {
65
- const obj = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
66
- const width = clampDim(obj.width, 16);
67
- const height = clampDim(obj.height, 16);
68
-
69
- let layers: DrawingLayer[];
70
- if (Array.isArray(obj.layers) && obj.layers.length > 0) {
71
- layers = obj.layers.map((rawLayer, index) =>
72
- normalizeLayer(rawLayer, index, width, height)
73
- );
74
- } else if (Array.isArray(obj.pixels)) {
75
- layers = [
76
- {
77
- id: newLayerId(),
78
- name: 'Layer 1',
79
- visible: true,
80
- frames: [normalizeFrame(obj.pixels as unknown[], width, height)],
81
- },
82
- ];
83
- } else {
84
- layers = [
85
- { id: newLayerId(), name: 'Layer 1', visible: true, frames: [blankPixelGrid(width, height)] },
86
- ];
87
- }
88
- const count = layers[0]?.frames.length ?? 1;
89
- for (const layer of layers) {
90
- while (layer.frames.length < count) layer.frames.push(blankPixelGrid(width, height));
91
- layer.frames.length = count;
92
- }
93
-
94
- const palette = Array.isArray(obj.palette)
95
- ? (obj.palette as unknown[]).filter((c): c is string => typeof c === 'string')
96
- : DEFAULT_PALETTE.slice();
97
- const fps = typeof obj.fps === 'number' ? obj.fps : 4;
98
- const playMode: DrawingPlayMode =
99
- obj.playMode === 'once' || obj.playMode === 'loop' ? obj.playMode : 'still';
100
- const initialFrame = typeof obj.initialFrame === 'number' ? Math.max(1, obj.initialFrame) : 1;
101
-
102
- return { width, height, fps, playMode, initialFrame, palette, layers };
103
- }
104
-
105
- function clampDim(value: unknown, fallback: number): number {
106
- return typeof value === 'number' && value > 0 ? Math.floor(value) : fallback;
107
- }
108
-
109
- function normalizeFrame(raw: unknown[], width: number, height: number): string[] {
110
- const out = blankPixelGrid(width, height);
111
- for (let i = 0; i < out.length; i++) {
112
- const cell = raw[i];
113
- if (typeof cell === 'string') out[i] = cell;
114
- }
115
- return out;
116
- }
117
-
118
- function normalizeLayer(
119
- raw: unknown,
120
- index: number,
121
- width: number,
122
- height: number
123
- ): DrawingLayer {
124
- const obj = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
125
- const frames: string[][] = Array.isArray(obj.frames)
126
- ? obj.frames.map((frame) =>
127
- Array.isArray(frame) ? normalizeFrame(frame as unknown[], width, height) : blankPixelGrid(width, height)
128
- )
129
- : [blankPixelGrid(width, height)];
130
- return {
131
- id: typeof obj.id === 'string' ? obj.id : newLayerId(),
132
- name: typeof obj.name === 'string' ? obj.name : `Layer ${index + 1}`,
133
- visible: obj.visible !== false,
134
- frames,
135
- };
136
- }
137
-
138
- // Seed the file map by scanning the deck dir. Vite module-cache is invalidated
139
- // on restart, so a newly-created file just shows up on the next reload.
140
- const rawModules = import.meta.glob<string>(
141
- ['../scenes/*.scene', '../drawings/*.drawing', '../behaviors/*.tsx'],
142
- { query: '?raw', import: 'default', eager: true }
143
- );
144
-
145
- export const initialFiles: FileMap = Object.fromEntries(
146
- Object.entries(rawModules)
147
- .map(([globPath, text]): [string, string] => [globPath.replace(/^\.\.\//, ''), text])
148
- .sort(([a], [b]) => a.localeCompare(b))
149
- );
150
-
151
- export function getFileKind(path: string): FileKind {
152
- if (path.endsWith('.scene')) return 'scene';
153
- if (path.endsWith('.drawing')) return 'drawing';
154
- if (path.endsWith('.ts') || path.endsWith('.tsx')) return 'code';
155
- return 'text';
156
- }
157
-
158
- export function parseJsonFile<TValue = unknown>(
159
- path: string,
160
- text: string
161
- ): { value: TValue | null; error: string | null } {
162
- try {
163
- return { value: JSON.parse(text) as TValue, error: null };
164
- } catch (error) {
165
- const message = error instanceof Error ? error.message : String(error);
166
- return { value: null, error: `${path}: ${message}` };
167
- }
168
- }
169
-
170
- export function formatJson(value: unknown): string {
171
- return `${JSON.stringify(value, null, 2)}\n`;
172
- }
173
-
174
- export function basename(path: string): string {
175
- return path.split('/').pop() ?? path;
176
- }
177
-
178
- type OrderNode = {
179
- isFile: boolean;
180
- path: string;
181
- children: OrderNode[];
182
- childMap: Map<string, OrderNode>;
183
- };
184
-
185
- // Flat list of file paths in the order FileBrowser renders them: a
186
- // depth-first walk of the directory tree, mirroring buildFileTree there.
187
- export function flatFileOrder(paths: string[]): string[] {
188
- const root: OrderNode = { isFile: false, path: '', children: [], childMap: new Map() };
189
- for (const path of paths) {
190
- const parts = path.split('/');
191
- let parent = root;
192
- for (let index = 0; index < parts.length; index++) {
193
- const name = parts[index];
194
- const nodePath = parts.slice(0, index + 1).join('/');
195
- const isFile = index === parts.length - 1;
196
- if (!parent.childMap.has(name)) {
197
- const node: OrderNode = { isFile, path: nodePath, children: [], childMap: new Map() };
198
- parent.childMap.set(name, node);
199
- parent.children.push(node);
200
- }
201
- const child = parent.childMap.get(name);
202
- if (!child || child.isFile) break;
203
- parent = child;
204
- }
205
- }
206
- const order: string[] = [];
207
- const walk = (node: OrderNode): void => {
208
- for (const child of node.children) {
209
- if (child.isFile) order.push(child.path);
210
- else walk(child);
211
- }
212
- };
213
- walk(root);
214
- return order;
215
- }
@@ -1,484 +0,0 @@
1
- import type { DrawingData, FileMap } from './files';
2
- import type { LayoutProps } from '../behaviors/Layout.tsx';
3
- import type { DrawingProps } from '../behaviors/Drawing.tsx';
4
-
5
- export type ComponentProps = object;
6
-
7
- export interface ActorData {
8
- id: string;
9
- components: Record<string, ComponentProps | undefined>;
10
- runtime?: Record<string, unknown>;
11
- }
12
-
13
- export interface SceneData {
14
- name?: string;
15
- background?: string;
16
- actors: ActorData[];
17
- status?: string;
18
- }
19
-
20
- export interface SceneDrawOptions {
21
- selectedActorIds?: string[];
22
- marquee?: SelectionRect | null;
23
- showGrid?: boolean;
24
- showDebugColliders?: boolean;
25
- useCamera?: boolean;
26
- // When true, draw a faint outlined placeholder for every actor that has a
27
- // Layout. Editor-only, so actors without a Drawing component are still
28
- // visible + selectable on the stage.
29
- editPlaceholders?: boolean;
30
- }
31
-
32
- export interface SelectionRect {
33
- x: number;
34
- y: number;
35
- width: number;
36
- height: number;
37
- }
38
-
39
- export interface Behavior<TProps extends object = object> {
40
- props: TProps;
41
- update?: (actor: ActorData, scene: SceneRuntime, dt: number) => void;
42
- draw?: (
43
- actor: ActorData,
44
- scene: SceneRuntime,
45
- ctx: CanvasRenderingContext2D,
46
- options: SceneDrawOptions
47
- ) => void;
48
- // Game-time UI. Returns React nodes rendered into a deck-sized, clipped
49
- // overlay above the canvas; coordinates are card units, like Layout. Called
50
- // every frame during play -- read state set by `update`, return null to
51
- // render nothing. The host parents these nodes, so there is no DOM teardown
52
- // to manage.
53
- ui?: (actor: ActorData, scene: SceneRuntime) => React.ReactNode;
54
- }
55
-
56
- export interface BehaviorClass {
57
- behaviorName: string;
58
- defaultProps: object;
59
- new (props: never): Behavior;
60
- Inspector?: (props: never) => React.ReactNode;
61
- }
62
-
63
- const CARD_WIDTH = 500;
64
- const CARD_HEIGHT = 700;
65
-
66
- export class SceneRuntime {
67
- behaviors: Map<string, BehaviorClass>;
68
- drawings: Record<string, DrawingData>;
69
- time: number;
70
- keys: Set<string>;
71
- pointer: { x: number; y: number; down: boolean };
72
- data: SceneData;
73
- actors: Map<string, ActorData>;
74
- camera?: { x: number; y: number };
75
- status?: string;
76
- // Set by PlayerController on E-press: the id of the actor to interact with
77
- // this frame. Behaviors like Dialog watch their own id here and consume it.
78
- interactTarget?: string;
79
- // Set by Portal when the player overlaps it; ScenePlayer reloads the scene.
80
- requestedScene?: string;
81
-
82
- constructor(
83
- sceneData: SceneData,
84
- behaviors: BehaviorClass[],
85
- drawings: Record<string, DrawingData>
86
- ) {
87
- this.behaviors = new Map(behaviors.map((Behavior) => [Behavior.behaviorName, Behavior]));
88
- this.drawings = drawings;
89
- this.time = 0;
90
- this.keys = new Set();
91
- this.pointer = { x: 0, y: 0, down: false };
92
- this.data = { actors: [] };
93
- this.actors = new Map();
94
- this.load(sceneData);
95
- }
96
-
97
- load(sceneData: SceneData): void {
98
- this.data = structuredClone(sceneData);
99
- this.actors = new Map();
100
- for (const actor of this.data.actors ?? []) {
101
- this.actors.set(actor.id, actor);
102
- for (const [behaviorName, Behavior] of this.behaviors) {
103
- const component = actor.components[behaviorName];
104
- if (component) {
105
- actor.components[behaviorName] = {
106
- ...Behavior.defaultProps,
107
- ...component,
108
- };
109
- }
110
- }
111
- }
112
- }
113
-
114
- clone(): SceneRuntime {
115
- return new SceneRuntime(this.serialize(), [...this.behaviors.values()], this.drawings);
116
- }
117
-
118
- serialize(): SceneData {
119
- return structuredClone(this.data);
120
- }
121
-
122
- getActor(actorId: string): ActorData | undefined {
123
- return this.actors.get(actorId);
124
- }
125
-
126
- // Depth-sorted draw order. Top-down feel: the actor whose base
127
- // (Layout.y + height) sits lower on screen draws in front, so the player
128
- // walks behind a building when north of its base and in front of it when
129
- // south. `z` is only a tiebreaker on identical feet-y -- it doesn't
130
- // override the painterly y sort, which is what bloom-run does too.
131
- getActors(): ActorData[] {
132
- return [...this.actors.values()].sort((a, b) => {
133
- const al = getLayout(a);
134
- const bl = getLayout(b);
135
- const afeet = (al?.y ?? 0) + (al?.height ?? 0);
136
- const bfeet = (bl?.y ?? 0) + (bl?.height ?? 0);
137
- if (afeet !== bfeet) return afeet - bfeet;
138
- const az = al?.z ?? 0;
139
- const bz = bl?.z ?? 0;
140
- return az - bz;
141
- });
142
- }
143
-
144
- getComponent(actor: ActorData | null | undefined, behaviorName: string): ComponentProps | null {
145
- return actor?.components?.[behaviorName] ?? null;
146
- }
147
-
148
- // Translate a screen-space pointer event into the scene's world-space
149
- // `pointer`. The runtime owns this mapping (it knows the camera) so every
150
- // input path -- the standalone ScenePlayer and the editor's play mode -- agree
151
- // on it; behaviors read `scene.pointer` in world coordinates. Pass `down` to
152
- // also update the press state.
153
- setPointerFromScreen(
154
- canvas: HTMLCanvasElement,
155
- clientX: number,
156
- clientY: number,
157
- down?: boolean
158
- ): void {
159
- const point = screenToCard(canvas, clientX, clientY);
160
- this.pointer.x = point.x + (this.camera?.x ?? 0);
161
- this.pointer.y = point.y + (this.camera?.y ?? 0);
162
- if (down !== undefined) this.pointer.down = down;
163
- }
164
-
165
- update(dt: number): void {
166
- this.time += dt;
167
- for (const actor of this.getActors()) {
168
- this.forEachBehavior(actor, (instance) => instance.update?.(actor, this, dt));
169
- }
170
- }
171
-
172
- private forEachBehavior(actor: ActorData, callback: (instance: Behavior) => void): void {
173
- for (const [behaviorName, props] of Object.entries(actor.components)) {
174
- if (!props) continue;
175
- const Behavior = this.behaviors.get(behaviorName);
176
- if (!Behavior) continue;
177
- callback(new Behavior(props as never));
178
- }
179
- }
180
-
181
- draw(ctx: CanvasRenderingContext2D, options: SceneDrawOptions = {}): void {
182
- ctx.clearRect(0, 0, CARD_WIDTH, CARD_HEIGHT);
183
- ctx.fillStyle = this.data.background ?? '#0f2a1d';
184
- ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT);
185
-
186
- ctx.save();
187
- if (options.useCamera && this.camera) {
188
- ctx.translate(-Math.round(this.camera.x ?? 0), -Math.round(this.camera.y ?? 0));
189
- }
190
-
191
- for (const actor of this.getActors()) {
192
- // Per-actor transform: Layout's `rotation` (degrees) rotates every draw
193
- // behavior of the actor about its center.
194
- ctx.save();
195
- applyLayoutRotation(ctx, getLayout(actor));
196
- if (options.editPlaceholders) drawEditPlaceholder(ctx, actor);
197
- this.forEachBehavior(actor, (instance) => instance.draw?.(actor, this, ctx, options));
198
- ctx.restore();
199
- }
200
-
201
- if (options.showGrid) drawGrid(ctx);
202
- if (options.selectedActorIds) {
203
- for (const id of options.selectedActorIds) {
204
- const actor = this.getActor(id);
205
- if (actor) drawSelection(ctx, actor);
206
- }
207
- }
208
- if (options.marquee) drawMarquee(ctx, options.marquee);
209
- ctx.restore();
210
- }
211
-
212
- actorAt(x: number, y: number): ActorData | null {
213
- const actors = this.getActors().slice().reverse();
214
- for (const actor of actors) {
215
- const layout = getLayout(actor);
216
- if (!layout) continue;
217
- if (
218
- x >= layout.x &&
219
- x <= layout.x + layout.width &&
220
- y >= layout.y &&
221
- y <= layout.y + layout.height
222
- ) {
223
- return actor;
224
- }
225
- }
226
- return null;
227
- }
228
-
229
- actorIdsInRect(rect: SelectionRect): string[] {
230
- const minX = rect.x;
231
- const minY = rect.y;
232
- const maxX = rect.x + rect.width;
233
- const maxY = rect.y + rect.height;
234
- const ids: string[] = [];
235
- for (const actor of this.getActors()) {
236
- const layout = getLayout(actor);
237
- if (!layout) continue;
238
- if (
239
- layout.x + layout.width >= minX &&
240
- layout.x <= maxX &&
241
- layout.y + layout.height >= minY &&
242
- layout.y <= maxY
243
- ) {
244
- ids.push(actor.id);
245
- }
246
- }
247
- return ids;
248
- }
249
- }
250
-
251
- export function makeScene(
252
- sceneData: SceneData,
253
- behaviors: BehaviorClass[],
254
- drawings: Record<string, DrawingData>
255
- ): SceneRuntime {
256
- return new SceneRuntime(sceneData, behaviors, drawings);
257
- }
258
-
259
- export function setActorComponent<TProps extends ComponentProps>(
260
- sceneData: SceneData,
261
- actorId: string,
262
- behaviorName: string,
263
- nextProps: Partial<TProps>
264
- ): SceneData {
265
- const next = structuredClone(sceneData);
266
- const actor = next.actors.find((candidate) => candidate.id === actorId);
267
- if (!actor) return sceneData;
268
- actor.components[behaviorName] = {
269
- ...(actor.components[behaviorName] ?? {}),
270
- ...nextProps,
271
- };
272
- return next;
273
- }
274
-
275
- export function removeActorComponent(
276
- sceneData: SceneData,
277
- actorId: string,
278
- behaviorName: string
279
- ): SceneData {
280
- const next = structuredClone(sceneData);
281
- const actor = next.actors.find((candidate) => candidate.id === actorId);
282
- if (!actor || !(behaviorName in actor.components)) return sceneData;
283
- delete actor.components[behaviorName];
284
- return next;
285
- }
286
-
287
- export function moveActors(
288
- sceneData: SceneData,
289
- actorIds: string[],
290
- dx: number,
291
- dy: number
292
- ): SceneData {
293
- if (actorIds.length === 0) return sceneData;
294
- const next = structuredClone(sceneData);
295
- let changed = false;
296
- for (const id of actorIds) {
297
- const actor = next.actors.find((candidate) => candidate.id === id);
298
- const layout = actor ? (actor.components.Layout as LayoutProps | undefined) : null;
299
- if (!actor || !layout) continue;
300
- actor.components.Layout = {
301
- ...layout,
302
- x: Math.round(layout.x + dx),
303
- y: Math.round(layout.y + dy),
304
- };
305
- changed = true;
306
- }
307
- return changed ? next : sceneData;
308
- }
309
-
310
- export function removeActors(sceneData: SceneData, actorIds: string[]): SceneData {
311
- if (actorIds.length === 0) return sceneData;
312
- const toRemove = new Set(actorIds);
313
- const next = structuredClone(sceneData);
314
- next.actors = next.actors.filter((actor) => !toRemove.has(actor.id));
315
- return next.actors.length === sceneData.actors.length ? sceneData : next;
316
- }
317
-
318
- export function duplicateActors(
319
- sceneData: SceneData,
320
- actorIds: string[]
321
- ): { sceneData: SceneData; newIds: string[] } {
322
- if (actorIds.length === 0) return { sceneData, newIds: [] };
323
- const next = structuredClone(sceneData);
324
- const existingIds = new Set(next.actors.map((actor) => actor.id));
325
- const newIds: string[] = [];
326
- for (const id of actorIds) {
327
- const source = next.actors.find((candidate) => candidate.id === id);
328
- if (!source) continue;
329
- const copy = structuredClone(source);
330
- copy.id = mintActorId(existingIds);
331
- existingIds.add(copy.id);
332
- const layout = copy.components.Layout as LayoutProps | undefined;
333
- if (layout) {
334
- copy.components.Layout = {
335
- ...layout,
336
- x: Math.round(layout.x + 1),
337
- y: Math.round(layout.y + 1),
338
- };
339
- }
340
- next.actors.push(copy);
341
- newIds.push(copy.id);
342
- }
343
- return { sceneData: next, newIds };
344
- }
345
-
346
- export function addActor(
347
- sceneData: SceneData,
348
- _files: FileMap,
349
- drawings: Record<string, DrawingData>
350
- ): { sceneData: SceneData; newId: string } {
351
- const next = structuredClone(sceneData);
352
- const existingIds = new Set(next.actors.map((actor) => actor.id));
353
- const id = mintActorId(existingIds);
354
- const width = 64;
355
- const height = 64;
356
- const layout: LayoutProps = {
357
- x: Math.round((CARD_WIDTH - width) / 2),
358
- y: Math.round((CARD_HEIGHT - height) / 2),
359
- width,
360
- height,
361
- z: 0,
362
- rotation: 0,
363
- };
364
- const components: ActorData['components'] = { Layout: layout };
365
- const drawingPath = Object.keys(drawings).sort()[0];
366
- if (drawingPath) {
367
- const drawing: DrawingProps = { file: drawingPath };
368
- components.Drawing = drawing;
369
- }
370
- next.actors.push({ id, components });
371
- return { sceneData: next, newId: id };
372
- }
373
-
374
- function mintActorId(existing: Set<string>): string {
375
- for (let attempt = 0; attempt < 64; attempt++) {
376
- const candidate = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, '0');
377
- if (!existing.has(candidate)) return candidate;
378
- }
379
- return `${Date.now().toString(16)}${Math.floor(Math.random() * 0xffff).toString(16)}`;
380
- }
381
-
382
- export function screenToCard(
383
- canvas: HTMLCanvasElement,
384
- clientX: number,
385
- clientY: number
386
- ): { x: number; y: number } {
387
- const rect = canvas.getBoundingClientRect();
388
- return {
389
- x: ((clientX - rect.left) / rect.width) * CARD_WIDTH,
390
- y: ((clientY - rect.top) / rect.height) * CARD_HEIGHT,
391
- };
392
- }
393
-
394
- export const cardSize = { width: CARD_WIDTH, height: CARD_HEIGHT };
395
-
396
- export function configureSceneCanvas(
397
- canvas: HTMLCanvasElement,
398
- ctx = canvas.getContext('2d')
399
- ): CanvasRenderingContext2D | null {
400
- if (!ctx) return null;
401
- const rect = canvas.getBoundingClientRect();
402
- const dpr = window.devicePixelRatio || 1;
403
- const displayWidth = rect.width || CARD_WIDTH;
404
- const displayHeight = rect.height || CARD_HEIGHT;
405
- const pixelWidth = Math.max(1, Math.round(displayWidth * dpr));
406
- const pixelHeight = Math.max(1, Math.round(displayHeight * dpr));
407
- if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
408
- if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
409
- ctx.setTransform(pixelWidth / CARD_WIDTH, 0, 0, pixelHeight / CARD_HEIGHT, 0, 0);
410
- ctx.imageSmoothingEnabled = true;
411
- return ctx;
412
- }
413
-
414
- function drawGrid(ctx: CanvasRenderingContext2D): void {
415
- ctx.save();
416
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
417
- ctx.lineWidth = 1;
418
- for (let x = 0; x <= CARD_WIDTH; x += 25) {
419
- ctx.beginPath();
420
- ctx.moveTo(x + 0.5, 0);
421
- ctx.lineTo(x + 0.5, CARD_HEIGHT);
422
- ctx.stroke();
423
- }
424
- for (let y = 0; y <= CARD_HEIGHT; y += 25) {
425
- ctx.beginPath();
426
- ctx.moveTo(0, y + 0.5);
427
- ctx.lineTo(CARD_WIDTH, y + 0.5);
428
- ctx.stroke();
429
- }
430
- ctx.restore();
431
- }
432
-
433
- function drawEditPlaceholder(ctx: CanvasRenderingContext2D, actor: ActorData): void {
434
- const layout = getLayout(actor);
435
- if (!layout) return;
436
- ctx.save();
437
- ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
438
- ctx.fillRect(layout.x, layout.y, layout.width, layout.height);
439
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
440
- ctx.lineWidth = 1;
441
- ctx.strokeRect(layout.x + 0.5, layout.y + 0.5, layout.width - 1, layout.height - 1);
442
- ctx.restore();
443
- }
444
-
445
- function drawMarquee(ctx: CanvasRenderingContext2D, rect: SelectionRect): void {
446
- if (rect.width === 0 || rect.height === 0) return;
447
- ctx.save();
448
- ctx.fillStyle = 'rgba(255, 255, 255, 0.12)';
449
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
450
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
451
- ctx.lineWidth = 1;
452
- ctx.setLineDash([4, 3]);
453
- ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1);
454
- ctx.restore();
455
- }
456
-
457
- function drawSelection(ctx: CanvasRenderingContext2D, actor: ActorData): void {
458
- const layout = getLayout(actor);
459
- if (!layout) return;
460
- ctx.save();
461
- applyLayoutRotation(ctx, layout);
462
- ctx.strokeStyle = '#fff';
463
- ctx.lineWidth = 2;
464
- ctx.setLineDash([6, 4]);
465
- ctx.strokeRect(layout.x + 1, layout.y + 1, layout.width - 2, layout.height - 2);
466
- ctx.restore();
467
- }
468
-
469
- function applyLayoutRotation(
470
- ctx: CanvasRenderingContext2D,
471
- layout: LayoutProps | undefined
472
- ): void {
473
- const rotation = layout?.rotation ?? 0;
474
- if (!rotation || !layout) return;
475
- const cx = layout.x + layout.width / 2;
476
- const cy = layout.y + layout.height / 2;
477
- ctx.translate(cx, cy);
478
- ctx.rotate((rotation * Math.PI) / 180);
479
- ctx.translate(-cx, -cy);
480
- }
481
-
482
- function getLayout(actor: ActorData): LayoutProps | undefined {
483
- return actor.components.Layout as LayoutProps | undefined;
484
- }