castle-web-cli 0.4.2 → 0.4.4

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 (96) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +9 -6
  3. package/package.json +1 -1
  4. package/kits/basic-2d-frozen/.prettierrc +0 -8
  5. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  6. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  7. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  8. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  9. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  10. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  11. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  12. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  13. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  14. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  15. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  16. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  17. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  18. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  19. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  20. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  21. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  22. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  23. package/kits/basic-2d-frozen/engine/files.js +0 -62
  24. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  25. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  26. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  27. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  28. package/kits/basic-2d-frozen/index.html +0 -11
  29. package/kits/basic-2d-frozen/main.jsx +0 -10
  30. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  31. package/kits/basic-2d-frozen/package.json +0 -41
  32. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  33. package/kits/basic-2d-frozen/vite.config.js +0 -1
  34. package/kits/rpg-2d/.prettierrc +0 -8
  35. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  36. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  37. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  38. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  39. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  40. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  41. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  42. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  43. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  44. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  45. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  46. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  47. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  48. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  49. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  50. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  51. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  52. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  53. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  55. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  56. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  57. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  58. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  59. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  60. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  61. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  62. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  63. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  67. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  70. package/kits/rpg-2d/editors/App.tsx +0 -163
  71. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  72. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  73. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  74. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  75. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  76. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  77. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  78. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  79. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  80. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  81. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  82. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  83. package/kits/rpg-2d/engine/drawing.ts +0 -81
  84. package/kits/rpg-2d/engine/files.ts +0 -215
  85. package/kits/rpg-2d/engine/scene.ts +0 -484
  86. package/kits/rpg-2d/engine/ui.module.css +0 -928
  87. package/kits/rpg-2d/engine/ui.tsx +0 -483
  88. package/kits/rpg-2d/eslint.config.js +0 -46
  89. package/kits/rpg-2d/index.html +0 -11
  90. package/kits/rpg-2d/main.tsx +0 -14
  91. package/kits/rpg-2d/package-lock.json +0 -3149
  92. package/kits/rpg-2d/package.json +0 -46
  93. package/kits/rpg-2d/scenes/main.scene +0 -203
  94. package/kits/rpg-2d/tsconfig.json +0 -17
  95. package/kits/rpg-2d/vite-env.d.ts +0 -7
  96. 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
- }