castle-web-cli 0.4.3 → 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 +1 -1
  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,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
- }