castle-web-cli 0.4.78 → 0.4.79
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/agent-prompts.d.ts +4 -1
- package/dist/agent-prompts.js +28 -7
- package/dist/agent.d.ts +7 -2
- package/dist/agent.js +655 -51
- package/dist/native/loop.d.ts +2 -0
- package/dist/native/loop.js +698 -0
- package/dist/native/openrouter.d.ts +55 -0
- package/dist/native/openrouter.js +354 -0
- package/dist/native/playtest-browser.d.ts +34 -0
- package/dist/native/playtest-browser.js +354 -0
- package/dist/native/playtest-executor.d.ts +3 -0
- package/dist/native/playtest-executor.js +156 -0
- package/dist/native/playtest.d.ts +131 -0
- package/dist/native/playtest.js +314 -0
- package/dist/native/tools.d.ts +38 -0
- package/dist/native/tools.js +630 -0
- package/dist/native/types.d.ts +40 -0
- package/dist/native/types.js +41 -0
- package/dist/serve.js +12 -0
- package/dist/shell/assets/{index-yGdKhgfZ.js → index-CNT3KxJb.js} +37 -37
- package/dist/shell/assets/{index-WE24qX3d.css → index-RZrw5gQ2.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/basic-2d/CLAUDE.md +29 -3
- package/kits/basic-2d/behaviors/Layout.jsx +10 -0
- package/kits/basic-2d/behaviors/Sprite.jsx +1 -1
- package/kits/basic-2d/blueprints/cauldron.scene +22 -0
- package/kits/basic-2d/castle.json +5 -7
- package/kits/basic-2d/docs/pxart-format.md +4 -3
- package/kits/basic-2d/drawings/cauldron.pxart +113 -0
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +247 -0
- package/kits/basic-2d/editors/PlayOnly.jsx +1 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +399 -411
- package/kits/basic-2d/editors/SelectionOverlay.jsx +125 -63
- package/kits/basic-2d/editors/SingleEditor.jsx +11 -2
- package/kits/basic-2d/editors/editorHistory.js +8 -2
- package/kits/basic-2d/editors/inspectorSheet.js +5 -19
- package/kits/basic-2d/engine/ScenePlayer.jsx +2 -2
- package/kits/basic-2d/engine/blueprint.js +423 -0
- package/kits/basic-2d/engine/files.js +1 -1
- package/kits/basic-2d/engine/scene.js +29 -29
- package/kits/basic-2d/engine/ui.jsx +160 -21
- package/kits/basic-2d/engine/ui.module.css +155 -13
- package/kits/basic-2d/pnpm-workspace.yaml +3 -0
- package/kits/basic-2d/scenes/main.scene +3 -13
- package/package.json +2 -1
- package/kits/basic-2d/drawings/pig.pxart +0 -26
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { renderSpriteFrame } from '../engine/pxart';
|
|
3
|
+
import { screenToCard } from '../engine/scene';
|
|
4
|
+
import { countBlueprintInstances, listBlueprints } from '../engine/blueprint';
|
|
5
|
+
import { ConfirmDialog, ContextMenu, cx, Icon, styles } from '../engine/ui';
|
|
6
|
+
|
|
7
|
+
const DRAG_THRESHOLD = 4;
|
|
8
|
+
|
|
9
|
+
// Deck-level blueprint library / hotbar: lists `blueprints/*.scene` (replacing
|
|
10
|
+
// the old scene-local stamp system). Mirrors castle-client's belt: TAP a slot
|
|
11
|
+
// to select the blueprint for editing in the sidebar; DRAG a slot onto the
|
|
12
|
+
// canvas to place a new instance; right-click for a delete option.
|
|
13
|
+
export function BlueprintLibrary({
|
|
14
|
+
files,
|
|
15
|
+
sprites,
|
|
16
|
+
canvasRef,
|
|
17
|
+
editCameraRef,
|
|
18
|
+
isPlaying,
|
|
19
|
+
selectedBlueprintPath,
|
|
20
|
+
onSelectBlueprint,
|
|
21
|
+
onAddActor,
|
|
22
|
+
dragPlace,
|
|
23
|
+
onDeleteBlueprint,
|
|
24
|
+
}) {
|
|
25
|
+
const blueprints = listBlueprints(files);
|
|
26
|
+
const dragRef = useRef(null);
|
|
27
|
+
const suppressClickRef = useRef(false);
|
|
28
|
+
const [dragPreview, setDragPreview] = useState(null);
|
|
29
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
30
|
+
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
31
|
+
|
|
32
|
+
const onSlotContextMenu = (event, blueprint) => {
|
|
33
|
+
if (isPlaying) return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
setContextMenu({ blueprint, x: event.clientX, y: event.clientY });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const onSlotPointerDown = (event, blueprint) => {
|
|
40
|
+
if (isPlaying || event.button !== 0) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
const drag = {
|
|
43
|
+
blueprint,
|
|
44
|
+
pointerId: event.pointerId,
|
|
45
|
+
startX: event.clientX,
|
|
46
|
+
startY: event.clientY,
|
|
47
|
+
moved: false,
|
|
48
|
+
onStage: false,
|
|
49
|
+
};
|
|
50
|
+
dragRef.current = drag;
|
|
51
|
+
try {
|
|
52
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
53
|
+
} catch {
|
|
54
|
+
// Window listeners still finish the drag if capture is unavailable.
|
|
55
|
+
}
|
|
56
|
+
const onMove = (moveEvent) => {
|
|
57
|
+
if (moveEvent.pointerId !== drag.pointerId) return;
|
|
58
|
+
if (!drag.moved && Math.hypot(moveEvent.clientX - drag.startX, moveEvent.clientY - drag.startY) > DRAG_THRESHOLD) {
|
|
59
|
+
drag.moved = true;
|
|
60
|
+
}
|
|
61
|
+
if (!drag.moved) return;
|
|
62
|
+
const position = eventToStagePoint(moveEvent, canvasRef, editCameraRef);
|
|
63
|
+
if (position) {
|
|
64
|
+
// Over the stage: the drag becomes a REAL placed instance immediately,
|
|
65
|
+
// so it grid-snaps and renders exactly like the actor it is. The
|
|
66
|
+
// floating thumbnail only exists off-stage.
|
|
67
|
+
if (drag.onStage) {
|
|
68
|
+
dragPlace.move(position);
|
|
69
|
+
} else {
|
|
70
|
+
drag.onStage = true;
|
|
71
|
+
setDragPreview(null);
|
|
72
|
+
dragPlace.enter(blueprint.path, position);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (drag.onStage) {
|
|
76
|
+
drag.onStage = false;
|
|
77
|
+
dragPlace.leave();
|
|
78
|
+
}
|
|
79
|
+
setDragPreview({ blueprint, x: moveEvent.clientX, y: moveEvent.clientY, overStage: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const onUp = (upEvent) => {
|
|
83
|
+
if (upEvent.pointerId !== drag.pointerId) return;
|
|
84
|
+
window.removeEventListener('pointermove', onMove);
|
|
85
|
+
window.removeEventListener('pointerup', onUp);
|
|
86
|
+
window.removeEventListener('pointercancel', onUp);
|
|
87
|
+
dragRef.current = null;
|
|
88
|
+
setDragPreview(null);
|
|
89
|
+
if (!drag.moved) return;
|
|
90
|
+
suppressClickRef.current = true;
|
|
91
|
+
// Cancelled drags (pointercancel) count as a release off-stage.
|
|
92
|
+
if (drag.onStage && upEvent.type !== 'pointercancel') dragPlace.drop();
|
|
93
|
+
else if (drag.onStage) dragPlace.leave();
|
|
94
|
+
};
|
|
95
|
+
window.addEventListener('pointermove', onMove);
|
|
96
|
+
window.addEventListener('pointerup', onUp);
|
|
97
|
+
window.addEventListener('pointercancel', onUp);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={styles.bpHotbar} aria-label="Blueprints">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={styles.bpActionSlot}
|
|
105
|
+
aria-label="Add actor"
|
|
106
|
+
title="Add actor (creates a new blueprint)"
|
|
107
|
+
onClick={onAddActor}
|
|
108
|
+
disabled={isPlaying}>
|
|
109
|
+
<span className={styles.dashedSquareIcon} aria-hidden="true" />
|
|
110
|
+
</button>
|
|
111
|
+
{blueprints.map((blueprint) => (
|
|
112
|
+
<button
|
|
113
|
+
key={blueprint.path}
|
|
114
|
+
type="button"
|
|
115
|
+
className={cx(styles.bpSlot, selectedBlueprintPath === blueprint.path && styles.bpSlotSelected)}
|
|
116
|
+
title={`${blueprint.name} -- click to edit, drag to place`}
|
|
117
|
+
disabled={isPlaying}
|
|
118
|
+
onPointerDown={(event) => onSlotPointerDown(event, blueprint)}
|
|
119
|
+
onContextMenu={(event) => onSlotContextMenu(event, blueprint)}
|
|
120
|
+
onClick={() => {
|
|
121
|
+
if (suppressClickRef.current) {
|
|
122
|
+
suppressClickRef.current = false;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
onSelectBlueprint(blueprint.path);
|
|
126
|
+
}}>
|
|
127
|
+
<BlueprintThumbnail blueprint={blueprint} sprites={sprites} />
|
|
128
|
+
<InstanceCountBadge files={files} blueprintPath={blueprint.path} />
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
131
|
+
{dragPreview ? <BlueprintDragPreview preview={dragPreview} sprites={sprites} /> : null}
|
|
132
|
+
{contextMenu ? (
|
|
133
|
+
<ContextMenu
|
|
134
|
+
x={contextMenu.x}
|
|
135
|
+
y={contextMenu.y}
|
|
136
|
+
onClose={() => setContextMenu(null)}
|
|
137
|
+
items={[
|
|
138
|
+
{
|
|
139
|
+
key: 'delete',
|
|
140
|
+
label: `Delete ${contextMenu.blueprint.name}...`,
|
|
141
|
+
onClick: () => setConfirmDelete(contextMenu.blueprint),
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
144
|
+
/>
|
|
145
|
+
) : null}
|
|
146
|
+
{confirmDelete ? (
|
|
147
|
+
<DeleteBlueprintConfirm
|
|
148
|
+
files={files}
|
|
149
|
+
blueprint={confirmDelete}
|
|
150
|
+
onCancel={() => setConfirmDelete(null)}
|
|
151
|
+
onConfirm={() => {
|
|
152
|
+
onDeleteBlueprint(confirmDelete.path);
|
|
153
|
+
setConfirmDelete(null);
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
) : null}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function DeleteBlueprintConfirm({ files, blueprint, onCancel, onConfirm }) {
|
|
162
|
+
const count = countBlueprintInstances(files, blueprint.path);
|
|
163
|
+
const instanceText = count === 1 ? '1 placed actor' : `${count} placed actors`;
|
|
164
|
+
return (
|
|
165
|
+
<ConfirmDialog
|
|
166
|
+
title={`Delete ${blueprint.name}?`}
|
|
167
|
+
message={
|
|
168
|
+
count > 0
|
|
169
|
+
? `This removes ${blueprint.name} and ${instanceText} across every scene in the deck. This can't be undone.`
|
|
170
|
+
: `${blueprint.name} has no placed actors. This can't be undone.`
|
|
171
|
+
}
|
|
172
|
+
confirmLabel="Delete"
|
|
173
|
+
onCancel={onCancel}
|
|
174
|
+
onConfirm={onConfirm}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function InstanceCountBadge({ files, blueprintPath }) {
|
|
180
|
+
const count = countBlueprintInstances(files, blueprintPath);
|
|
181
|
+
if (count === 0) return null;
|
|
182
|
+
return <span className={styles.bpSlotBadge}>{count > 99 ? '99+' : count}</span>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function BlueprintDragPreview({ preview, sprites }) {
|
|
186
|
+
const layout = preview.blueprint.components?.Layout;
|
|
187
|
+
const width = Math.max(24, Math.min(80, layout?.width ?? 48));
|
|
188
|
+
const height = Math.max(24, Math.min(80, layout?.height ?? 48));
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
className={cx(styles.bpDragPreview, preview.overStage && styles.bpDragPreviewOverStage)}
|
|
192
|
+
style={{
|
|
193
|
+
left: preview.x,
|
|
194
|
+
top: preview.y,
|
|
195
|
+
width,
|
|
196
|
+
height,
|
|
197
|
+
transform: `translate(-50%, -50%) rotate(${layout?.rotation ?? 0}deg)`,
|
|
198
|
+
}}>
|
|
199
|
+
<BlueprintThumbnail blueprint={preview.blueprint} sprites={sprites} preview />
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function BlueprintThumbnail({ blueprint, sprites, preview = false }) {
|
|
205
|
+
const canvasRef = useRef(null);
|
|
206
|
+
const spritePath = blueprint.components?.Sprite?.file;
|
|
207
|
+
const sprite = spritePath ? sprites?.[spritePath] : null;
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const canvas = canvasRef.current;
|
|
210
|
+
if (!canvas || !sprite) return;
|
|
211
|
+
renderSpriteFrame(sprite, 0, canvas);
|
|
212
|
+
}, [sprite]);
|
|
213
|
+
if (!sprite) {
|
|
214
|
+
return (
|
|
215
|
+
<span className={styles.bpSlotFallback}>
|
|
216
|
+
<Icon name="clone" />
|
|
217
|
+
</span>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return (
|
|
221
|
+
<canvas
|
|
222
|
+
ref={canvasRef}
|
|
223
|
+
className={cx(styles.bpSlotThumb, preview && styles.bpPreviewThumb)}
|
|
224
|
+
aria-hidden="true"
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function eventToStagePoint(event, canvasRef, editCameraRef) {
|
|
230
|
+
const canvas = canvasRef.current;
|
|
231
|
+
if (!canvas) return null;
|
|
232
|
+
const rect = canvas.getBoundingClientRect();
|
|
233
|
+
if (
|
|
234
|
+
event.clientX < rect.left ||
|
|
235
|
+
event.clientX > rect.right ||
|
|
236
|
+
event.clientY < rect.top ||
|
|
237
|
+
event.clientY > rect.bottom
|
|
238
|
+
) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const raw = screenToCard(canvas, event.clientX, event.clientY);
|
|
242
|
+
const camera = editCameraRef.current ?? { x: 0, y: 0 };
|
|
243
|
+
return {
|
|
244
|
+
x: raw.x + camera.x,
|
|
245
|
+
y: raw.y + camera.y,
|
|
246
|
+
};
|
|
247
|
+
}
|