castle-web-cli 0.4.129 → 0.4.130
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.js +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +9 -10
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- package/kits/physics-3d/CLAUDE.md +109 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Portal.jsx +19 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/portal.scene +21 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +139 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +414 -0
- package/kits/physics-3d/scenes/model-lab.scene +127 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -132,14 +132,14 @@ export function PixelToolStrip({ tools, tool, onSelectTool, ariaLabel, disabled
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Compact W × H selects above the artboard (not a sidebar panel).
|
|
135
|
-
export function CanvasSizeBar({ width, height, onResize }) {
|
|
135
|
+
export function CanvasSizeBar({ width, height, onResize, disabled = false }) {
|
|
136
136
|
return (
|
|
137
137
|
<div className={styles.canvasSizeBar} aria-label="Canvas size">
|
|
138
|
-
<ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} />
|
|
138
|
+
<ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} disabled={disabled} />
|
|
139
139
|
<span className={styles.canvasSizeSep} aria-hidden="true">
|
|
140
140
|
×
|
|
141
141
|
</span>
|
|
142
|
-
<ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} />
|
|
142
|
+
<ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} disabled={disabled} />
|
|
143
143
|
</div>
|
|
144
144
|
);
|
|
145
145
|
}
|
|
@@ -175,13 +175,13 @@ const FINISH_STEPS = [
|
|
|
175
175
|
// (properties of the .pxart asset — see docs/pxart-format.md — not per-actor
|
|
176
176
|
// Sprite props). Highlights whichever preset the pair exactly matches, or none
|
|
177
177
|
// when it matches no preset.
|
|
178
|
-
export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
178
|
+
export function FinishBar({ renderer, cornerRadius, onChange, disabled = false }) {
|
|
179
179
|
const isOn = (step) =>
|
|
180
180
|
step.renderer === (renderer ?? GRID_RENDERER) && step.cornerRadius === (cornerRadius ?? 0);
|
|
181
181
|
return (
|
|
182
182
|
<div className={styles.finishBar} aria-label="Finish">
|
|
183
183
|
<span>Finish</span>
|
|
184
|
-
<div className={styles.finishSegments} role="radiogroup" aria-label="Finish">
|
|
184
|
+
<div className={styles.finishSegments} role="radiogroup" aria-label="Finish" aria-disabled={disabled || undefined}>
|
|
185
185
|
{FINISH_STEPS.map((step) => (
|
|
186
186
|
<button
|
|
187
187
|
key={step.id}
|
|
@@ -189,6 +189,7 @@ export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
|
189
189
|
role="radio"
|
|
190
190
|
aria-label={step.label}
|
|
191
191
|
aria-checked={isOn(step) ? 'true' : 'false'}
|
|
192
|
+
disabled={disabled}
|
|
192
193
|
className={cx(styles.finishSegment, isOn(step) && styles.finishSegmentOn)}
|
|
193
194
|
title={step.title}
|
|
194
195
|
onClick={() =>
|
|
@@ -202,7 +203,7 @@ export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
|
202
203
|
);
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
function ResolutionSelect({ label, value, onChange }) {
|
|
206
|
+
function ResolutionSelect({ label, value, onChange, disabled = false }) {
|
|
206
207
|
const options = RESOLUTION_STEPS.includes(value)
|
|
207
208
|
? RESOLUTION_STEPS
|
|
208
209
|
: [...RESOLUTION_STEPS, value].sort((a, b) => a - b);
|
|
@@ -211,6 +212,7 @@ function ResolutionSelect({ label, value, onChange }) {
|
|
|
211
212
|
className={styles.canvasSizeSelect}
|
|
212
213
|
aria-label={label}
|
|
213
214
|
value={value}
|
|
215
|
+
disabled={disabled}
|
|
214
216
|
onChange={(event) => onChange(Number(event.target.value))}>
|
|
215
217
|
{options.map((step) => (
|
|
216
218
|
<option key={step} value={step}>
|
|
@@ -940,10 +942,14 @@ export function PxArtInspectorDock({
|
|
|
940
942
|
moveActions,
|
|
941
943
|
selectActions,
|
|
942
944
|
pathActions,
|
|
945
|
+
readOnly = false,
|
|
943
946
|
}) {
|
|
944
947
|
const { tool, picking, setPicking } = toolState;
|
|
945
948
|
return (
|
|
946
|
-
<aside
|
|
949
|
+
<aside
|
|
950
|
+
className={cx(styles.pxArtInspector, styles.drawingToolsPanel, readOnly && styles.drawingReadOnly)}
|
|
951
|
+
aria-disabled={readOnly || undefined}
|
|
952
|
+
inert={readOnly || undefined}>
|
|
947
953
|
<div className={cx(styles.sheetBody, styles.inspectorBody, styles.drawingSettingsColumn)}>
|
|
948
954
|
{PALETTE_TOOLS.has(tool) || picking ? (
|
|
949
955
|
<Panel
|
|
@@ -146,7 +146,7 @@ function useResizableTimelineHeight() {
|
|
|
146
146
|
// the top, Aseprite-style) by frame COLUMNS, with thumbnails per cell, layer +
|
|
147
147
|
// frame controls, linked-cell affordances, onion-skin toggles, playback, and a
|
|
148
148
|
// tags editor. All mutation flows through `actions` (wired in PxArtEditor).
|
|
149
|
-
export function PxArtTimeline({ sprite, text, selection, playing, onion, actions }) {
|
|
149
|
+
export function PxArtTimeline({ sprite, text, selection, playing, onion, actions, readOnly = false }) {
|
|
150
150
|
// Hooks run unconditionally (order-stable); only the render branches on viewport.
|
|
151
151
|
const compact = useCompactViewport();
|
|
152
152
|
const resizable = useResizableTimelineHeight();
|
|
@@ -159,7 +159,13 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
159
159
|
});
|
|
160
160
|
const grid = (
|
|
161
161
|
<div className={tl.scroll}>
|
|
162
|
-
<TimelineGrid
|
|
162
|
+
<TimelineGrid
|
|
163
|
+
sprite={sprite}
|
|
164
|
+
text={text}
|
|
165
|
+
selection={selection}
|
|
166
|
+
actions={actions}
|
|
167
|
+
readOnly={readOnly}
|
|
168
|
+
/>
|
|
163
169
|
</div>
|
|
164
170
|
);
|
|
165
171
|
if (compact) {
|
|
@@ -178,6 +184,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
178
184
|
playing={playing}
|
|
179
185
|
onion={onion}
|
|
180
186
|
actions={actions}
|
|
187
|
+
readOnly={readOnly}
|
|
181
188
|
/>
|
|
182
189
|
{grid}
|
|
183
190
|
</section>
|
|
@@ -198,6 +205,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
198
205
|
playing={playing}
|
|
199
206
|
onion={onion}
|
|
200
207
|
actions={actions}
|
|
208
|
+
readOnly={readOnly}
|
|
201
209
|
/>
|
|
202
210
|
{grid}
|
|
203
211
|
{SHOW_TAGS ? <TagsPanel sprite={sprite} actions={actions} /> : null}
|
|
@@ -211,7 +219,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
211
219
|
// (duplicate/move/delete/rename/link/unlink/visibility) lives in the right-click
|
|
212
220
|
// context menus on the grid's layer rows, frame headers, and cells (see
|
|
213
221
|
// TimelineGrid + ContextMenu).
|
|
214
|
-
function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
222
|
+
function TimelineBar({ sprite, selection, playing, onion, actions, readOnly = false }) {
|
|
215
223
|
const { layerIndex, frameIndex } = selection;
|
|
216
224
|
// A single-frame sprite has no animation, so the three animation controls
|
|
217
225
|
// (global duration, onion skin, play/pause) are meaningless — gate them on
|
|
@@ -227,19 +235,20 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
227
235
|
return (
|
|
228
236
|
<div className={tl.bar}>
|
|
229
237
|
<div className={tl.barGroup}>
|
|
230
|
-
<IconButton icon="grid" label="Pixel" onClick={() => actions.addLayer(layerIndex)} />
|
|
238
|
+
<IconButton icon="grid" label="Pixel" disabled={readOnly} onClick={() => actions.addLayer(layerIndex)} />
|
|
231
239
|
<span className={tl.barLabel}>Pixel</span>
|
|
232
240
|
</div>
|
|
233
241
|
<div className={tl.barGroup}>
|
|
234
242
|
<IconButton
|
|
235
243
|
icon="draw-polygon"
|
|
236
244
|
label="Path"
|
|
245
|
+
disabled={readOnly}
|
|
237
246
|
onClick={() => actions.addLayer(layerIndex, 'path')}
|
|
238
247
|
/>
|
|
239
248
|
<span className={tl.barLabel}>Path</span>
|
|
240
249
|
</div>
|
|
241
250
|
<div className={tl.barGroup}>
|
|
242
|
-
<IconButton icon="film" label="Frame" onClick={() => actions.addFrame(frameIndex)} />
|
|
251
|
+
<IconButton icon="film" label="Frame" disabled={readOnly} onClick={() => actions.addFrame(frameIndex)} />
|
|
243
252
|
<span className={tl.barLabel}>Frame</span>
|
|
244
253
|
</div>
|
|
245
254
|
<div className={tl.barSpacer} />
|
|
@@ -249,7 +258,11 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
249
258
|
<span className={tl.durationIcon} title="Default frame duration (ms) — applies to all frames">
|
|
250
259
|
<FaGlyph icon={faStopwatch} className={tl.durationGlyph} />
|
|
251
260
|
</span>
|
|
252
|
-
<GlobalDurationInput
|
|
261
|
+
<GlobalDurationInput
|
|
262
|
+
value={sprite.defaultDurationMs}
|
|
263
|
+
onCommit={actions.setDefaultDuration}
|
|
264
|
+
disabled={readOnly}
|
|
265
|
+
/>
|
|
253
266
|
</div>
|
|
254
267
|
<OnionControls onion={onion} actions={actions} />
|
|
255
268
|
<div className={tl.barGroup}>
|
|
@@ -268,12 +281,13 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
268
281
|
|
|
269
282
|
// The sprite's GLOBAL duration. Commits a finite, positive value on blur/Enter;
|
|
270
283
|
// an empty/invalid entry snaps back to the current value (see CommitInput).
|
|
271
|
-
function GlobalDurationInput({ value, onCommit }) {
|
|
284
|
+
function GlobalDurationInput({ value, onCommit, disabled = false }) {
|
|
272
285
|
return (
|
|
273
286
|
<CommitInput
|
|
274
287
|
text={String(value)}
|
|
275
288
|
className={tl.globalDuration}
|
|
276
289
|
title="Global frame duration (ms) — inherited by every frame without its own override"
|
|
290
|
+
disabled={disabled}
|
|
277
291
|
onCommit={(draft) => {
|
|
278
292
|
const ms = Number(draft);
|
|
279
293
|
if (Number.isFinite(ms) && ms > 0) onCommit(ms);
|
|
@@ -322,7 +336,7 @@ function OnionControls({ onion, actions }) {
|
|
|
322
336
|
);
|
|
323
337
|
}
|
|
324
338
|
|
|
325
|
-
function TimelineGrid({ sprite, text, selection, actions }) {
|
|
339
|
+
function TimelineGrid({ sprite, text, selection, actions, readOnly = false }) {
|
|
326
340
|
const frames = sprite.frames;
|
|
327
341
|
// Render rows top-down: the last layer (top of the composite stack) first.
|
|
328
342
|
const rows = sprite.layers.map((_, i) => i).reverse();
|
|
@@ -353,12 +367,13 @@ function TimelineGrid({ sprite, text, selection, actions }) {
|
|
|
353
367
|
const openMenu = useCallback(
|
|
354
368
|
(target, event) => {
|
|
355
369
|
selectTarget(target);
|
|
370
|
+
if (readOnly) return;
|
|
356
371
|
const rect = event.currentTarget?.getBoundingClientRect?.();
|
|
357
372
|
const x = Number.isFinite(event.clientX) ? event.clientX : rect?.left + rect?.width / 2;
|
|
358
373
|
const y = Number.isFinite(event.clientY) ? event.clientY : rect?.top + rect?.height / 2;
|
|
359
374
|
setMenu({ ...target, x: x ?? 0, y: y ?? 0 });
|
|
360
375
|
},
|
|
361
|
-
[selectTarget]
|
|
376
|
+
[selectTarget, readOnly]
|
|
362
377
|
);
|
|
363
378
|
const press = useTimelineItemPress(openMenu);
|
|
364
379
|
const activateTarget = (target, event) => {
|
|
@@ -412,6 +427,7 @@ function TimelineGrid({ sprite, text, selection, actions }) {
|
|
|
412
427
|
onPointerDown={(event) => press.onPointerDown(event, { kind: 'layer', layerIndex: li })}
|
|
413
428
|
actions={actions}
|
|
414
429
|
onContextMenu={openLayerMenu}
|
|
430
|
+
readOnly={readOnly}
|
|
415
431
|
/>
|
|
416
432
|
{frames.map((_, f) => (
|
|
417
433
|
<CellThumb
|
|
@@ -744,7 +760,7 @@ const kindBadgeStyle = {
|
|
|
744
760
|
// on blur/Enter, re-syncing to `text` when idle. The caller's `onCommit(draft)`
|
|
745
761
|
// interprets the raw string (set / clear / revert). Shared by the per-frame and
|
|
746
762
|
// global duration fields so their edit/commit behavior stays identical.
|
|
747
|
-
function CommitInput({ text, className, placeholder, title, onCommit }) {
|
|
763
|
+
function CommitInput({ text, className, placeholder, title, onCommit, disabled = false }) {
|
|
748
764
|
const [draft, setDraft] = useState(text);
|
|
749
765
|
const editing = useRef(false);
|
|
750
766
|
if (!editing.current && draft !== text) setDraft(text);
|
|
@@ -759,6 +775,7 @@ function CommitInput({ text, className, placeholder, title, onCommit }) {
|
|
|
759
775
|
placeholder={placeholder}
|
|
760
776
|
title={title}
|
|
761
777
|
inputMode="numeric"
|
|
778
|
+
disabled={disabled}
|
|
762
779
|
onFocus={() => (editing.current = true)}
|
|
763
780
|
onChange={(event) => setDraft(event.target.value)}
|
|
764
781
|
onBlur={commit}
|
|
@@ -807,7 +824,7 @@ function FaGlyph({ icon, className = tl.visGlyph }) {
|
|
|
807
824
|
);
|
|
808
825
|
}
|
|
809
826
|
|
|
810
|
-
function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions, onContextMenu }) {
|
|
827
|
+
function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions, onContextMenu, readOnly = false }) {
|
|
811
828
|
return (
|
|
812
829
|
<div
|
|
813
830
|
className={active ? tl.layerHead + ' ' + tl.layerHeadActive : tl.layerHead}
|
|
@@ -832,6 +849,7 @@ function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions,
|
|
|
832
849
|
max="100"
|
|
833
850
|
value={Math.round(layer.opacity * 100)}
|
|
834
851
|
title={`Opacity ${Math.round(layer.opacity * 100)}%`}
|
|
852
|
+
disabled={readOnly}
|
|
835
853
|
onClick={(event) => event.stopPropagation()}
|
|
836
854
|
onChange={(event) => actions.patchLayer(index, { opacity: Number(event.target.value) / 100 })}
|
|
837
855
|
/>
|
|
@@ -108,6 +108,12 @@
|
|
|
108
108
|
text-align: center;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
.globalDuration:disabled {
|
|
112
|
+
opacity: 0.55;
|
|
113
|
+
cursor: default;
|
|
114
|
+
color: var(--castle-inspector-muted);
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
/* Icon-only label (stopwatch) sitting next to the global duration input, in
|
|
112
118
|
place of the old "Duration … ms · all frames" text. */
|
|
113
119
|
.durationIcon {
|
|
@@ -333,6 +339,11 @@
|
|
|
333
339
|
accent-color: var(--castle-selected);
|
|
334
340
|
}
|
|
335
341
|
|
|
342
|
+
.opacity:disabled {
|
|
343
|
+
opacity: 0.4;
|
|
344
|
+
cursor: default;
|
|
345
|
+
}
|
|
346
|
+
|
|
336
347
|
.opacity::-webkit-slider-runnable-track {
|
|
337
348
|
height: 3px;
|
|
338
349
|
border-radius: 2px;
|
|
@@ -69,13 +69,25 @@ function parseBlueprintText(text) {
|
|
|
69
69
|
function resolveTemplateRefs(template, files, fromPath) {
|
|
70
70
|
if (!template) return template;
|
|
71
71
|
for (const props of Object.values(template.components ?? {})) {
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
resolveRefsIn(props, files, fromPath);
|
|
73
|
+
}
|
|
74
|
+
return template;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Nested because a ref is not always a bare prop: `Shape.art` can be one string
|
|
78
|
+
// for the whole shape OR an object naming a file per face, and an unresolved
|
|
79
|
+
// face ref means an untextured surface with no error anywhere. Rewriting only
|
|
80
|
+
// when the resolved path is actually in the file map is what keeps this from
|
|
81
|
+
// mangling strings that were never refs.
|
|
82
|
+
function resolveRefsIn(props, files, fromPath) {
|
|
83
|
+
for (const [key, value] of Object.entries(props ?? {})) {
|
|
84
|
+
if (typeof value === 'string' && value) {
|
|
74
85
|
const resolved = resolveDeckFile(value, fromPath);
|
|
75
86
|
if (resolved !== value && files?.[resolved] !== undefined) props[key] = resolved;
|
|
87
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
88
|
+
resolveRefsIn(value, files, fromPath);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
|
-
return template;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
// Resolve a blueprint's template from the LIVE files map -- the load-bearing
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Consumer side of the serve's `files_changed` broadcast.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// Consumer side of the serve's `files_changed` broadcast. Data files are
|
|
2
|
+
// re-read over the serve's files API and folded into React state, so an editor
|
|
3
|
+
// shows a drawing someone else just changed without reloading anything.
|
|
4
|
+
//
|
|
5
|
+
// A CODE change does nothing here. Only a page reload picks up new code, and a
|
|
6
|
+
// reload thrown at a panel someone is working in loses their place, so the
|
|
7
|
+
// decision belongs to the person: every panel header carries a reload control,
|
|
8
|
+
// and the shell offers the Play panel a restart when it has fallen behind.
|
|
9
9
|
import { useEffect, useRef, useState } from 'react';
|
|
10
|
-
import { onFilesChanged
|
|
10
|
+
import { onFilesChanged } from 'castle-web-sdk';
|
|
11
11
|
import { initialFiles } from './files';
|
|
12
12
|
|
|
13
13
|
const DATA_EXTS = ['.scene', '.pxart', '.sprite', '.style', '.drawing'];
|
|
@@ -17,7 +17,20 @@ export function isDataFile(path) {
|
|
|
17
17
|
return DATA_EXTS.some((ext) => path.endsWith(ext));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Which iframe is speaking -- every panel runs its own copy of this.
|
|
21
|
+
function panelTag() {
|
|
22
|
+
try {
|
|
23
|
+
const q = new URLSearchParams(location.search);
|
|
24
|
+
return q.get('panel') || q.get('file') || 'deck';
|
|
25
|
+
} catch {
|
|
26
|
+
return 'deck';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Whether a change means this bundle's code is stale. Only used for the log
|
|
31
|
+
// line -- nothing acts on it -- but knowing a panel went stale and when is what
|
|
32
|
+
// makes a vague "it looked wrong" report checkable against `.castle/serve.log`.
|
|
33
|
+
function isCodeChange(change) {
|
|
21
34
|
if (isDataFile(change.path)) return false;
|
|
22
35
|
if (change.path.endsWith('.html')) return true;
|
|
23
36
|
// In the module graph (directly or as someone's import) -> our code changed.
|
|
@@ -37,34 +50,34 @@ async function readDeckFile(path) {
|
|
|
37
50
|
// Deck files as live state: starts from the build-time glob snapshot and stays
|
|
38
51
|
// current as files change on disk from any source (editors, agents, terminal).
|
|
39
52
|
// `shouldSkipPath` lets an editor keep its own in-flight edits from being
|
|
40
|
-
// clobbered by the fs echo of a stale write.
|
|
41
|
-
// applied data change — key a player on it to rebuild from fresh data.
|
|
53
|
+
// clobbered by the fs echo of a stale write.
|
|
42
54
|
export function useLiveDeckFiles({ shouldSkipPath } = {}) {
|
|
43
55
|
const [files, setFiles] = useState(initialFiles);
|
|
44
|
-
const [dataVersion, setDataVersion] = useState(0);
|
|
45
56
|
const skipRef = useRef(shouldSkipPath);
|
|
46
57
|
skipRef.current = shouldSkipPath;
|
|
47
58
|
|
|
48
59
|
useEffect(
|
|
49
60
|
() =>
|
|
50
61
|
onFilesChanged((event) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
const stale = event.changes.find(isCodeChange);
|
|
63
|
+
if (stale) console.log(`[reload] ${panelTag()} code stale <- ${stale.path}`);
|
|
64
|
+
// Data still applies in the same event: a change that carries both is
|
|
65
|
+
// one edit, and holding the data back would leave the panel showing
|
|
66
|
+
// neither the old state nor the new one.
|
|
55
67
|
const dataChanges = event.changes.filter(
|
|
56
68
|
(change) => isDataFile(change.path) && !skipRef.current?.(change.path)
|
|
57
69
|
);
|
|
58
70
|
if (dataChanges.length === 0) return;
|
|
59
|
-
|
|
71
|
+
console.log(`[reload] ${panelTag()} data <- ${dataChanges.map((c) => c.path).join(', ')}`);
|
|
72
|
+
void applyDataChanges(dataChanges, setFiles);
|
|
60
73
|
}),
|
|
61
74
|
[]
|
|
62
75
|
);
|
|
63
76
|
|
|
64
|
-
return { files, setFiles
|
|
77
|
+
return { files, setFiles };
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
async function applyDataChanges(changes, setFiles
|
|
80
|
+
async function applyDataChanges(changes, setFiles) {
|
|
68
81
|
const updates = await Promise.all(
|
|
69
82
|
changes.map(async (change) => {
|
|
70
83
|
if (change.event === 'delete') return { path: change.path, text: null };
|
|
@@ -85,5 +98,4 @@ async function applyDataChanges(changes, setFiles, setDataVersion) {
|
|
|
85
98
|
}
|
|
86
99
|
return next;
|
|
87
100
|
});
|
|
88
|
-
setDataVersion((version) => version + 1);
|
|
89
101
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SceneRuntime (see engine/scene.js `makeScene`) to register itself via
|
|
4
4
|
// `runtime.registerSystem(...)`. A system is a plain object with an optional
|
|
5
5
|
// `afterBehaviors(scene, dt)` hook, run once per frame after all behavior
|
|
6
|
-
// updates. Empty in a kit with no `systems/` dir
|
|
6
|
+
// updates. Empty in a kit with no `systems/` dir; a kit adds a
|
|
7
7
|
// system by dropping a file here -- no edits to the engine required. Symmetric
|
|
8
8
|
// with editors/behaviorRegistry.js.
|
|
9
9
|
// Root-anchored, deck + imports (see engine/files.js).
|
|
@@ -740,8 +740,8 @@ function settleSheet(drag, setHeight, expandedRef) {
|
|
|
740
740
|
const settled = setHeight(drag.startHeight - (drag.lastY - drag.startY));
|
|
741
741
|
if (settled > peek + 2) expandedRef.current = settled;
|
|
742
742
|
}
|
|
743
|
-
// Persist a sheet's resting height across reloads (
|
|
744
|
-
//
|
|
743
|
+
// Persist a sheet's resting height across reloads (picking up a code change
|
|
744
|
+
// means reloading the panel, which would otherwise reset every sheet to the
|
|
745
745
|
// default size). Keyed per sheet via `storageKey`; sessionStorage-scoped and
|
|
746
746
|
// best-effort, the same degrade-to-memory-only contract as editorHistory.
|
|
747
747
|
function sheetHeightStorageKey(key) {
|
|
@@ -1550,6 +1550,11 @@
|
|
|
1550
1550
|
cursor: default;
|
|
1551
1551
|
}
|
|
1552
1552
|
|
|
1553
|
+
.button:disabled:hover,
|
|
1554
|
+
.iconButton:disabled:hover {
|
|
1555
|
+
background: var(--castle-inspector-button-bg);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1553
1558
|
.actorList {
|
|
1554
1559
|
display: grid;
|
|
1555
1560
|
gap: 4px;
|
|
@@ -1608,6 +1613,13 @@
|
|
|
1608
1613
|
display: none;
|
|
1609
1614
|
}
|
|
1610
1615
|
|
|
1616
|
+
/* Read-only paint/inspector chrome: still there so the layout doesn't jump,
|
|
1617
|
+
but clearly not interactive. */
|
|
1618
|
+
.drawingReadOnly {
|
|
1619
|
+
opacity: 0.45;
|
|
1620
|
+
pointer-events: none;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1611
1623
|
/* Stack the tool rail and the undo/redo rail vertically (the shared rule above is
|
|
1612
1624
|
a flex row, which pushed the second rail out of the 50px column). */
|
|
1613
1625
|
.artboardToolColumn {
|
|
@@ -1721,6 +1733,22 @@
|
|
|
1721
1733
|
color: var(--castle-selected-ink);
|
|
1722
1734
|
}
|
|
1723
1735
|
|
|
1736
|
+
.finishSegment:disabled {
|
|
1737
|
+
cursor: default;
|
|
1738
|
+
opacity: 0.4;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
.finishSegment:disabled:hover {
|
|
1742
|
+
background: var(--castle-inspector-button-bg);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
.finishSegment.finishSegmentOn:disabled,
|
|
1746
|
+
.finishSegment.finishSegmentOn:disabled:hover {
|
|
1747
|
+
opacity: 0.85;
|
|
1748
|
+
background: var(--castle-selected);
|
|
1749
|
+
color: var(--castle-selected-ink);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1724
1752
|
.canvasSizeSep {
|
|
1725
1753
|
color: var(--castle-inspector-muted);
|
|
1726
1754
|
user-select: none;
|
|
@@ -1742,6 +1770,12 @@
|
|
|
1742
1770
|
background-position: right 6px center;
|
|
1743
1771
|
}
|
|
1744
1772
|
|
|
1773
|
+
.canvasSizeSelect:disabled {
|
|
1774
|
+
opacity: 0.55;
|
|
1775
|
+
cursor: default;
|
|
1776
|
+
color: var(--castle-inspector-muted);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1745
1779
|
.pxArtInspector {
|
|
1746
1780
|
min-width: 0;
|
|
1747
1781
|
border-left: 1px solid var(--castle-inspector-divider);
|
|
@@ -2121,6 +2155,14 @@
|
|
|
2121
2155
|
cursor: default;
|
|
2122
2156
|
}
|
|
2123
2157
|
|
|
2158
|
+
.drawingToolButton.drawingToolSelected:disabled {
|
|
2159
|
+
opacity: 0.7;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
.drawingToolButton:disabled:hover {
|
|
2163
|
+
background: var(--castle-inspector-button-bg);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2124
2166
|
/* Active / selected square icon button -- the eyedropper reference: a light
|
|
2125
2167
|
fill with a dark glyph. The single selected/engaged look shared by the
|
|
2126
2168
|
IconButton component (`active`), the onion-skin toggle, and the pixel
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# physics-3d kit
|
|
2
|
+
|
|
3
|
+
A 3D actor/behavior kit: three.js rendering with PBR lighting and rapier
|
|
4
|
+
physics, built ON TOP of the `physics-2d` kit as an import. The actor /
|
|
5
|
+
behavior / scene / blueprint model, deck file map, live reload, and the
|
|
6
|
+
`.pxart` pixel-art pipeline (including its editor) all come from
|
|
7
|
+
`imports/castle.physics-2d/` and work exactly as that kit's CLAUDE.md
|
|
8
|
+
describes. This file covers what is different in 3D.
|
|
9
|
+
|
|
10
|
+
## Quick reference
|
|
11
|
+
|
|
12
|
+
<!-- Injected into Castle's create assistant every turn; keep this compact and factual. -->
|
|
13
|
+
|
|
14
|
+
- 3D actor / behavior / scene framework: three.js + rapier, 1 unit = 1 meter,
|
|
15
|
+
y up. Scenes are `scenes/*.scene` (blueprints too, under `blueprints/`),
|
|
16
|
+
behaviors `behaviors/*.jsx` -- same JSON shapes as physics-2d, but the
|
|
17
|
+
component vocabulary is 3D (`Transform`, `Shape`, `Body`, `Solid`, ...).
|
|
18
|
+
- Every actor needs a `Transform` (`x/y/z`, `rotationX/Y/Z` in degrees).
|
|
19
|
+
Every Transform actor gets a rapier body automatically -- fixed by default,
|
|
20
|
+
`Body { type: 'dynamic' }` to be simulated, `'kinematic'` to be unpushable
|
|
21
|
+
but follow Transform edits (doors, platforms).
|
|
22
|
+
- Colliders come from `Shape` and are **sensors unless solid**: add a
|
|
23
|
+
`Solid {}` behavior (or `Shape.solid: true`) to block; leave it off for
|
|
24
|
+
pickups / trigger zones. Behaviors get `onCollisionEnter(actor, scene,
|
|
25
|
+
other)` / `onCollisionExit` for both contacts and sensor overlaps.
|
|
26
|
+
- `Shape`: `kind` `'box' | 'sphere' | 'capsule' | 'cylinder'` with
|
|
27
|
+
`width/height/depth` or `radius`, PBR knobs (`color`, `roughness`,
|
|
28
|
+
`metalness`, `clearcoat`, `emissive`, `emissiveIntensity`), and `art` -- a
|
|
29
|
+
`.pxart`/image path baked as a nearest-filtered albedo map. On boxes `art`
|
|
30
|
+
may be a per-face object `{ all, sides, front, back, left, right, top,
|
|
31
|
+
bottom }`; any ref can pick a frame of a multi-frame pxart with
|
|
32
|
+
`path#<frame>`. A flat `color` with no art is a fine look.
|
|
33
|
+
- Motion goes through `scene.physics3d`, never by writing a dynamic actor's
|
|
34
|
+
Transform: `setVelocity(actor, {x,y,z})`, `applyImpulse`, `getVelocity`,
|
|
35
|
+
`mass`, `castRay(origin, dir, maxToi, excludeActor)` (rays skip sensors).
|
|
36
|
+
Fixed/kinematic actors ARE moved by writing their Transform.
|
|
37
|
+
- The scene has the lighting rig built in (hemisphere + ambient + one
|
|
38
|
+
shadow-casting sun, ground plane at y=0); meshes cast and receive shadows
|
|
39
|
+
automatically. Don't add your own lights for a normal scene.
|
|
40
|
+
- Built-in gameplay behaviors: `Player` (WASD/arrows move, space jumps,
|
|
41
|
+
drag orbits the camera, faux-isometric follow cam), `Pickup` (sensor
|
|
42
|
+
collectible -> player score), `Door` (kinematic slide-up when the player is
|
|
43
|
+
near), `Lookable { text }` (hud caption when the player faces it nearby).
|
|
44
|
+
- Hud: behaviors write `scene.hud = { score, look }`; the player behavior
|
|
45
|
+
already does.
|
|
46
|
+
- Scene switching: place a `Portal { scene: 'scenes/other.scene' }` actor (a
|
|
47
|
+
sensor -- the portal blueprint is a glowing ring), or set
|
|
48
|
+
`scene.requestedScene = 'scenes/other.scene'` from any behavior (applied
|
|
49
|
+
between frames; safer than calling `scene.loadFromFile` mid-callback). A
|
|
50
|
+
load rebuilds the physics world and resets the scene to its authored state;
|
|
51
|
+
`scene.persistent` survives loads -- the player's score, collected-pickup
|
|
52
|
+
memory, and camera angle live there, so they cross scenes.
|
|
53
|
+
- A scene can dress the ground: `"ground": { "art": "drawings/floor.pxart",
|
|
54
|
+
"tile": 2, "color": "#..." }` at the scene file's top level (`tile` = meters
|
|
55
|
+
per texture repeat). `Shape.artRepeat: [u, v]` tiles a texture across a
|
|
56
|
+
face so long walls don't stretch.
|
|
57
|
+
- `.scene` files open in the kit's 3d scene editor (same chrome as the 2d
|
|
58
|
+
kit's): blueprint hotbar (tap to inspect/edit the template, drag onto the
|
|
59
|
+
stage to place), orbit camera (drag orbits, wheel zooms), click to select /
|
|
60
|
+
click again to cycle / shift to multi-select, move & rotate gizmos (keys
|
|
61
|
+
1/2/3 pick select/move/rotate), floating clone/delete toolbar on the
|
|
62
|
+
selection, behavior-panel inspector with instance-override tints, undo/redo
|
|
63
|
+
(cmd+z / cmd+shift+z), cmd+d duplicate, delete key removes. `.pxart` opens
|
|
64
|
+
in the imported pixel-art editor.
|
|
65
|
+
- Blueprint thumbnails in the hotbar / inspector are live 3d corner-view
|
|
66
|
+
renders of each template (engine3d/thumbnails.js) -- no icon data needed on
|
|
67
|
+
the blueprint.
|
|
68
|
+
- 3d models are `.pxmodel` files under `models/` -- part-based JSON (named
|
|
69
|
+
primitive parts with hierarchy, per-part material/art), spec in
|
|
70
|
+
docs/pxmodel-format.md. A `Model` behavior (`{ file, scale, yOffset }`)
|
|
71
|
+
renders one instead of the Shape mesh; part ids legL/legR/armL/armR/torso
|
|
72
|
+
walk-animate automatically from observed motion. `.pxmodel` opens in the
|
|
73
|
+
kit's model editor (parts list, gizmos, part inspector, walk preview);
|
|
74
|
+
`scenes/model-lab.scene` is an all-model scene for trying it.
|
|
75
|
+
- Controls are tank-style: W/S (or up/down) walk forward/back along facing,
|
|
76
|
+
A/D (or left/right) turn; a pointer/touch drag is a virtual stick where the
|
|
77
|
+
drag angle blends walking and turning. Space jumps.
|
|
78
|
+
- After any edit: `npm run restart`. After a coherent change: `npm run check`
|
|
79
|
+
(lint + duplicate gate + single-file bundle) must pass.
|
|
80
|
+
|
|
81
|
+
## What lives where
|
|
82
|
+
|
|
83
|
+
- `engine3d/` -- the renderer (`world3d.js`, vase-style rig: PCFSoft shadows,
|
|
84
|
+
EffectComposer RenderPass + OutputPass), `materials.js` (pxart -> albedo
|
|
85
|
+
textures, per-face box materials), `Scene3DPlayer.jsx` / `PlayOnly3D.jsx`
|
|
86
|
+
(the play mount). Internal; behaviors shouldn't import from here.
|
|
87
|
+
- `systems/physics3d.js` -- the rapier system, auto-discovered by the imported
|
|
88
|
+
engine's `systems/*.js` seam. Fixed 1/60 step, body-per-actor reconcile,
|
|
89
|
+
collision/overlap events, `scene.physics3d` API.
|
|
90
|
+
- `behaviors/` -- the 3D component vocabulary plus gameplay behaviors. Same
|
|
91
|
+
rules as physics-2d: `static behaviorName`, `defaultProps`, per-frame
|
|
92
|
+
instances (state on `actor.runtime`).
|
|
93
|
+
- `imports/castle.physics-2d/` -- read-only. Its engine modules are imported
|
|
94
|
+
directly (scene runtime, files, live reload, pxart, art, behavior
|
|
95
|
+
registry); its 2D-only pieces (canvas ScenePlayer, matter physics, 2D
|
|
96
|
+
editors other than pxart) are present but unused here.
|
|
97
|
+
|
|
98
|
+
## Known state / caveats
|
|
99
|
+
|
|
100
|
+
- The 2D kit's behaviors (Layout, Sprite, Collider, RigidBody, ...) are still
|
|
101
|
+
in the behavior registry via the import's root-anchored globs, and its
|
|
102
|
+
matter-js system installs (idle) into every runtime. Harmless but present;
|
|
103
|
+
don't attach 2D components to 3D actors.
|
|
104
|
+
- Transform is the source of truth: dynamic bodies write back position (and
|
|
105
|
+
rotation unless `lockRotations`), fixed/kinematic bodies follow Transform.
|
|
106
|
+
A locked-rotation dynamic actor (the player) keeps `rotationY` free for
|
|
107
|
+
facing.
|
|
108
|
+
- Space is bound to jump in this kit (unlike physics-2d, where the editor
|
|
109
|
+
reserves it).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Physics options. Every actor with a Transform gets a rapier body whether or
|
|
2
|
+
// not it carries this behavior -- fixed by default. Add a Body to opt into
|
|
3
|
+
// motion: 'dynamic' is simulated (gravity, pushes), 'kinematic' is unpushable
|
|
4
|
+
// but follows Transform edits with proper contact resolution (moving doors,
|
|
5
|
+
// platforms). `lockRotations` keeps a dynamic body upright (characters).
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { SelectField } from '@imports/castle.physics-2d/engine/ui';
|
|
8
|
+
import { AutoFields, overrideProps } from '@imports/castle.physics-2d/engine/autoInspector';
|
|
9
|
+
|
|
10
|
+
const TYPE_OPTIONS = [
|
|
11
|
+
{ value: 'fixed', label: 'Fixed' },
|
|
12
|
+
{ value: 'dynamic', label: 'Dynamic' },
|
|
13
|
+
{ value: 'kinematic', label: 'Kinematic' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function BodyInspector({ component, setComponent, override }) {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<SelectField
|
|
20
|
+
label="Type"
|
|
21
|
+
value={component.type}
|
|
22
|
+
options={TYPE_OPTIONS}
|
|
23
|
+
onChange={(type) => setComponent({ type })}
|
|
24
|
+
{...overrideProps(override, 'type')}
|
|
25
|
+
/>
|
|
26
|
+
<AutoFields
|
|
27
|
+
defaultProps={Body.defaultProps}
|
|
28
|
+
component={component}
|
|
29
|
+
setComponent={setComponent}
|
|
30
|
+
exclude={['type']}
|
|
31
|
+
override={override}
|
|
32
|
+
/>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Body {
|
|
38
|
+
static Inspector = BodyInspector;
|
|
39
|
+
static behaviorName = 'Body';
|
|
40
|
+
static defaultProps = {
|
|
41
|
+
type: 'fixed', // 'fixed' | 'dynamic' | 'kinematic'
|
|
42
|
+
friction: 0.6,
|
|
43
|
+
restitution: 0,
|
|
44
|
+
gravityScale: 1,
|
|
45
|
+
lockRotations: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
constructor(props) {
|
|
49
|
+
this.props = props;
|
|
50
|
+
}
|
|
51
|
+
}
|