castle-web-cli 0.4.119 → 0.4.121
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/api.js +13 -2
- package/dist/castle-host/host.d.ts +16 -0
- package/dist/castle-host/host.js +249 -11
- package/dist/castle-host/leaderboardPanel.d.ts +73 -0
- package/dist/castle-host/leaderboardPanel.js +714 -0
- package/dist/ide.d.ts +3 -0
- package/dist/ide.js +79 -0
- package/dist/index.js +1 -6
- package/dist/init.js +1 -1
- package/dist/preview.d.ts +0 -1
- package/dist/preview.js +18 -23
- package/dist/save-deck.js +33 -2
- package/dist/serve.js +9 -4
- package/dist/shell/assets/index-BkJ87APM.css +1 -0
- package/dist/shell/assets/index-UEwjXWKI.js +434 -0
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +64 -9
- package/kits/physics-2d/behaviors/AnalogStick.jsx +40 -9
- package/kits/physics-2d/behaviors/Collider.jsx +12 -0
- package/kits/physics-2d/behaviors/Draggable.jsx +67 -23
- package/kits/physics-2d/behaviors/Goal.jsx +2 -0
- package/kits/physics-2d/behaviors/Joints.jsx +5 -0
- package/kits/physics-2d/behaviors/RigidBody.jsx +16 -0
- package/kits/physics-2d/behaviors/Slingshot.jsx +48 -16
- package/kits/physics-2d/behaviors/Sound.jsx +11 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +7 -0
- package/kits/physics-2d/behaviors/Tone.jsx +12 -0
- package/kits/physics-2d/behaviors/Video.jsx +6 -0
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/SceneEditor.jsx +53 -8
- package/kits/physics-2d/editors/pixelInspector.jsx +30 -9
- package/kits/physics-2d/editors/pxArtTimeline.jsx +147 -18
- package/kits/physics-2d/engine/ScenePlayer.jsx +9 -3
- package/kits/physics-2d/engine/autoInspector.jsx +28 -3
- package/kits/physics-2d/engine/physics/PhysicsSystem.js +117 -0
- package/kits/physics-2d/engine/physics/controls.js +43 -19
- package/kits/physics-2d/engine/propertyRanges.js +27 -0
- package/kits/physics-2d/engine/scene.js +86 -4
- package/kits/physics-2d/engine/ui.jsx +14 -1
- package/kits/physics-2d/engine/ui.module.css +1 -0
- package/kits/physics-2d/package-lock.json +1 -1
- package/package.json +2 -1
- package/dist/shell/assets/index-WtgFx1s8.js +0 -144
- package/dist/shell/assets/index-Y6cJRRCX.css +0 -1
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
// through scene.physics. (Controls are touch/pointer-first; keyboard, when
|
|
6
6
|
// added, should only DUPLICATE an on-screen control, never be the only input.)
|
|
7
7
|
|
|
8
|
+
// Authored Draggable `stiffness` (0..1, 0 = floppy .. 1 = rigid) -> the matter
|
|
9
|
+
// constraint values that hold the object. Geometric, like joints.js's rope map
|
|
10
|
+
// and for the same reason: matter's useful range here is ~0.001 (slack) to ~0.4
|
|
11
|
+
// (tight), and everything above that is indistinguishably rigid -- so a linear
|
|
12
|
+
// prop spends nearly all of its travel in "stiff" and never reaches the loose
|
|
13
|
+
// end at all.
|
|
14
|
+
//
|
|
15
|
+
// Damping rides along rather than being its own knob: a slack band should be
|
|
16
|
+
// free to oscillate and a tight one shouldn't, so one control moves the whole
|
|
17
|
+
// feel together.
|
|
18
|
+
export function dragHold(stiffness) {
|
|
19
|
+
const s = Math.min(1, Math.max(0, stiffness ?? 0.3));
|
|
20
|
+
return { stiffness: 0.001 * Math.pow(400, s), damping: 0.02 + 0.25 * s };
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
export function centerOf(layout) {
|
|
9
24
|
return { x: layout.x + layout.width / 2, y: layout.y + layout.height / 2 };
|
|
10
25
|
}
|
|
@@ -21,26 +36,35 @@ export function clampLength(v, max) {
|
|
|
21
36
|
return { x: v.x * s, y: v.y * s };
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
// Springy chase velocity toward a target point (Draggable): a fraction of the
|
|
25
|
-
// gap per step, so the body accelerates toward the pointer and still collides.
|
|
26
|
-
export function chaseVelocity(target, layout, props = {}) {
|
|
27
|
-
const stiffness = props.stiffness ?? 0.5;
|
|
28
|
-
const c = centerOf(layout);
|
|
29
|
-
return { x: (target.x - c.x) * stiffness, y: (target.y - c.y) * stiffness };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
39
|
// --- Touch ownership -------------------------------------------------------
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Each control claims the pointer it started on (scene.claimPointer), so a
|
|
41
|
+
// finger drives exactly one thing and the others leave it alone. Two priorities
|
|
42
|
+
// are all this needs:
|
|
43
|
+
//
|
|
44
|
+
// TARGETED the press landed on THIS actor's collider, so the intent is
|
|
45
|
+
// unambiguous -- a Draggable being grabbed, a Slingshot pressed on
|
|
46
|
+
// its own body.
|
|
47
|
+
// GREEDY the control takes any press anywhere (a Slingshot with
|
|
48
|
+
// grabAnywhere, an AnalogStick), so it must yield to a targeted one.
|
|
49
|
+
//
|
|
50
|
+
// Ranking them explicitly is what makes the outcome independent of actor order.
|
|
51
|
+
// Previously this was a `pressOnDraggable` query that hardcoded one behavior's
|
|
52
|
+
// name into shared code and had no answer at all once two fingers were down.
|
|
53
|
+
export const TARGETED_CLAIM = 10;
|
|
54
|
+
export const GREEDY_CLAIM = 0;
|
|
55
|
+
|
|
56
|
+
// Take the first fresh press this frame that `actorId` is allowed to have, and
|
|
57
|
+
// claim it. `priorityFor(pointer)` returns the claim priority to bid, or null to
|
|
58
|
+
// pass on that pointer -- which is the only part that differs between controls.
|
|
59
|
+
// Returns the claimed pointer, or null if there was nothing to take.
|
|
60
|
+
export function acquirePress(scene, actorId, priorityFor) {
|
|
61
|
+
for (const p of scene.pointers.values()) {
|
|
62
|
+
if (!p.justPressed) continue;
|
|
63
|
+
const priority = priorityFor(p);
|
|
64
|
+
if (priority == null) continue;
|
|
65
|
+
if (scene.claimPointer(p.id, actorId, priority)) return p;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
// Run `draw(ctx)` in world space, undoing the actor's Layout rotation that the
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Named inspector ranges for behaviors' `static propertyMeta`.
|
|
2
|
+
//
|
|
3
|
+
// A numeric prop with no meta renders as an unbounded scrubber stepping by whole
|
|
4
|
+
// numbers, which is wrong for anything living in 0..1. The recurring shapes get a
|
|
5
|
+
// name here so "a normalized level" is defined once instead of restated in every
|
|
6
|
+
// behavior -- and so a range change lands everywhere at once.
|
|
7
|
+
//
|
|
8
|
+
// A `max` is a hard authoring ceiling, not just a slider bound: typed entry is
|
|
9
|
+
// clamped to it too (see NumberField's `commit`). So only give something a max
|
|
10
|
+
// when exceeding it is meaningless or actively breaks the simulation.
|
|
11
|
+
|
|
12
|
+
// A normalized 0..1 level: volume, and anything else expressed as a fraction.
|
|
13
|
+
export const UNIT = { min: 0, max: 1, step: 0.05 };
|
|
14
|
+
|
|
15
|
+
// Stereo position, -1 (left) .. +1 (right). Hard-bounded by definition.
|
|
16
|
+
export const PAN = { min: -1, max: 1, step: 0.05 };
|
|
17
|
+
|
|
18
|
+
// Open-ended and non-negative -- a floor, but no ceiling, for quantities matter
|
|
19
|
+
// or the browser treats as unbounded (restitution, friction, seconds).
|
|
20
|
+
export const POSITIVE = { min: 0, step: 0.05 };
|
|
21
|
+
|
|
22
|
+
// Matter's air/angular friction. Above 1 it flips the velocity sign and the body
|
|
23
|
+
// destabilizes, so 1 is a real ceiling. Finer step: the useful values are small.
|
|
24
|
+
export const DAMPING = { min: 0, max: 1, step: 0.01 };
|
|
25
|
+
|
|
26
|
+
// A distance in card units (px).
|
|
27
|
+
export const PIXELS = { min: 0, step: 5 };
|
|
@@ -41,6 +41,12 @@ export class SceneRuntime {
|
|
|
41
41
|
this.files = files ?? {};
|
|
42
42
|
this.time = 0;
|
|
43
43
|
this.keys = new Set();
|
|
44
|
+
// Every active pointer (finger / mouse / stylus), keyed by the browser's
|
|
45
|
+
// pointerId. A control that starts a gesture latches the id it started on
|
|
46
|
+
// and follows THAT pointer, so a second finger landing can't steal or drop
|
|
47
|
+
// an in-progress drag. `pointer` stays the primary (first-down) pointer for
|
|
48
|
+
// single-touch behaviors and for decks reading the documented API.
|
|
49
|
+
this.pointers = new Map();
|
|
44
50
|
this.pointer = { x: 0, y: 0, down: false };
|
|
45
51
|
this.data = { actors: [] };
|
|
46
52
|
this.actors = new Map();
|
|
@@ -210,11 +216,85 @@ export class SceneRuntime {
|
|
|
210
216
|
// input path -- the standalone ScenePlayer and the editor's play mode --
|
|
211
217
|
// agree on it; behaviors read `scene.pointer` in world coordinates. Pass
|
|
212
218
|
// `down` to also update the press state.
|
|
213
|
-
|
|
219
|
+
// `pointerId` identifies which finger/device this event came from; the
|
|
220
|
+
// browser reuses it for every event of one contact, press through release.
|
|
221
|
+
setPointerFromScreen(canvas, clientX, clientY, down, pointerId = 0) {
|
|
214
222
|
const point = screenToCard(canvas, clientX, clientY);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
const x = point.x + (this.camera?.x ?? 0);
|
|
224
|
+
const y = point.y + (this.camera?.y ?? 0);
|
|
225
|
+
let p = this.pointers.get(pointerId);
|
|
226
|
+
if (!p) {
|
|
227
|
+
// Only a press creates a pointer. A move with no press is a mouse hover,
|
|
228
|
+
// and a stray event after release must not resurrect a finished contact.
|
|
229
|
+
if (down !== true) return this.syncPrimaryPointer(x, y);
|
|
230
|
+
p = { id: pointerId, x, y, down: false, justPressed: false, claimedBy: null, claimPriority: 0 };
|
|
231
|
+
this.pointers.set(pointerId, p);
|
|
232
|
+
}
|
|
233
|
+
p.x = x;
|
|
234
|
+
p.y = y;
|
|
235
|
+
// The press edge is per-pointer, so a finger landing while another is
|
|
236
|
+
// already held still registers as a new press (a shared `down` flag would
|
|
237
|
+
// show no rising edge and the second press would be invisible).
|
|
238
|
+
if (down === true && !p.down) p.justPressed = true;
|
|
239
|
+
if (down !== undefined) p.down = down;
|
|
240
|
+
this.syncPrimaryPointer(x, y);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Claim a pointer for one gesture, so two controls can't both act on the same
|
|
244
|
+
// finger. Returns whether `ownerId` holds it afterwards.
|
|
245
|
+
//
|
|
246
|
+
// `priority` resolves the case that actor order otherwise decides arbitrarily:
|
|
247
|
+
// a TARGETED control (a Draggable whose collider was actually pressed) must
|
|
248
|
+
// beat a GREEDY one (a Slingshot set to grab anywhere, an AnalogStick that
|
|
249
|
+
// takes any press), no matter which actor updates first. A higher priority
|
|
250
|
+
// takes the pointer from a lower one; equal or lower leaves it alone.
|
|
251
|
+
//
|
|
252
|
+
// Losing a claim is how the greedy control learns to stand down, and it costs
|
|
253
|
+
// nothing visible: on the press frame its own displacement from its origin is
|
|
254
|
+
// still zero, so it has commanded no velocity yet.
|
|
255
|
+
claimPointer(pointerId, ownerId, priority = 0) {
|
|
256
|
+
const p = this.pointers.get(pointerId);
|
|
257
|
+
if (!p) return false;
|
|
258
|
+
if (p.claimedBy == null || p.claimedBy === ownerId || priority > p.claimPriority) {
|
|
259
|
+
p.claimedBy = ownerId;
|
|
260
|
+
p.claimPriority = priority;
|
|
261
|
+
}
|
|
262
|
+
return p.claimedBy === ownerId;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// True while `ownerId` still holds this pointer. A control that latched a
|
|
266
|
+
// pointer checks this each frame so it releases when outranked. Claims need no
|
|
267
|
+
// cleanup: they live on the pointer entry, which disappears when it lifts.
|
|
268
|
+
ownsPointer(pointerId, ownerId) {
|
|
269
|
+
return this.pointers.get(pointerId)?.claimedBy === ownerId;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Released pointers are reaped at the end of the frame rather than on the
|
|
273
|
+
// event, so a tap that begins and ends between two frames is still seen once
|
|
274
|
+
// -- and so a control latched to a pointer observes `down: false` before the
|
|
275
|
+
// entry disappears.
|
|
276
|
+
reapPointers() {
|
|
277
|
+
for (const [id, p] of this.pointers) {
|
|
278
|
+
p.justPressed = false;
|
|
279
|
+
if (!p.down) this.pointers.delete(id);
|
|
280
|
+
}
|
|
281
|
+
this.syncPrimaryPointer();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// `pointer` mirrors the primary -- the oldest pointer still held (Map keeps
|
|
285
|
+
// insertion order) -- so single-touch behaviors and decks reading the
|
|
286
|
+
// documented `scene.pointer` API behave exactly as before.
|
|
287
|
+
syncPrimaryPointer(fallbackX, fallbackY) {
|
|
288
|
+
for (const p of this.pointers.values()) {
|
|
289
|
+
if (!p.down) continue;
|
|
290
|
+
this.pointer.x = p.x;
|
|
291
|
+
this.pointer.y = p.y;
|
|
292
|
+
this.pointer.down = true;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (fallbackX !== undefined) this.pointer.x = fallbackX;
|
|
296
|
+
if (fallbackY !== undefined) this.pointer.y = fallbackY;
|
|
297
|
+
this.pointer.down = false;
|
|
218
298
|
}
|
|
219
299
|
|
|
220
300
|
update(dt) {
|
|
@@ -228,6 +308,8 @@ export class SceneRuntime {
|
|
|
228
308
|
for (const system of this.systems) {
|
|
229
309
|
system.afterBehaviors?.(this, dt);
|
|
230
310
|
}
|
|
311
|
+
// Every behavior and system has now seen this frame's presses and releases.
|
|
312
|
+
this.reapPointers();
|
|
231
313
|
}
|
|
232
314
|
|
|
233
315
|
forEachBehavior(actor, callback) {
|
|
@@ -458,6 +458,19 @@ function NumberInlineInput({ value, min, max, step, onCommit, onDone }) {
|
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
// Snap to a multiple of `step`. `Math.round(v / step) * step` alone reintroduces
|
|
462
|
+
// binary error the moment `step` isn't representable in base 2 -- 0.05 x 14 is
|
|
463
|
+
// 0.7000000000000001, which then gets stored and rendered in full -- so re-round
|
|
464
|
+
// to the decimal places the step itself implies.
|
|
465
|
+
function snapToStep(v, step) {
|
|
466
|
+
const snapped = Math.round(v / step) * step;
|
|
467
|
+
const text = String(step);
|
|
468
|
+
const decimals = text.includes('e-')
|
|
469
|
+
? Number(text.split('e-')[1])
|
|
470
|
+
: (text.split('.')[1] ?? '').length;
|
|
471
|
+
return Number(snapped.toFixed(Math.min(decimals, 100)));
|
|
472
|
+
}
|
|
473
|
+
|
|
461
474
|
export function NumberField({ label, value, onChange, min, max, step = 1, overridden, defaultValue, onReset }) {
|
|
462
475
|
const current = Number.isFinite(value) ? (value ?? 0) : 0;
|
|
463
476
|
// A field with BOTH a min and max is a true slider (fill + thumb), the pointer's X
|
|
@@ -481,7 +494,7 @@ export function NumberField({ label, value, onChange, min, max, step = 1, overri
|
|
|
481
494
|
return v;
|
|
482
495
|
}
|
|
483
496
|
function scrubTo(v) {
|
|
484
|
-
if (step) v =
|
|
497
|
+
if (step) v = snapToStep(v, step);
|
|
485
498
|
v = clampVal(v);
|
|
486
499
|
if (v !== current) onChange(v);
|
|
487
500
|
}
|
|
@@ -1797,6 +1797,7 @@
|
|
|
1797
1797
|
z-index: 200;
|
|
1798
1798
|
width: min(280px, calc(100vw - 16px));
|
|
1799
1799
|
max-height: min(420px, calc(100vh - 16px));
|
|
1800
|
+
box-sizing: border-box;
|
|
1800
1801
|
overflow: auto;
|
|
1801
1802
|
padding: 12px 14px 14px;
|
|
1802
1803
|
border: 1px solid var(--castle-inspector-border);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "castle-web-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.121",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"castle-web": "./dist/index.js"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@xterm/xterm": "^6.0.0",
|
|
38
38
|
"codemirror": "^6.0.2",
|
|
39
39
|
"dockview": "^4.13.1",
|
|
40
|
+
"html2canvas": "^1.4.1",
|
|
40
41
|
"marked": "^18.0.5",
|
|
41
42
|
"nanoid": "^5.1.7",
|
|
42
43
|
"open": "^10.0.0",
|