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,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
- }