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.
- package/dist/index.js +8 -2
- package/dist/init.js +9 -6
- package/package.json +1 -1
- package/kits/basic-2d-frozen/.prettierrc +0 -8
- package/kits/basic-2d-frozen/CLAUDE.md +0 -131
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
- package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
- package/kits/basic-2d-frozen/editors/App.jsx +0 -152
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
- package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
- package/kits/basic-2d-frozen/engine/files.js +0 -62
- package/kits/basic-2d-frozen/engine/scene.js +0 -420
- package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
- package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
- package/kits/basic-2d-frozen/eslint.config.js +0 -50
- package/kits/basic-2d-frozen/index.html +0 -11
- package/kits/basic-2d-frozen/main.jsx +0 -10
- package/kits/basic-2d-frozen/package-lock.json +0 -2706
- package/kits/basic-2d-frozen/package.json +0 -41
- package/kits/basic-2d-frozen/scenes/main.scene +0 -108
- package/kits/basic-2d-frozen/vite.config.js +0 -1
- package/kits/rpg-2d/.prettierrc +0 -8
- package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
- package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
- package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
- package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
- package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
- package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
- package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
- package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
- package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
- package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
- package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
- package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
- package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
- package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
- package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
- package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
- package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
- package/kits/rpg-2d/drawings/floor.drawing +0 -70
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
- package/kits/rpg-2d/editors/App.tsx +0 -163
- package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
- package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
- package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
- package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
- package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
- package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
- package/kits/rpg-2d/editors/editorHistory.ts +0 -75
- package/kits/rpg-2d/editors/editorProps.ts +0 -10
- package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
- package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
- package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
- package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
- package/kits/rpg-2d/engine/drawing.ts +0 -81
- package/kits/rpg-2d/engine/files.ts +0 -215
- package/kits/rpg-2d/engine/scene.ts +0 -484
- package/kits/rpg-2d/engine/ui.module.css +0 -928
- package/kits/rpg-2d/engine/ui.tsx +0 -483
- package/kits/rpg-2d/eslint.config.js +0 -46
- package/kits/rpg-2d/index.html +0 -11
- package/kits/rpg-2d/main.tsx +0 -14
- package/kits/rpg-2d/package-lock.json +0 -3149
- package/kits/rpg-2d/package.json +0 -46
- package/kits/rpg-2d/scenes/main.scene +0 -203
- package/kits/rpg-2d/tsconfig.json +0 -17
- package/kits/rpg-2d/vite-env.d.ts +0 -7
- 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 {};
|
package/kits/rpg-2d/.prettierrc
DELETED
|
@@ -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
|
-
}
|