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,45 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import type { Behavior } from '../engine/scene';
|
|
5
|
-
|
|
6
|
-
export interface FriendProps {
|
|
7
|
-
// Display name -- shown above the NPC by the editor / inspector. Not used by
|
|
8
|
-
// dialog UI; the speaker label lives on Dialog itself.
|
|
9
|
-
name: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Marker behavior: tags an actor as a talkable NPC. PlayerController scans
|
|
13
|
-
// for actors with this component (or a Dialog) when picking the interact
|
|
14
|
-
// target on E / Space. Pair with Dialog to give the NPC something to say.
|
|
15
|
-
// Relationship-meter math from the cozy-2d Friend is intentionally stripped.
|
|
16
|
-
export class Friend implements Behavior<FriendProps> {
|
|
17
|
-
static behaviorName = 'Friend';
|
|
18
|
-
static defaultProps: FriendProps = {
|
|
19
|
-
name: 'Friend',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
props: FriendProps;
|
|
23
|
-
|
|
24
|
-
constructor(props: FriendProps) {
|
|
25
|
-
this.props = props;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static Inspector({
|
|
29
|
-
component,
|
|
30
|
-
setComponent,
|
|
31
|
-
}: {
|
|
32
|
-
component: FriendProps;
|
|
33
|
-
setComponent: (nextProps: Partial<FriendProps>) => void;
|
|
34
|
-
}) {
|
|
35
|
-
return (
|
|
36
|
-
<Panel title="Friend">
|
|
37
|
-
<AutoFields
|
|
38
|
-
defaultProps={Friend.defaultProps}
|
|
39
|
-
component={component}
|
|
40
|
-
setComponent={setComponent}
|
|
41
|
-
/>
|
|
42
|
-
</Panel>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Behavior } from '../engine/scene';
|
|
2
|
-
|
|
3
|
-
export interface LayoutProps {
|
|
4
|
-
x: number;
|
|
5
|
-
y: number;
|
|
6
|
-
width: number;
|
|
7
|
-
height: number;
|
|
8
|
-
z?: number;
|
|
9
|
-
// Rotation in degrees, applied about the actor center when drawing.
|
|
10
|
-
rotation?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class Layout implements Behavior<LayoutProps> {
|
|
14
|
-
static behaviorName = 'Layout';
|
|
15
|
-
static defaultProps: LayoutProps = {
|
|
16
|
-
x: 0,
|
|
17
|
-
y: 0,
|
|
18
|
-
width: 32,
|
|
19
|
-
height: 32,
|
|
20
|
-
z: 0,
|
|
21
|
-
rotation: 0,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
props: LayoutProps;
|
|
25
|
-
|
|
26
|
-
constructor(props: LayoutProps) {
|
|
27
|
-
this.props = props;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
|
|
2
|
-
import { getColliderRect, intersects } from './Collider';
|
|
3
|
-
import type { ColliderProps } from './Collider';
|
|
4
|
-
import type { DrawingProps } from './Drawing';
|
|
5
|
-
import type { LayoutProps } from './Layout.tsx';
|
|
6
|
-
|
|
7
|
-
export type Facing = 'down' | 'up' | 'left' | 'right';
|
|
8
|
-
|
|
9
|
-
export interface PlayerControllerProps {
|
|
10
|
-
speed: number;
|
|
11
|
-
// Prefix for walk/idle drawing files. The controller swaps the actor's
|
|
12
|
-
// Drawing component to `${spritePrefix}-${state}-${facing}.drawing` based on
|
|
13
|
-
// movement -- e.g. `drawings/player` resolves to `drawings/player-walk-up.drawing`.
|
|
14
|
-
spritePrefix: string;
|
|
15
|
-
// Card-px range at which proximity-triggered behaviors (Dialog, Portal,
|
|
16
|
-
// etc.) consider the player "near". Dialog reads this off the player to
|
|
17
|
-
// decide when to auto-open.
|
|
18
|
-
interactRange: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface PlayerRuntime extends Record<string, unknown> {
|
|
22
|
-
facing: Facing;
|
|
23
|
-
moving: boolean;
|
|
24
|
-
// Tap-to-move destination in world coords; null when key-driven or idle.
|
|
25
|
-
moveTargetX: number | null;
|
|
26
|
-
moveTargetY: number | null;
|
|
27
|
-
// Tracks the pointer.down rising/falling edge so a tap registers once per
|
|
28
|
-
// press and so a release on a held drag can stop the player.
|
|
29
|
-
pointerWasDown: boolean;
|
|
30
|
-
// Seconds the current press has been held -- separates a quick tap
|
|
31
|
-
// (single move-to-point) from a held drag (re-target every frame).
|
|
32
|
-
pressTime: number;
|
|
33
|
-
// While true, each frame re-aims the ground move target at the live
|
|
34
|
-
// pointer position so a held drag continuously steers the player.
|
|
35
|
-
dragFollow: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Arrival tolerance for tap-to-move (card px). When the player is within this
|
|
39
|
-
// distance of the target, the walk ends.
|
|
40
|
-
const ARRIVE_EPS = 6;
|
|
41
|
-
// A press released within this many seconds counts as a quick tap (single
|
|
42
|
-
// move-to-point); anything longer is a held drag that stops on release.
|
|
43
|
-
const QUICK_TAP_SEC = 0.18;
|
|
44
|
-
|
|
45
|
-
// Top-down 4-direction walk + idle. WASD / arrows steer directly; a quick
|
|
46
|
-
// pointer tap walks to the tap point; a held drag continuously re-aims at
|
|
47
|
-
// the live pointer. NPC dialogs are proximity-triggered by Dialog itself,
|
|
48
|
-
// not by this controller -- the player just walks, and getting close to an
|
|
49
|
-
// NPC opens its dialog automatically.
|
|
50
|
-
export class PlayerController implements Behavior<PlayerControllerProps> {
|
|
51
|
-
static behaviorName = 'PlayerController';
|
|
52
|
-
static defaultProps: PlayerControllerProps = {
|
|
53
|
-
speed: 180,
|
|
54
|
-
spritePrefix: 'drawings/player',
|
|
55
|
-
interactRange: 36,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
props: PlayerControllerProps;
|
|
59
|
-
|
|
60
|
-
constructor(props: PlayerControllerProps) {
|
|
61
|
-
this.props = props;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
update(actor: ActorData, scene: SceneRuntime, dt: number): void {
|
|
65
|
-
const layout = actor.components.Layout as LayoutProps | undefined;
|
|
66
|
-
if (!layout) return;
|
|
67
|
-
|
|
68
|
-
const runtime = getPlayerRuntime(actor);
|
|
69
|
-
|
|
70
|
-
// While any actor's dialog is open, freeze the player: no movement, no
|
|
71
|
-
// tap-to-move. Pointer-edge tracking is forced "down" so the click that
|
|
72
|
-
// closes the dialog cannot also start a new ground walk on the same
|
|
73
|
-
// frame -- the user must release first.
|
|
74
|
-
if (isAnyDialogOpen(scene)) {
|
|
75
|
-
runtime.moving = false;
|
|
76
|
-
runtime.pointerWasDown = true;
|
|
77
|
-
runtime.dragFollow = false;
|
|
78
|
-
runtime.pressTime = 0;
|
|
79
|
-
runtime.moveTargetX = null;
|
|
80
|
-
runtime.moveTargetY = null;
|
|
81
|
-
syncDrawing(actor, runtime, this.props.spritePrefix);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.consumePointer(scene, runtime, dt);
|
|
86
|
-
|
|
87
|
-
const dx = readAxis(scene, ['a', 'A', 'ArrowLeft'], ['d', 'D', 'ArrowRight']);
|
|
88
|
-
const dy = readAxis(scene, ['w', 'W', 'ArrowUp'], ['s', 'S', 'ArrowDown']);
|
|
89
|
-
|
|
90
|
-
if (dx !== 0 || dy !== 0) {
|
|
91
|
-
runtime.moveTargetX = null;
|
|
92
|
-
runtime.moveTargetY = null;
|
|
93
|
-
runtime.dragFollow = false;
|
|
94
|
-
this.walkByVector(actor, scene, runtime, dx, dy, dt);
|
|
95
|
-
} else if (runtime.moveTargetX !== null && runtime.moveTargetY !== null) {
|
|
96
|
-
this.walkToTarget(actor, scene, runtime, dt);
|
|
97
|
-
} else {
|
|
98
|
-
runtime.moving = false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
syncDrawing(actor, runtime, this.props.spritePrefix);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private walkByVector(
|
|
105
|
-
actor: ActorData,
|
|
106
|
-
scene: SceneRuntime,
|
|
107
|
-
runtime: PlayerRuntime,
|
|
108
|
-
dx: number,
|
|
109
|
-
dy: number,
|
|
110
|
-
dt: number
|
|
111
|
-
): void {
|
|
112
|
-
runtime.moving = true;
|
|
113
|
-
faceVector(runtime, dx, dy);
|
|
114
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
115
|
-
const speed = this.props.speed;
|
|
116
|
-
moveAndCollide(actor, scene, (dx / len) * speed * dt, 0);
|
|
117
|
-
moveAndCollide(actor, scene, 0, (dy / len) * speed * dt);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private walkToTarget(
|
|
121
|
-
actor: ActorData,
|
|
122
|
-
scene: SceneRuntime,
|
|
123
|
-
runtime: PlayerRuntime,
|
|
124
|
-
dt: number
|
|
125
|
-
): void {
|
|
126
|
-
const layout = actor.components.Layout as LayoutProps;
|
|
127
|
-
const tx = runtime.moveTargetX ?? 0;
|
|
128
|
-
const ty = runtime.moveTargetY ?? 0;
|
|
129
|
-
const cx = layout.x + layout.width / 2;
|
|
130
|
-
const cy = layout.y + layout.height / 2;
|
|
131
|
-
let dx = tx - cx;
|
|
132
|
-
let dy = ty - cy;
|
|
133
|
-
const dist = Math.hypot(dx, dy);
|
|
134
|
-
|
|
135
|
-
if (dist <= ARRIVE_EPS) {
|
|
136
|
-
runtime.moveTargetX = null;
|
|
137
|
-
runtime.moveTargetY = null;
|
|
138
|
-
runtime.moving = false;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
dx /= dist;
|
|
143
|
-
dy /= dist;
|
|
144
|
-
const speed = this.props.speed;
|
|
145
|
-
const step = Math.min(speed * dt, dist);
|
|
146
|
-
runtime.moving = true;
|
|
147
|
-
faceVector(runtime, dx, dy);
|
|
148
|
-
moveAndCollide(actor, scene, dx * step, 0);
|
|
149
|
-
moveAndCollide(actor, scene, 0, dy * step);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// A quick press-release becomes a single move-to-tap-point. A held press
|
|
153
|
-
// becomes a drag-follow: each frame re-aim the ground target at the live
|
|
154
|
-
// pointer. Releasing a held drag stops the player. There is no tap-on-NPC
|
|
155
|
-
// special case any more -- Dialog auto-opens on proximity, so the user
|
|
156
|
-
// just walks where they want.
|
|
157
|
-
private consumePointer(scene: SceneRuntime, runtime: PlayerRuntime, dt: number): void {
|
|
158
|
-
const isDown = scene.pointer.down;
|
|
159
|
-
const pressedThisFrame = isDown && !runtime.pointerWasDown;
|
|
160
|
-
const releasedThisFrame = !isDown && runtime.pointerWasDown;
|
|
161
|
-
runtime.pointerWasDown = isDown;
|
|
162
|
-
|
|
163
|
-
if (pressedThisFrame) {
|
|
164
|
-
runtime.moveTargetX = scene.pointer.x;
|
|
165
|
-
runtime.moveTargetY = scene.pointer.y;
|
|
166
|
-
runtime.dragFollow = true;
|
|
167
|
-
runtime.pressTime = 0;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (isDown) {
|
|
172
|
-
runtime.pressTime += dt;
|
|
173
|
-
if (runtime.dragFollow) {
|
|
174
|
-
runtime.moveTargetX = scene.pointer.x;
|
|
175
|
-
runtime.moveTargetY = scene.pointer.y;
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (releasedThisFrame) {
|
|
181
|
-
const wasQuickTap = runtime.pressTime <= QUICK_TAP_SEC;
|
|
182
|
-
if (!wasQuickTap && runtime.dragFollow) {
|
|
183
|
-
runtime.moveTargetX = null;
|
|
184
|
-
runtime.moveTargetY = null;
|
|
185
|
-
}
|
|
186
|
-
runtime.dragFollow = false;
|
|
187
|
-
runtime.pressTime = 0;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function getPlayerRuntime(actor: ActorData): PlayerRuntime {
|
|
193
|
-
if (!actor.runtime) actor.runtime = {};
|
|
194
|
-
const runtime = actor.runtime as PlayerRuntime;
|
|
195
|
-
if (!runtime.facing) runtime.facing = 'down';
|
|
196
|
-
if (typeof runtime.moving !== 'boolean') runtime.moving = false;
|
|
197
|
-
if (runtime.moveTargetX === undefined) runtime.moveTargetX = null;
|
|
198
|
-
if (runtime.moveTargetY === undefined) runtime.moveTargetY = null;
|
|
199
|
-
if (typeof runtime.pointerWasDown !== 'boolean') runtime.pointerWasDown = false;
|
|
200
|
-
if (typeof runtime.pressTime !== 'number') runtime.pressTime = 0;
|
|
201
|
-
if (typeof runtime.dragFollow !== 'boolean') runtime.dragFollow = false;
|
|
202
|
-
return runtime;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function readAxis(scene: SceneRuntime, negatives: string[], positives: string[]): number {
|
|
206
|
-
const neg = negatives.some((key) => scene.keys.has(key)) ? 1 : 0;
|
|
207
|
-
const pos = positives.some((key) => scene.keys.has(key)) ? 1 : 0;
|
|
208
|
-
return pos - neg;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function faceVector(runtime: PlayerRuntime, dx: number, dy: number): void {
|
|
212
|
-
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
213
|
-
if (dx > 0) runtime.facing = 'right';
|
|
214
|
-
else if (dx < 0) runtime.facing = 'left';
|
|
215
|
-
} else {
|
|
216
|
-
if (dy > 0) runtime.facing = 'down';
|
|
217
|
-
else if (dy < 0) runtime.facing = 'up';
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function syncDrawing(actor: ActorData, runtime: PlayerRuntime, prefix: string): void {
|
|
222
|
-
const drawing = actor.components.Drawing as DrawingProps | undefined;
|
|
223
|
-
if (!drawing) return;
|
|
224
|
-
const state = runtime.moving ? 'walk' : 'idle';
|
|
225
|
-
drawing.file = `${prefix}-${state}-${runtime.facing}.drawing`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function moveAndCollide(actor: ActorData, scene: SceneRuntime, dx: number, dy: number): void {
|
|
229
|
-
const layout = actor.components.Layout as LayoutProps | undefined;
|
|
230
|
-
if (!layout) return;
|
|
231
|
-
if (dx === 0 && dy === 0) return;
|
|
232
|
-
layout.x += dx;
|
|
233
|
-
layout.y += dy;
|
|
234
|
-
const myRect = getColliderRect(actor);
|
|
235
|
-
if (!myRect) return;
|
|
236
|
-
for (const other of scene.getActors()) {
|
|
237
|
-
if (other.id === actor.id) continue;
|
|
238
|
-
const otherCollider = other.components.Collider as ColliderProps | undefined;
|
|
239
|
-
if (!otherCollider || otherCollider.kind !== 'solid') continue;
|
|
240
|
-
const otherRect = getColliderRect(other);
|
|
241
|
-
if (!otherRect || !intersects(myRect, otherRect)) continue;
|
|
242
|
-
if (dx !== 0) layout.x -= dx;
|
|
243
|
-
if (dy !== 0) layout.y -= dy;
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function isAnyDialogOpen(scene: SceneRuntime): boolean {
|
|
249
|
-
for (const actor of scene.getActors()) {
|
|
250
|
-
if (!actor.components.Dialog) continue;
|
|
251
|
-
const runtime = actor.runtime as { open?: boolean } | undefined;
|
|
252
|
-
if (runtime?.open) return true;
|
|
253
|
-
}
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import { getColliderRect, intersects } from './Collider';
|
|
5
|
-
import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
|
|
6
|
-
|
|
7
|
-
export interface PortalProps {
|
|
8
|
-
// File path of the scene to swap to when the player overlaps this portal.
|
|
9
|
-
// e.g. 'scenes/town.scene'. The engine reloads the scene on the next frame.
|
|
10
|
-
targetScene: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Scene transition trigger. When any actor with a PlayerController overlaps
|
|
14
|
-
// the portal's collider, set `scene.requestedScene = targetScene` so the
|
|
15
|
-
// ScenePlayer mounts the next scene.
|
|
16
|
-
export class Portal implements Behavior<PortalProps> {
|
|
17
|
-
static behaviorName = 'Portal';
|
|
18
|
-
static defaultProps: PortalProps = {
|
|
19
|
-
targetScene: '',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
props: PortalProps;
|
|
23
|
-
|
|
24
|
-
constructor(props: PortalProps) {
|
|
25
|
-
this.props = props;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
update(actor: ActorData, scene: SceneRuntime): void {
|
|
29
|
-
if (!this.props.targetScene) return;
|
|
30
|
-
const myRect = getColliderRect(actor);
|
|
31
|
-
if (!myRect) return;
|
|
32
|
-
for (const candidate of scene.getActors()) {
|
|
33
|
-
if (!candidate.components.PlayerController) continue;
|
|
34
|
-
const playerRect = getColliderRect(candidate);
|
|
35
|
-
if (!playerRect) continue;
|
|
36
|
-
if (intersects(myRect, playerRect)) {
|
|
37
|
-
scene.requestedScene = this.props.targetScene;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
static Inspector({
|
|
44
|
-
component,
|
|
45
|
-
setComponent,
|
|
46
|
-
}: {
|
|
47
|
-
component: PortalProps;
|
|
48
|
-
setComponent: (nextProps: Partial<PortalProps>) => void;
|
|
49
|
-
}) {
|
|
50
|
-
return (
|
|
51
|
-
<Panel title="Portal">
|
|
52
|
-
<AutoFields
|
|
53
|
-
defaultProps={Portal.defaultProps}
|
|
54
|
-
component={component}
|
|
55
|
-
setComponent={setComponent}
|
|
56
|
-
/>
|
|
57
|
-
</Panel>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import type { Behavior } from '../engine/scene';
|
|
5
|
-
|
|
6
|
-
export interface QuestStep {
|
|
7
|
-
id: string;
|
|
8
|
-
title: string;
|
|
9
|
-
// One-line hint shown beneath the title.
|
|
10
|
-
hint: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface QuestLogProps {
|
|
14
|
-
// Visible chapter title -- e.g. 'Chapter 1: The Awakening'.
|
|
15
|
-
chapter: string;
|
|
16
|
-
// The full quest line for this chapter, in order. The first uncompleted
|
|
17
|
-
// step is the active quest. Add an id to `completedIds` to mark done.
|
|
18
|
-
steps: QuestStep[];
|
|
19
|
-
// Step ids already completed (seeded for a fresh save).
|
|
20
|
-
completedIds: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Place on a 'manager' actor (one per scene). Renders a small HUD panel with
|
|
24
|
-
// the chapter title and the active quest hint.
|
|
25
|
-
export class QuestLog implements Behavior<QuestLogProps> {
|
|
26
|
-
static behaviorName = 'QuestLog';
|
|
27
|
-
static defaultProps: QuestLogProps = {
|
|
28
|
-
chapter: 'Chapter 1',
|
|
29
|
-
steps: [],
|
|
30
|
-
completedIds: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
props: QuestLogProps;
|
|
34
|
-
|
|
35
|
-
constructor(props: QuestLogProps) {
|
|
36
|
-
this.props = props;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
ui(): React.ReactNode {
|
|
40
|
-
const done = new Set(this.props.completedIds);
|
|
41
|
-
const active = this.props.steps.find((step) => !done.has(step.id));
|
|
42
|
-
return (
|
|
43
|
-
<div
|
|
44
|
-
style={{
|
|
45
|
-
position: 'absolute',
|
|
46
|
-
top: 12,
|
|
47
|
-
right: 12,
|
|
48
|
-
maxWidth: 220,
|
|
49
|
-
padding: '8px 12px',
|
|
50
|
-
background: 'rgba(12, 16, 24, 0.78)',
|
|
51
|
-
color: '#f4f1de',
|
|
52
|
-
border: '1px solid rgba(255, 255, 255, 0.35)',
|
|
53
|
-
borderRadius: 6,
|
|
54
|
-
font: '11px/1.3 system-ui, sans-serif',
|
|
55
|
-
pointerEvents: 'none',
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
<div style={{ fontWeight: 700, color: '#ffd166', marginBottom: 4 }}>
|
|
59
|
-
{this.props.chapter}
|
|
60
|
-
</div>
|
|
61
|
-
{active ? (
|
|
62
|
-
<>
|
|
63
|
-
<div style={{ fontWeight: 600 }}>{active.title}</div>
|
|
64
|
-
<div style={{ opacity: 0.85 }}>{active.hint}</div>
|
|
65
|
-
</>
|
|
66
|
-
) : (
|
|
67
|
-
<div style={{ opacity: 0.85 }}>All quests complete.</div>
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
static Inspector({
|
|
74
|
-
component,
|
|
75
|
-
setComponent,
|
|
76
|
-
}: {
|
|
77
|
-
component: QuestLogProps;
|
|
78
|
-
setComponent: (nextProps: Partial<QuestLogProps>) => void;
|
|
79
|
-
}) {
|
|
80
|
-
return (
|
|
81
|
-
<Panel title="QuestLog">
|
|
82
|
-
<AutoFields
|
|
83
|
-
defaultProps={QuestLog.defaultProps}
|
|
84
|
-
component={component}
|
|
85
|
-
setComponent={setComponent}
|
|
86
|
-
/>
|
|
87
|
-
</Panel>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,123 +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 SaveMenuProps {
|
|
7
|
-
// localStorage key for this deck's save slot. Decks pick a unique value
|
|
8
|
-
// (e.g. 'rpg-2d-starter/save-1') so multiple decks don't clobber each other.
|
|
9
|
-
storageKey: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface SaveMenuRuntime extends Record<string, unknown> {
|
|
13
|
-
savePressed: boolean;
|
|
14
|
-
loadPressed: boolean;
|
|
15
|
-
toast: string;
|
|
16
|
-
toastUntil: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Save / load slot. Press F5 to save the current scene snapshot to
|
|
20
|
-
// localStorage, F9 to load it. A small toast confirms the action.
|
|
21
|
-
export class SaveMenu implements Behavior<SaveMenuProps> {
|
|
22
|
-
static behaviorName = 'SaveMenu';
|
|
23
|
-
static defaultProps: SaveMenuProps = {
|
|
24
|
-
storageKey: 'rpg-2d/save',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
props: SaveMenuProps;
|
|
28
|
-
|
|
29
|
-
constructor(props: SaveMenuProps) {
|
|
30
|
-
this.props = props;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
update(actor: ActorData, scene: SceneRuntime): void {
|
|
34
|
-
const runtime = getSaveMenuRuntime(actor);
|
|
35
|
-
const savePressed = scene.keys.has('F5');
|
|
36
|
-
const loadPressed = scene.keys.has('F9');
|
|
37
|
-
if (savePressed && !runtime.savePressed) this.save(scene, runtime);
|
|
38
|
-
if (loadPressed && !runtime.loadPressed) this.load(scene, runtime);
|
|
39
|
-
runtime.savePressed = savePressed;
|
|
40
|
-
runtime.loadPressed = loadPressed;
|
|
41
|
-
if (runtime.toast && scene.time > runtime.toastUntil) runtime.toast = '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private save(scene: SceneRuntime, runtime: SaveMenuRuntime): void {
|
|
45
|
-
try {
|
|
46
|
-
const snapshot = JSON.stringify(scene.serialize());
|
|
47
|
-
window.localStorage.setItem(this.props.storageKey, snapshot);
|
|
48
|
-
flashToast(runtime, scene, 'saved');
|
|
49
|
-
} catch {
|
|
50
|
-
flashToast(runtime, scene, 'save failed');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private load(scene: SceneRuntime, runtime: SaveMenuRuntime): void {
|
|
55
|
-
try {
|
|
56
|
-
const snapshot = window.localStorage.getItem(this.props.storageKey);
|
|
57
|
-
if (!snapshot) {
|
|
58
|
-
flashToast(runtime, scene, 'no save found');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
scene.load(JSON.parse(snapshot));
|
|
62
|
-
flashToast(runtime, scene, 'loaded');
|
|
63
|
-
} catch {
|
|
64
|
-
flashToast(runtime, scene, 'load failed');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
ui(actor: ActorData): React.ReactNode {
|
|
69
|
-
const runtime = actor.runtime as SaveMenuRuntime | undefined;
|
|
70
|
-
if (!runtime?.toast) return null;
|
|
71
|
-
return (
|
|
72
|
-
<div
|
|
73
|
-
style={{
|
|
74
|
-
position: 'absolute',
|
|
75
|
-
top: 12,
|
|
76
|
-
left: 12,
|
|
77
|
-
padding: '6px 10px',
|
|
78
|
-
background: 'rgba(12, 16, 24, 0.85)',
|
|
79
|
-
color: '#f4f1de',
|
|
80
|
-
border: '1px solid rgba(255, 255, 255, 0.35)',
|
|
81
|
-
borderRadius: 4,
|
|
82
|
-
font: '11px/1.3 system-ui, sans-serif',
|
|
83
|
-
pointerEvents: 'none',
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
{runtime.toast} (F5 save / F9 load)
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
static Inspector({
|
|
92
|
-
component,
|
|
93
|
-
setComponent,
|
|
94
|
-
}: {
|
|
95
|
-
component: SaveMenuProps;
|
|
96
|
-
setComponent: (nextProps: Partial<SaveMenuProps>) => void;
|
|
97
|
-
}) {
|
|
98
|
-
return (
|
|
99
|
-
<Panel title="SaveMenu">
|
|
100
|
-
<AutoFields
|
|
101
|
-
defaultProps={SaveMenu.defaultProps}
|
|
102
|
-
component={component}
|
|
103
|
-
setComponent={setComponent}
|
|
104
|
-
/>
|
|
105
|
-
</Panel>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getSaveMenuRuntime(actor: ActorData): SaveMenuRuntime {
|
|
111
|
-
if (!actor.runtime) actor.runtime = {};
|
|
112
|
-
const runtime = actor.runtime as SaveMenuRuntime;
|
|
113
|
-
if (typeof runtime.savePressed !== 'boolean') runtime.savePressed = false;
|
|
114
|
-
if (typeof runtime.loadPressed !== 'boolean') runtime.loadPressed = false;
|
|
115
|
-
if (typeof runtime.toast !== 'string') runtime.toast = '';
|
|
116
|
-
if (typeof runtime.toastUntil !== 'number') runtime.toastUntil = 0;
|
|
117
|
-
return runtime;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function flashToast(runtime: SaveMenuRuntime, scene: SceneRuntime, message: string): void {
|
|
121
|
-
runtime.toast = message;
|
|
122
|
-
runtime.toastUntil = scene.time + 1.6;
|
|
123
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Panel } from '../engine/ui';
|
|
3
|
-
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import { drawAnimatedDrawing } from './Drawing';
|
|
5
|
-
import { frameIndexAt } from '../engine/drawing';
|
|
6
|
-
import type { ActorData, Behavior, SceneRuntime } from '../engine/scene';
|
|
7
|
-
import type { LayoutProps } from './Layout.tsx';
|
|
8
|
-
|
|
9
|
-
export interface TilemapLayer {
|
|
10
|
-
// Single-character grid: each row is a string of chars whose mapping to a
|
|
11
|
-
// drawing file lives in `tiles`. '.' means empty.
|
|
12
|
-
rows: string[];
|
|
13
|
-
// Tile rendering offset from Layout's (x,y) origin in card pixels.
|
|
14
|
-
offsetZ?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TilemapProps {
|
|
18
|
-
// Px-size of one tile cell -- tile drawings are scaled to this.
|
|
19
|
-
tileSize: number;
|
|
20
|
-
// Char -> drawing file. Use '.' for empty / sky tiles.
|
|
21
|
-
tiles: Record<string, string>;
|
|
22
|
-
// Layers render back-to-front in array order, all anchored at the actor's
|
|
23
|
-
// Layout origin. Each layer is its own char grid so a Tilemap can mix
|
|
24
|
-
// floor + decoration + roof.
|
|
25
|
-
layers: TilemapLayer[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Multi-layer tile grid. The actor's Layout supplies the origin; each cell
|
|
29
|
-
// renders the drawing keyed by its char. Empty cells ('.') skip drawing.
|
|
30
|
-
export class Tilemap implements Behavior<TilemapProps> {
|
|
31
|
-
static behaviorName = 'Tilemap';
|
|
32
|
-
static defaultProps: TilemapProps = {
|
|
33
|
-
tileSize: 32,
|
|
34
|
-
tiles: {},
|
|
35
|
-
layers: [],
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
props: TilemapProps;
|
|
39
|
-
|
|
40
|
-
constructor(props: TilemapProps) {
|
|
41
|
-
this.props = props;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
draw(actor: ActorData, scene: SceneRuntime, ctx: CanvasRenderingContext2D): void {
|
|
45
|
-
const layout = actor.components.Layout as LayoutProps | undefined;
|
|
46
|
-
if (!layout) return;
|
|
47
|
-
const size = this.props.tileSize;
|
|
48
|
-
for (const layer of this.props.layers) {
|
|
49
|
-
for (let row = 0; row < layer.rows.length; row++) {
|
|
50
|
-
const line = layer.rows[row];
|
|
51
|
-
for (let col = 0; col < line.length; col++) {
|
|
52
|
-
const char = line.charAt(col);
|
|
53
|
-
if (char === '.' || char === ' ') continue;
|
|
54
|
-
const file = this.props.tiles[char];
|
|
55
|
-
if (!file) continue;
|
|
56
|
-
const drawing = scene.drawings[file];
|
|
57
|
-
if (!drawing) continue;
|
|
58
|
-
const frameIndex = frameIndexAt(drawing, scene.time);
|
|
59
|
-
drawAnimatedDrawing(
|
|
60
|
-
ctx,
|
|
61
|
-
drawing,
|
|
62
|
-
frameIndex,
|
|
63
|
-
layout.x + col * size,
|
|
64
|
-
layout.y + row * size,
|
|
65
|
-
size,
|
|
66
|
-
size
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
static Inspector({
|
|
74
|
-
component,
|
|
75
|
-
setComponent,
|
|
76
|
-
}: {
|
|
77
|
-
component: TilemapProps;
|
|
78
|
-
setComponent: (nextProps: Partial<TilemapProps>) => void;
|
|
79
|
-
}) {
|
|
80
|
-
return (
|
|
81
|
-
<Panel title="Tilemap">
|
|
82
|
-
<AutoFields
|
|
83
|
-
defaultProps={Tilemap.defaultProps}
|
|
84
|
-
component={component}
|
|
85
|
-
setComponent={setComponent}
|
|
86
|
-
/>
|
|
87
|
-
</Panel>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|