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,41 +0,0 @@
1
- {
2
- "name": "basic-2d",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "serve": "node ../../cli/dist/index.js serve . --open",
7
- "restart": "node ../../cli/dist/index.js restart .",
8
- "screenshot": "node ../../cli/dist/index.js screenshot .",
9
- "check": "eslint . && jscpd && node --input-type=module -e \"const { bundleProject } = await import('../../cli/dist/bundle.js'); await bundleProject('.');\""
10
- },
11
- "jscpd": {
12
- "path": [
13
- "engine",
14
- "editors",
15
- "behaviors"
16
- ],
17
- "threshold": 0,
18
- "reporters": [
19
- "consoleFull"
20
- ]
21
- },
22
- "dependencies": {
23
- "@codemirror/commands": "^6.10.3",
24
- "@codemirror/lang-javascript": "^6.2.5",
25
- "@codemirror/language": "^6.12.3",
26
- "@codemirror/state": "^6.6.0",
27
- "@codemirror/view": "^6.41.1",
28
- "@fortawesome/free-solid-svg-icons": "^5.15.4",
29
- "@lezer/highlight": "^1.2.3",
30
- "castle-web-sdk": "file:../../sdk",
31
- "codemirror": "^6.0.2",
32
- "react": "^19.2.4",
33
- "react-dom": "^19.2.4"
34
- },
35
- "devDependencies": {
36
- "eslint": "^9.0.0",
37
- "eslint-plugin-react-hooks": "^5.0.0",
38
- "jscpd": "^4.0.5",
39
- "prettier": "^3.8.3"
40
- }
41
- }
@@ -1,108 +0,0 @@
1
- {
2
- "name": "Main Scene",
3
- "background": "#1b2030",
4
- "actors": [
5
- {
6
- "id": "actor_1",
7
- "components": {
8
- "Layout": {
9
- "x": 80,
10
- "y": 115,
11
- "width": 64,
12
- "height": 64,
13
- "z": 0,
14
- "rotation": 0
15
- },
16
- "Drawing": {
17
- "file": "drawings/floor.drawing",
18
- "tint": "#ff6b6bff"
19
- }
20
- }
21
- },
22
- {
23
- "id": "actor_2",
24
- "components": {
25
- "Layout": {
26
- "x": 172,
27
- "y": 160,
28
- "width": 48,
29
- "height": 48,
30
- "z": 0,
31
- "rotation": 0
32
- },
33
- "Drawing": {
34
- "file": "drawings/floor.drawing",
35
- "tint": "#ffd166ff"
36
- }
37
- }
38
- },
39
- {
40
- "id": "actor_3",
41
- "components": {
42
- "Layout": {
43
- "x": 229,
44
- "y": 63,
45
- "width": 80,
46
- "height": 56,
47
- "z": 0,
48
- "rotation": 0
49
- },
50
- "Drawing": {
51
- "file": "drawings/floor.drawing",
52
- "tint": "#06d6a0ff"
53
- }
54
- }
55
- },
56
- {
57
- "id": "actor_4",
58
- "components": {
59
- "Layout": {
60
- "x": 184,
61
- "y": 239,
62
- "width": 56,
63
- "height": 56,
64
- "z": 0,
65
- "rotation": 0
66
- },
67
- "Drawing": {
68
- "file": "drawings/floor.drawing",
69
- "tint": "#4dabf7ff"
70
- }
71
- }
72
- },
73
- {
74
- "id": "actor_5",
75
- "components": {
76
- "Layout": {
77
- "x": 324,
78
- "y": 259,
79
- "width": 64,
80
- "height": 80,
81
- "z": 0,
82
- "rotation": 0
83
- },
84
- "Drawing": {
85
- "file": "drawings/floor.drawing",
86
- "tint": "#b197fcff"
87
- }
88
- }
89
- },
90
- {
91
- "id": "actor_6",
92
- "components": {
93
- "Layout": {
94
- "x": 292,
95
- "y": 389,
96
- "width": 96,
97
- "height": 64,
98
- "z": 0,
99
- "rotation": 0
100
- },
101
- "Drawing": {
102
- "file": "drawings/floor.drawing",
103
- "tint": "#f06595ff"
104
- }
105
- }
106
- }
107
- ]
108
- }
@@ -1 +0,0 @@
1
- export default {};
@@ -1,8 +0,0 @@
1
- {
2
- "printWidth": 100,
3
- "tabWidth": 2,
4
- "singleQuote": true,
5
- "bracketSameLine": true,
6
- "trailingComma": "es5",
7
- "arrowParens": "always"
8
- }
@@ -1,52 +0,0 @@
1
- import { cardSize } from '../engine/scene';
2
- import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
3
- import type { LayoutProps } from './Layout.tsx';
4
-
5
- export interface CameraProps {
6
- target: string;
7
- followX: boolean;
8
- followY: boolean;
9
- // Room bounds in card units. The camera is clamped so it never scrolls
10
- // past the room edges.
11
- roomWidth: number;
12
- roomHeight: number;
13
- }
14
-
15
- // Follow camera: keeps the target actor centered in the viewport, clamped to
16
- // the room bounds so the empty area past the room edges never scrolls in.
17
- export class Camera implements Behavior<CameraProps> {
18
- static behaviorName = 'Camera';
19
- static defaultProps: CameraProps = {
20
- target: '',
21
- followX: true,
22
- followY: true,
23
- roomWidth: 900,
24
- roomHeight: 1100,
25
- };
26
-
27
- props: CameraProps;
28
-
29
- constructor(props: CameraProps) {
30
- this.props = props;
31
- }
32
-
33
- update(_actor: ActorData, scene: SceneRuntime): void {
34
- const target = this.props.target ? scene.getActor(this.props.target) : undefined;
35
- const targetLayout = target?.components.Layout as LayoutProps | undefined;
36
- if (!targetLayout) return;
37
-
38
- const centerX = targetLayout.x + targetLayout.width / 2;
39
- const centerY = targetLayout.y + targetLayout.height / 2;
40
- const desiredX = centerX - cardSize.width / 2;
41
- const desiredY = centerY - cardSize.height / 2;
42
-
43
- scene.camera = {
44
- x: this.props.followX ? clamp(desiredX, 0, this.props.roomWidth - cardSize.width) : 0,
45
- y: this.props.followY ? clamp(desiredY, 0, this.props.roomHeight - cardSize.height) : 0,
46
- };
47
- }
48
- }
49
-
50
- function clamp(value: number, min: number, max: number): number {
51
- return Math.max(min, Math.min(Math.max(min, max), value));
52
- }
@@ -1,98 +0,0 @@
1
- import React from 'react';
2
- import { Panel, SelectField } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
- import type { ActorData, Behavior, SceneDrawOptions, SceneRuntime } from '../engine/scene';
5
- import type { LayoutProps } from './Layout.tsx';
6
-
7
- export type ColliderKind = 'solid' | 'pickup';
8
-
9
- export interface ColliderProps {
10
- kind: ColliderKind;
11
- width: number;
12
- height: number;
13
- offsetX?: number;
14
- offsetY?: number;
15
- debug?: boolean;
16
- }
17
-
18
- export interface Rect {
19
- x: number;
20
- y: number;
21
- width: number;
22
- height: number;
23
- }
24
-
25
- export class Collider implements Behavior<ColliderProps> {
26
- static behaviorName = 'Collider';
27
- static defaultProps: ColliderProps = {
28
- kind: 'solid',
29
- width: 32,
30
- height: 32,
31
- offsetX: 0,
32
- offsetY: 0,
33
- debug: false,
34
- };
35
-
36
- props: ColliderProps;
37
-
38
- constructor(props: ColliderProps) {
39
- this.props = props;
40
- }
41
-
42
- draw(
43
- actor: ActorData,
44
- _scene: SceneRuntime,
45
- ctx: CanvasRenderingContext2D,
46
- options: SceneDrawOptions
47
- ): void {
48
- if (!options.showDebugColliders && !this.props.debug) return;
49
- const rect = getColliderRect(actor);
50
- if (!rect) return;
51
- ctx.save();
52
- ctx.strokeStyle = this.props.kind === 'pickup' ? '#ffe17a' : '#8db7ff';
53
- ctx.lineWidth = 2;
54
- ctx.strokeRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
55
- ctx.restore();
56
- }
57
-
58
- static Inspector({
59
- component,
60
- setComponent,
61
- }: {
62
- component: ColliderProps;
63
- setComponent: (nextProps: Partial<ColliderProps>) => void;
64
- }) {
65
- return (
66
- <Panel title="Collider">
67
- <SelectField
68
- label="Kind"
69
- value={component.kind}
70
- onChange={(kind) => setComponent({ kind: kind as ColliderKind })}
71
- options={['solid', 'pickup']}
72
- />
73
- <AutoFields
74
- defaultProps={Collider.defaultProps}
75
- component={component}
76
- setComponent={setComponent}
77
- exclude={['kind']}
78
- />
79
- </Panel>
80
- );
81
- }
82
- }
83
-
84
- export function getColliderRect(actor: ActorData): Rect | null {
85
- const layout = actor.components.Layout as LayoutProps | undefined;
86
- const collider = actor.components.Collider as ColliderProps | undefined;
87
- if (!layout || !collider) return null;
88
- return {
89
- x: layout.x + (collider.offsetX ?? 0),
90
- y: layout.y + (collider.offsetY ?? 0),
91
- width: collider.width ?? layout.width,
92
- height: collider.height ?? layout.height,
93
- };
94
- }
95
-
96
- export function intersects(a: Rect, b: Rect): boolean {
97
- return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
98
- }
@@ -1,184 +0,0 @@
1
- import React from 'react';
2
- import { Panel } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
- import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
5
-
6
- export interface DialogProps {
7
- // One line per text-box page. Player advances with E or click.
8
- lines: string[];
9
- // Who is speaking -- rendered as a label above the text box.
10
- speaker: string;
11
- // Card-px range from the player center at which this dialog auto-opens.
12
- // Falls back to the player's PlayerController.interactRange when 0.
13
- range: number;
14
- }
15
-
16
- interface DialogRuntime extends Record<string, unknown> {
17
- open: boolean;
18
- page: number;
19
- advancePressed: boolean;
20
- pointerWasDown: boolean;
21
- // Was the player inside the proximity radius on the previous frame? The
22
- // dialog opens on the rising edge of this -- i.e., when the player first
23
- // walks into range. Bloom-run's overlap-trigger pattern.
24
- wasInside: boolean;
25
- // Once the player has finished the conversation it never auto-opens
26
- // again for this scene session, even on re-entry.
27
- done: boolean;
28
- }
29
-
30
- // Proximity-trigger dialog. When the player walks within `range`, the dialog
31
- // auto-opens to page 0. E or click advance pages; finishing the last page
32
- // closes the box and locks it shut until the player leaves the radius and
33
- // walks back in. (Space is reserved for the editor's play/stop toggle.)
34
- export class Dialog implements Behavior<DialogProps> {
35
- static behaviorName = 'Dialog';
36
- static defaultProps: DialogProps = {
37
- lines: ['...'],
38
- speaker: '',
39
- range: 0,
40
- };
41
-
42
- props: DialogProps;
43
-
44
- constructor(props: DialogProps) {
45
- this.props = props;
46
- }
47
-
48
- update(actor: ActorData, scene: SceneRuntime): void {
49
- const runtime = getDialogRuntime(actor);
50
-
51
- const player = findPlayer(scene);
52
- const range = this.props.range || getPlayerRange(player);
53
- const inside = player ? actorCenterDistance(actor, player) <= range : false;
54
-
55
- // Rising-edge entry: open and arm against the input edge that may have
56
- // accompanied the walk-in (click / held E). Once `done`, the
57
- // conversation is over for this session and never auto-reopens.
58
- if (inside && !runtime.wasInside && !runtime.open && !runtime.done) {
59
- runtime.open = true;
60
- runtime.page = 0;
61
- runtime.advancePressed = true;
62
- runtime.pointerWasDown = true;
63
- }
64
-
65
- if (runtime.open) {
66
- // Space is intentionally NOT an advance key -- the editor uses it to
67
- // toggle play/stop, so consuming it here would fight the editor's
68
- // global shortcut. E and click only.
69
- const pressed = scene.keys.has('e') || scene.keys.has('E');
70
- const pointerDown = scene.pointer.down;
71
- const keyEdge = pressed && !runtime.advancePressed;
72
- const pointerEdge = pointerDown && !runtime.pointerWasDown;
73
- if (keyEdge || pointerEdge) {
74
- runtime.page += 1;
75
- if (runtime.page >= this.props.lines.length) {
76
- runtime.open = false;
77
- runtime.page = 0;
78
- // Conversation finished -- permanently lock this dialog for the
79
- // session so walking back into range does not re-open it.
80
- runtime.done = true;
81
- runtime.wasInside = true;
82
- runtime.advancePressed = pressed;
83
- runtime.pointerWasDown = pointerDown;
84
- return;
85
- }
86
- }
87
- runtime.advancePressed = pressed;
88
- runtime.pointerWasDown = pointerDown;
89
- // Hold wasInside true while open so a brief out-of-range glitch can't
90
- // re-trigger anything weird mid-dialog.
91
- runtime.wasInside = true;
92
- return;
93
- }
94
-
95
- runtime.wasInside = inside;
96
- }
97
-
98
- ui(actor: ActorData): React.ReactNode {
99
- const runtime = actor.runtime as DialogRuntime | undefined;
100
- if (!runtime?.open) return null;
101
- const line = this.props.lines[runtime.page] ?? '';
102
- return (
103
- <div
104
- style={{
105
- position: 'absolute',
106
- left: 16,
107
- right: 16,
108
- bottom: 16,
109
- padding: '14px 18px',
110
- background: 'rgba(12, 16, 24, 0.92)',
111
- color: '#f4f1de',
112
- border: '2px solid rgba(255, 255, 255, 0.6)',
113
- borderRadius: 8,
114
- font: '14px/1.4 system-ui, sans-serif',
115
- pointerEvents: 'none',
116
- }}
117
- >
118
- {this.props.speaker ? (
119
- <div style={{ fontWeight: 700, marginBottom: 6, color: '#ffd166' }}>
120
- {this.props.speaker}
121
- </div>
122
- ) : null}
123
- <div>{line}</div>
124
- <div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
125
- {runtime.page + 1} / {this.props.lines.length} -- press E or click
126
- </div>
127
- </div>
128
- );
129
- }
130
-
131
- static Inspector({
132
- component,
133
- setComponent,
134
- }: {
135
- component: DialogProps;
136
- setComponent: (nextProps: Partial<DialogProps>) => void;
137
- }) {
138
- return (
139
- <Panel title="Dialog">
140
- <AutoFields
141
- defaultProps={Dialog.defaultProps}
142
- component={component}
143
- setComponent={setComponent}
144
- />
145
- </Panel>
146
- );
147
- }
148
- }
149
-
150
- function getDialogRuntime(actor: ActorData): DialogRuntime {
151
- if (!actor.runtime) actor.runtime = {};
152
- const runtime = actor.runtime as DialogRuntime;
153
- if (typeof runtime.open !== 'boolean') runtime.open = false;
154
- if (typeof runtime.page !== 'number') runtime.page = 0;
155
- if (typeof runtime.advancePressed !== 'boolean') runtime.advancePressed = false;
156
- if (typeof runtime.pointerWasDown !== 'boolean') runtime.pointerWasDown = false;
157
- if (typeof runtime.wasInside !== 'boolean') runtime.wasInside = false;
158
- if (typeof runtime.done !== 'boolean') runtime.done = false;
159
- return runtime;
160
- }
161
-
162
- function findPlayer(scene: SceneRuntime): ActorData | null {
163
- for (const candidate of scene.getActors()) {
164
- if (candidate.components.PlayerController) return candidate;
165
- }
166
- return null;
167
- }
168
-
169
- function getPlayerRange(player: ActorData | null): number {
170
- if (!player) return 120;
171
- const props = player.components.PlayerController as { interactRange?: number } | undefined;
172
- return props?.interactRange ?? 120;
173
- }
174
-
175
- function actorCenterDistance(a: ActorData, b: ActorData): number {
176
- const la = a.components.Layout as { x: number; y: number; width: number; height: number } | undefined;
177
- const lb = b.components.Layout as { x: number; y: number; width: number; height: number } | undefined;
178
- if (!la || !lb) return Infinity;
179
- const ax = la.x + la.width / 2;
180
- const ay = la.y + la.height / 2;
181
- const bx = lb.x + lb.width / 2;
182
- const by = lb.y + lb.height / 2;
183
- return Math.hypot(ax - bx, ay - by);
184
- }
@@ -1,161 +0,0 @@
1
- import React from 'react';
2
- import { Panel, SelectField } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
- import { compositeFrame, frameIndexAt, parseHexColor, toHexColor } from '../engine/drawing';
5
- import type { DrawingData, FileMap } from '../engine/files';
6
- import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
7
- import type { LayoutProps } from './Layout.tsx';
8
-
9
- export interface DrawingProps {
10
- file: string;
11
- tint?: string;
12
- }
13
-
14
- // Renders the actor's drawing -- a layered, optionally animated pixel-art
15
- // sprite. Pulls the active frame from the drawing's playMode + fps + scene
16
- // time, composites visible layers, optionally tints, and blits.
17
- export class Drawing implements Behavior<DrawingProps> {
18
- static behaviorName = 'Drawing';
19
- static defaultProps: DrawingProps = {
20
- file: 'drawings/floor.drawing',
21
- tint: '#ffffffff',
22
- };
23
-
24
- props: DrawingProps;
25
-
26
- constructor(props: DrawingProps) {
27
- this.props = props;
28
- }
29
-
30
- draw(actor: ActorData, scene: SceneRuntime, ctx: CanvasRenderingContext2D): void {
31
- if (actor.runtime?.collected) return;
32
- const layout = actor.components.Layout as LayoutProps | undefined;
33
- if (!layout) return;
34
-
35
- const drawing = scene.drawings[this.props.file];
36
- if (!drawing) return;
37
-
38
- const frameIndex = frameIndexAt(drawing, scene.time);
39
- drawAnimatedDrawing(
40
- ctx,
41
- drawing,
42
- frameIndex,
43
- layout.x,
44
- layout.y,
45
- layout.width,
46
- layout.height,
47
- this.props.tint
48
- );
49
- }
50
-
51
- static Inspector({
52
- component,
53
- setComponent,
54
- files,
55
- }: {
56
- component: DrawingProps;
57
- files: FileMap;
58
- setComponent: (nextProps: Partial<DrawingProps>) => void;
59
- }) {
60
- const drawingFiles = getDrawingFiles(files);
61
- return (
62
- <Panel title="Drawing">
63
- <SelectField
64
- label="File"
65
- value={component.file}
66
- onChange={(file) => setComponent({ file })}
67
- options={drawingFiles}
68
- />
69
- <AutoFields
70
- defaultProps={Drawing.defaultProps}
71
- component={component}
72
- setComponent={setComponent}
73
- only={['tint']}
74
- />
75
- </Panel>
76
- );
77
- }
78
- }
79
-
80
- // Composite the visible layers of `frameIndex` and blit the result, tinted,
81
- // as one image. A single `drawImage` (vs per-pixel fillRect on the main ctx)
82
- // avoids hairline seams under any actor rotation.
83
- export function drawAnimatedDrawing(
84
- ctx: CanvasRenderingContext2D,
85
- drawing: DrawingData,
86
- frameIndex: number,
87
- x: number,
88
- y: number,
89
- width: number,
90
- height: number,
91
- tint?: string
92
- ): void {
93
- if (drawing.width <= 0 || drawing.height <= 0) return;
94
- const canvas = getFrameCanvas(drawing, frameIndex, tint);
95
- const prevSmoothing = ctx.imageSmoothingEnabled;
96
- ctx.imageSmoothingEnabled = false;
97
- ctx.drawImage(canvas, x, y, width, height);
98
- ctx.imageSmoothingEnabled = prevSmoothing;
99
- }
100
-
101
- // Cache of native-resolution offscreen canvases keyed by drawing object, then
102
- // by `frameIndex|tint`. A drawing edit produces a fresh DrawingData object,
103
- // so a new object naturally misses the WeakMap and rebuilds.
104
- const frameCanvasCache = new WeakMap<DrawingData, Map<string, HTMLCanvasElement>>();
105
-
106
- function getFrameCanvas(
107
- drawing: DrawingData,
108
- frameIndex: number,
109
- tint?: string
110
- ): HTMLCanvasElement {
111
- const key = `${frameIndex}|${tint ?? ''}`;
112
- let byKey = frameCanvasCache.get(drawing);
113
- if (!byKey) {
114
- byKey = new Map();
115
- frameCanvasCache.set(drawing, byKey);
116
- }
117
- const cached = byKey.get(key);
118
- if (cached) return cached;
119
-
120
- const canvas = document.createElement('canvas');
121
- canvas.width = drawing.width;
122
- canvas.height = drawing.height;
123
- const octx = canvas.getContext('2d');
124
- if (octx) {
125
- const composited = compositeFrame(drawing, frameIndex);
126
- const tintRgba = parseTint(tint);
127
- for (let py = 0; py < drawing.height; py++) {
128
- for (let px = 0; px < drawing.width; px++) {
129
- const color = composited[py * drawing.width + px];
130
- if (!color || color.endsWith('00')) continue;
131
- octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
132
- octx.fillRect(px, py, 1, 1);
133
- }
134
- }
135
- }
136
- byKey.set(key, canvas);
137
- return canvas;
138
- }
139
-
140
- function parseTint(tint?: string): [number, number, number, number] | null {
141
- if (!tint) return null;
142
- const rgba = parseHexColor(tint);
143
- if (!rgba) return null;
144
- if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) return null;
145
- return rgba;
146
- }
147
-
148
- function applyTint(color: string, tint: [number, number, number, number]): string {
149
- const rgba = parseHexColor(color);
150
- if (!rgba) return color;
151
- return toHexColor([
152
- (rgba[0] * tint[0]) / 255,
153
- (rgba[1] * tint[1]) / 255,
154
- (rgba[2] * tint[2]) / 255,
155
- (rgba[3] * tint[3]) / 255,
156
- ]);
157
- }
158
-
159
- function getDrawingFiles(files: FileMap): string[] {
160
- return Object.keys(files).filter((path) => path.endsWith('.drawing'));
161
- }