castle-web-cli 0.4.120 → 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/ide.d.ts +1 -0
- package/dist/ide.js +44 -0
- package/dist/init.js +1 -1
- package/dist/shell/assets/{index-Cxmq9Sed.js → index-UEwjXWKI.js} +58 -58
- package/dist/shell/index.html +1 -1
- package/kits/physics-2d/CLAUDE.md +56 -5
- 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 +42 -6
- 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/package-lock.json +1 -1
- package/package.json +2 -1
package/dist/shell/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" type="image/png" sizes="32x32" href="/__castle/ide/favicon-32x32.png" />
|
|
11
11
|
<link rel="icon" type="image/png" sizes="16x16" href="/__castle/ide/favicon-16x16.png" />
|
|
12
12
|
<link rel="icon" href="/__castle/ide/favicon.ico" sizes="any" />
|
|
13
|
-
<script type="module" crossorigin src="/__castle/ide/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/__castle/ide/assets/index-UEwjXWKI.js"></script>
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-BkJ87APM.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
@@ -72,7 +72,21 @@ A behavior is a class. Minimal contract:
|
|
|
72
72
|
// behaviors/MyThing.jsx
|
|
73
73
|
export class MyThing {
|
|
74
74
|
static behaviorName = 'MyThing'; // must match the key used in .scene
|
|
75
|
-
static defaultProps = { speed: 200 };
|
|
75
|
+
static defaultProps = { speed: 200, power: 0.5 };
|
|
76
|
+
|
|
77
|
+
// Optional. Inspector ranges for numeric props. Without one, a number field is
|
|
78
|
+
// an unbounded scrubber stepping by 1 — wrong for anything living in 0..1.
|
|
79
|
+
// Giving BOTH min and max makes it a real slider. A `max` also caps typed
|
|
80
|
+
// entry, so only set one where going past it is meaningless.
|
|
81
|
+
static propertyMeta = {
|
|
82
|
+
power: { min: 0, max: 1, step: 0.05 },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Optional. Other components this one needs BESIDE IT on the same actor.
|
|
86
|
+
// Adding this behavior in the editor adds any that are missing. It does NOT
|
|
87
|
+
// fix a blueprint you write by hand — so when you author a blueprint, include
|
|
88
|
+
// the siblings yourself.
|
|
89
|
+
static expectedSiblings = ['Collider'];
|
|
76
90
|
|
|
77
91
|
constructor(props) {
|
|
78
92
|
this.props = props;
|
|
@@ -231,6 +245,11 @@ In a behavior's `update`, steer bodies through `scene.physics` (never write
|
|
|
231
245
|
- `scene.physics.applyForce(actor, { x, y })` — add a matter-space force.
|
|
232
246
|
- `scene.physics.getVelocity(actor)` — current velocity `{ x, y }`.
|
|
233
247
|
- `scene.physics.isColliding(a, b)` — are these two actors touching right now?
|
|
248
|
+
- `scene.physics.actorAtPoint(x, y)` — topmost actor whose **collider** contains
|
|
249
|
+
the point, or null. Shape-accurate and rotation-aware, so it's what a tap test
|
|
250
|
+
should use. (`scene.actorAt` tests the rectangular Layout box instead, which is
|
|
251
|
+
looser — it's what the editor uses for click-to-select.)
|
|
252
|
+
- `scene.physics.containsPoint(actor, x, y)` — the same test for one actor.
|
|
234
253
|
|
|
235
254
|
### Collision callbacks
|
|
236
255
|
|
|
@@ -254,13 +273,37 @@ export class Coin {
|
|
|
254
273
|
### Built-in controls (add these behaviors, no code needed)
|
|
255
274
|
|
|
256
275
|
These read input and drive the actor through the physics system, so the actor
|
|
257
|
-
still collides with the world. Each needs a `RigidBody` (dynamic) + `Collider
|
|
276
|
+
still collides with the world. Each needs a `RigidBody` (dynamic) + `Collider` --
|
|
277
|
+
**not optional**: no Collider means no physics body at all, and the control is
|
|
278
|
+
silently inert. The editor adds them for you (`expectedSiblings`), but a
|
|
279
|
+
blueprint you write by hand must include them itself.
|
|
258
280
|
Controls are **touch/pointer-first** (Castle games are played on mobile first;
|
|
259
281
|
touch degrades to mouse). Keyboard, if added, should only *duplicate* an
|
|
260
282
|
on-screen control -- never be the only way to play.
|
|
261
283
|
|
|
262
|
-
- **`Draggable`** — grab the actor with the pointer and fling it (
|
|
263
|
-
|
|
284
|
+
- **`Draggable`** — grab the actor with the pointer and fling it (collides).
|
|
285
|
+
`stiffness`.
|
|
286
|
+
- **The touch zone is the `Collider`, at its live orientation** — not the
|
|
287
|
+
Layout box. A circle is round to the touch, and a rotated actor's grabbable
|
|
288
|
+
area rotates with it. An actor whose collider is smaller than its Layout box
|
|
289
|
+
(which `mode: 'auto'` produces, since it shrink-wraps the sprite's opaque
|
|
290
|
+
pixels) is grabbable only where the collider is.
|
|
291
|
+
- **The hold is at the point you touched**, so grabbing off-center turns the
|
|
292
|
+
object as well as moving it — grab a plank by one end and it swings.
|
|
293
|
+
- **A drag belongs to the finger that started it.** Later fingers can't steal
|
|
294
|
+
or drop it, and a second finger can drag a second actor at the same time.
|
|
295
|
+
- `stiffness` is `0` (loose, swingy) to `1` (rigid, snaps to the pointer),
|
|
296
|
+
default `0.3`. It's mapped onto matter's very compressed useful band, so the
|
|
297
|
+
low end is genuinely floppy — reach for `0.1`–`0.3` for a rag-doll feel and
|
|
298
|
+
`0.7`+ for a tight one.
|
|
299
|
+
|
|
300
|
+
**All three controls are per-finger**, and each claims the pointer it started on,
|
|
301
|
+
so they coexist: a player can work an `AnalogStick` with one thumb while dragging
|
|
302
|
+
with the other, and two `Draggable` actors can be dragged at once. A press that
|
|
303
|
+
lands on a control's own collider outranks one that merely takes any press, so a
|
|
304
|
+
`Slingshot` set to `grabAnywhere` never steals a finger that grabbed a
|
|
305
|
+
`Draggable`. Custom controls get the same treatment via `scene.claimPointer` —
|
|
306
|
+
see the SceneRuntime API below.
|
|
264
307
|
- **`Slingshot`** — press the actor, pull back, release to launch it the
|
|
265
308
|
opposite way (angry-birds), with an aim arrow. `speed`, `maxDrag`.
|
|
266
309
|
- **`AnalogStick`** — on-screen virtual joystick (press anywhere) that drives
|
|
@@ -366,6 +409,12 @@ draw(actor, scene, ctx) {
|
|
|
366
409
|
directly is only for `static`/`kinematic` bodies.
|
|
367
410
|
- **Editor vs play.** Physics only steps during play; in the editor actors sit
|
|
368
411
|
where you place them. `RigidBody.velocityX/Y` apply once when play starts.
|
|
412
|
+
- **No `Collider` means no physics at all.** A body only exists for an actor with
|
|
413
|
+
a Collider, so `RigidBody`, `Draggable`, `Slingshot`, `AnalogStick`, `Joints`
|
|
414
|
+
and collision callbacks are all silently inert without one — nothing errors,
|
|
415
|
+
the actor just sits there. The editor's add-behavior button pulls the missing
|
|
416
|
+
pieces in, but a blueprint you write yourself gets no such help: if it has any
|
|
417
|
+
of those, give it a `Collider` too.
|
|
369
418
|
- **A joint binds only when both actors have a body.** If a `Joints` entry's
|
|
370
419
|
owner or `target` has no `Collider` (or the target id is wrong/missing), that
|
|
371
420
|
link is silently skipped until both bodies exist. `weld`/`pin` want the two
|
|
@@ -431,7 +480,9 @@ To generate path art, emit an svg of **shapes** rather than per-pixel rects and
|
|
|
431
480
|
|
|
432
481
|
- `scene.time` — seconds since start.
|
|
433
482
|
- `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
|
|
434
|
-
- `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted.
|
|
483
|
+
- `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted. The **primary** pointer: the one held longest. Use this for anything single-touch.
|
|
484
|
+
- `scene.pointers` — `Map` of every active pointer (finger / mouse / stylus) by id, each `{ id, x, y, down, justPressed, claimedBy }`, same coordinates. For multi-touch: latch the `id` a gesture began on and follow that entry, so another finger landing can't hijack it. `justPressed` is that pointer's press edge for this frame (a shared `down` flag would miss a finger landing while another is already held). Entries disappear on release.
|
|
485
|
+
- `scene.claimPointer(id, ownerId, priority?)` / `scene.ownsPointer(id, ownerId)` — take a finger for one gesture so two controls can't both act on it, and check you still hold it. A higher `priority` takes it from a lower one, which is how a control that was actually pressed (a `Draggable`'s collider) beats one that takes any press (an `AnalogStick`), regardless of actor order. Claims vanish with the pointer, so there's nothing to release. This is what lets a player work a stick with one thumb and drag with the other.
|
|
435
486
|
- `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
|
|
436
487
|
- `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
|
|
437
488
|
- `scene.colliderRect(actorOrId)` — rect from Layout + Collider, or null.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Panel, SelectField } from '../engine/ui';
|
|
3
3
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
GREEDY_CLAIM,
|
|
6
|
+
acquirePress,
|
|
7
|
+
analogVelocity,
|
|
8
|
+
inActorWorldSpace,
|
|
9
|
+
stickVector,
|
|
10
|
+
} from '../engine/physics/controls';
|
|
5
11
|
|
|
6
12
|
// AnalogStick: an on-screen virtual joystick that drives the actor it's on --
|
|
7
13
|
// the look and feel of castle-client's Analog Stick. Press anywhere to place
|
|
@@ -13,6 +19,8 @@ import { analogVelocity, inActorWorldSpace, pressOnDraggable, stickVector } from
|
|
|
13
19
|
// axes -> restrict to 'horizontal' / 'vertical' / 'both'.
|
|
14
20
|
export class AnalogStick {
|
|
15
21
|
static behaviorName = 'AnalogStick';
|
|
22
|
+
// Drives a body, so it needs one: Collider + dynamic RigidBody.
|
|
23
|
+
static expectedSiblings = ['Collider', 'RigidBody'];
|
|
16
24
|
|
|
17
25
|
static defaultProps = {
|
|
18
26
|
speed: 6,
|
|
@@ -20,27 +28,49 @@ export class AnalogStick {
|
|
|
20
28
|
axes: 'both',
|
|
21
29
|
};
|
|
22
30
|
|
|
31
|
+
// `maxRadius` is the stick's travel in px; below ~10 the knob has no usable
|
|
32
|
+
// throw and the control reads as broken.
|
|
33
|
+
static propertyMeta = {
|
|
34
|
+
speed: { min: 0, step: 0.5 },
|
|
35
|
+
maxRadius: { min: 10, step: 5 },
|
|
36
|
+
};
|
|
37
|
+
|
|
23
38
|
constructor(props) {
|
|
24
39
|
this.props = props;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// The finger this stick is following, placing it on a fresh press if it isn't
|
|
43
|
+
// following one yet. The stick stays with the finger that placed it, so a
|
|
44
|
+
// second finger elsewhere (another stick, a drag) never yanks it -- and it
|
|
45
|
+
// gives the pointer up if a targeted control outranks its claim.
|
|
46
|
+
trackedPointer(actor, scene) {
|
|
29
47
|
const rt = actor.runtime;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
if (rt._stickPointerId != null && scene.ownsPointer(rt._stickPointerId, actor.id)) {
|
|
49
|
+
const held = scene.pointers.get(rt._stickPointerId);
|
|
50
|
+
if (held?.down) return held;
|
|
51
|
+
}
|
|
52
|
+
rt._stickPointerId = null;
|
|
53
|
+
// Any fresh press will do -- the stick is placed wherever you touch -- but
|
|
54
|
+
// only one nobody more specific wants.
|
|
55
|
+
const press = acquirePress(scene, actor.id, () => GREEDY_CLAIM);
|
|
56
|
+
if (!press) return null;
|
|
57
|
+
rt._stickPointerId = press.id;
|
|
58
|
+
rt._stickOrigin = { x: press.x, y: press.y };
|
|
59
|
+
return press;
|
|
60
|
+
}
|
|
35
61
|
|
|
62
|
+
update(actor, scene) {
|
|
63
|
+
const rt = actor.runtime;
|
|
64
|
+
const p = this.trackedPointer(actor, scene);
|
|
36
65
|
// Only drive velocity while the stick is actually held.
|
|
37
|
-
if (p
|
|
66
|
+
if (p?.down && rt._stickOrigin) {
|
|
38
67
|
rt._stickKnob = stickVector(p, rt._stickOrigin, this.props.maxRadius ?? 60);
|
|
39
68
|
scene.physics.setVelocity(actor, analogVelocity(p, rt._stickOrigin, this.props));
|
|
40
69
|
rt._stickActive = true;
|
|
41
70
|
return;
|
|
42
71
|
}
|
|
43
72
|
|
|
73
|
+
rt._stickPointerId = null;
|
|
44
74
|
rt._stickOrigin = null;
|
|
45
75
|
rt._stickKnob = null;
|
|
46
76
|
// Stop ONCE on release, and only on the driven axes (so a top-down mover
|
|
@@ -92,6 +122,7 @@ export class AnalogStick {
|
|
|
92
122
|
/>
|
|
93
123
|
<AutoFields
|
|
94
124
|
defaultProps={AnalogStick.defaultProps}
|
|
125
|
+
meta={AnalogStick.propertyMeta}
|
|
95
126
|
component={component}
|
|
96
127
|
setComponent={setComponent}
|
|
97
128
|
exclude={['axes']}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { NumberField, Panel, SelectField } from '../engine/ui';
|
|
3
3
|
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
+
import { POSITIVE } from '../engine/propertyRanges';
|
|
4
5
|
import { computeAutoFit, getColliderRect, getColliderShapes, intersects } from '../engine/collider';
|
|
5
6
|
|
|
6
7
|
// --- inspector styles ------------------------------------------------------
|
|
@@ -99,6 +100,16 @@ export class Collider {
|
|
|
99
100
|
density: 0.001,
|
|
100
101
|
};
|
|
101
102
|
|
|
103
|
+
// Inspector ranges. No `max` on any of these: matter treats them as open-ended
|
|
104
|
+
// (bounciness "can exceed 1" per the docs, and stiction above 1 is meaningful),
|
|
105
|
+
// and a max is a hard authoring ceiling here -- typed entry is clamped to it,
|
|
106
|
+
// not just the slider. `min: 0` and a fine step are the parts that are safe.
|
|
107
|
+
static propertyMeta = {
|
|
108
|
+
bounciness: POSITIVE,
|
|
109
|
+
friction: POSITIVE,
|
|
110
|
+
frictionStatic: POSITIVE,
|
|
111
|
+
};
|
|
112
|
+
|
|
102
113
|
constructor(props) {
|
|
103
114
|
this.props = props;
|
|
104
115
|
}
|
|
@@ -276,6 +287,7 @@ export class Collider {
|
|
|
276
287
|
<NumberField label="Density" value={density} onChange={(v) => setComponent({ density: v })} />
|
|
277
288
|
<AutoFields
|
|
278
289
|
defaultProps={Collider.defaultProps}
|
|
290
|
+
meta={Collider.propertyMeta}
|
|
279
291
|
component={component}
|
|
280
292
|
setComponent={setComponent}
|
|
281
293
|
exclude={['density']}
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Panel } from '../engine/ui';
|
|
3
3
|
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import {
|
|
4
|
+
import { UNIT } from '../engine/propertyRanges';
|
|
5
|
+
import { TARGETED_CLAIM, dragHold, inActorWorldSpace } from '../engine/physics/controls';
|
|
5
6
|
|
|
6
|
-
// Draggable: grab the actor with the pointer and fling it around.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// even if the pointer slips off. Needs a RigidBody (dynamic) + Collider.
|
|
7
|
+
// Draggable: grab the actor with the pointer and fling it around. The grab is a
|
|
8
|
+
// real physics constraint from the exact spot that was touched to the pointer,
|
|
9
|
+
// so the object still collides with the world AND turns when held off-center --
|
|
10
|
+
// grab a plank by its end and it swings.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// The touch zone is the actor's COLLIDER at its current orientation (via
|
|
13
|
+
// scene.physics.actorAtPoint), not its Layout box, so a circle is round and a
|
|
14
|
+
// rotated actor's grabbable area rotates with it.
|
|
15
|
+
//
|
|
16
|
+
// A drag belongs to the finger that started it: it follows that pointer alone
|
|
17
|
+
// until that pointer lifts, so later fingers can't steal or drop it, and a
|
|
18
|
+
// second finger can drag a second actor at the same time.
|
|
19
|
+
//
|
|
20
|
+
// stiffness -> how tightly the object is held, 0 (loose and swingy) to 1
|
|
21
|
+
// (rigid, snaps to the pointer). Mapped through `dragHold`, which spreads the
|
|
22
|
+
// authored range over matter's very compressed useful band and eases damping
|
|
23
|
+
// along with it, so the low end is genuinely floppy rather than just slow.
|
|
13
24
|
export class Draggable {
|
|
14
25
|
static behaviorName = 'Draggable';
|
|
26
|
+
// Inert without a body to hold: needs a Collider (to have one at all) and a
|
|
27
|
+
// dynamic RigidBody (so the grab can actually move it).
|
|
28
|
+
static expectedSiblings = ['Collider', 'RigidBody'];
|
|
15
29
|
|
|
16
30
|
static defaultProps = {
|
|
17
|
-
stiffness: 0.
|
|
31
|
+
stiffness: 0.3,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 0..1 with a fine step, so the inspector renders a real slider over the
|
|
35
|
+
// whole range rather than an unbounded whole-number scrubber.
|
|
36
|
+
static propertyMeta = {
|
|
37
|
+
stiffness: UNIT,
|
|
18
38
|
};
|
|
19
39
|
|
|
20
40
|
constructor(props) {
|
|
@@ -22,26 +42,49 @@ export class Draggable {
|
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
update(actor, scene) {
|
|
25
|
-
const p = scene.pointer;
|
|
26
45
|
const rt = actor.runtime;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!
|
|
31
|
-
|
|
46
|
+
const held = rt._dragPointerId == null ? null : scene.pointers.get(rt._dragPointerId);
|
|
47
|
+
|
|
48
|
+
// The finger that started this drag lifted (or its contact was cancelled).
|
|
49
|
+
if (rt._dragPointerId != null && !held?.down) {
|
|
50
|
+
scene.physics.release(actor);
|
|
51
|
+
rt._dragPointerId = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (held) return scene.physics.moveGrab(actor, held.x, held.y);
|
|
32
55
|
|
|
33
|
-
|
|
56
|
+
// Idle: look for a new press on this actor. Each pointer carries its own
|
|
57
|
+
// press edge, so a finger landing while another is already down still
|
|
58
|
+
// registers -- and an actor already being dragged never gets here, so the
|
|
59
|
+
// first finger keeps it.
|
|
60
|
+
for (const p of scene.pointers.values()) {
|
|
61
|
+
if (!p.justPressed) continue;
|
|
62
|
+
if (scene.physics.actorAtPoint(p.x, p.y)?.id !== actor.id) continue;
|
|
63
|
+
// A press on this collider is unambiguous, so it outranks any control that
|
|
64
|
+
// merely takes presses anywhere -- whatever order the actors update in.
|
|
65
|
+
if (!scene.claimPointer(p.id, actor.id, TARGETED_CLAIM)) continue;
|
|
66
|
+
if (scene.physics.grab(actor, p.x, p.y, dragHold(this.props.stiffness))) {
|
|
67
|
+
rt._dragPointerId = p.id;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
34
71
|
}
|
|
35
72
|
|
|
36
|
-
// Feedback while held (castle-client's Drag overlay): a circle
|
|
37
|
-
// object + a circle at the touch, connected by a line
|
|
38
|
-
// with a solid white border.
|
|
39
|
-
//
|
|
73
|
+
// Feedback while held (castle-client's Drag overlay): a circle where the
|
|
74
|
+
// object is actually being held + a circle at the touch, connected by a line
|
|
75
|
+
// -- both translucent white with a solid white border. The handle comes from
|
|
76
|
+
// the constraint the simulation is solving, so it sits exactly where the
|
|
77
|
+
// physics holds the object and tracks it through rotation. Drawn in world
|
|
78
|
+
// space so the overlay itself doesn't spin with the actor.
|
|
40
79
|
draw(actor, scene, ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
80
|
+
const id = actor.runtime._dragPointerId;
|
|
81
|
+
if (id == null) return;
|
|
82
|
+
const handle = scene.physics.grabPoint(actor);
|
|
83
|
+
const touch = scene.pointers.get(id);
|
|
84
|
+
if (!handle || !touch) return;
|
|
85
|
+
inActorWorldSpace(ctx, actor.components.Layout, (g) =>
|
|
86
|
+
drawDrag(g, handle, { x: touch.x, y: touch.y })
|
|
87
|
+
);
|
|
45
88
|
}
|
|
46
89
|
|
|
47
90
|
static Inspector({ component, setComponent, override }) {
|
|
@@ -49,6 +92,7 @@ export class Draggable {
|
|
|
49
92
|
<Panel title="Draggable" overridden={override?.anyOverridden()}>
|
|
50
93
|
<AutoFields
|
|
51
94
|
defaultProps={Draggable.defaultProps}
|
|
95
|
+
meta={Draggable.propertyMeta}
|
|
52
96
|
component={component}
|
|
53
97
|
setComponent={setComponent}
|
|
54
98
|
override={override}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// `isTrigger: true`. Put it on an actor with a Layout and a trigger Collider.
|
|
4
4
|
export class Goal {
|
|
5
5
|
static behaviorName = 'Goal';
|
|
6
|
+
// Scores on collision callbacks, which only fire for an actor with a body.
|
|
7
|
+
static expectedSiblings = ['Collider'];
|
|
6
8
|
|
|
7
9
|
static defaultProps = {};
|
|
8
10
|
|
|
@@ -24,6 +24,11 @@ const ANCHOR_MAX = 120;
|
|
|
24
24
|
// point every instance at the same actor.
|
|
25
25
|
export class Joints {
|
|
26
26
|
static behaviorName = 'Joints';
|
|
27
|
+
// A link binds two bodies, so the OWNER needs a Collider to have one. Only the
|
|
28
|
+
// owner: the `target` actor needs one too, but it isn't a sibling -- it's a
|
|
29
|
+
// separate actor the author picks later, and inventing a Collider on it isn't
|
|
30
|
+
// this hook's business.
|
|
31
|
+
static expectedSiblings = ['Collider'];
|
|
27
32
|
|
|
28
33
|
static defaultProps = { list: [] };
|
|
29
34
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Panel, SelectField } from '../engine/ui';
|
|
3
3
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
4
|
+
import { DAMPING } from '../engine/propertyRanges';
|
|
4
5
|
|
|
5
6
|
// RigidBody makes an actor a physics object. Pair it with a Collider (which
|
|
6
7
|
// gives the shape + material). The simulation reads these props each frame; the
|
|
@@ -17,6 +18,9 @@ import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
|
17
18
|
// velocityX/velocityY -> initial velocity, applied once when play starts.
|
|
18
19
|
export class RigidBody {
|
|
19
20
|
static behaviorName = 'RigidBody';
|
|
21
|
+
// No Collider means no body at all (matterBridge's `isPhysicsActor`), so a
|
|
22
|
+
// RigidBody on its own simulates nothing.
|
|
23
|
+
static expectedSiblings = ['Collider'];
|
|
20
24
|
|
|
21
25
|
static defaultProps = {
|
|
22
26
|
bodyType: 'dynamic',
|
|
@@ -31,6 +35,17 @@ export class RigidBody {
|
|
|
31
35
|
angularVelocity: 0,
|
|
32
36
|
};
|
|
33
37
|
|
|
38
|
+
// `drag`/`angularDrag` are matter's air friction, where values above 1 flip
|
|
39
|
+
// the velocity sign and destabilize the body -- so 1 is a real ceiling, not a
|
|
40
|
+
// convenience. `gravityScale` deliberately has none: negative is legitimate
|
|
41
|
+
// (it inverts gravity), as is scaling well past 1.
|
|
42
|
+
static propertyMeta = {
|
|
43
|
+
gravityScale: { step: 0.1 },
|
|
44
|
+
drag: DAMPING,
|
|
45
|
+
angularDrag: DAMPING,
|
|
46
|
+
angularVelocity: { step: 0.1 },
|
|
47
|
+
};
|
|
48
|
+
|
|
34
49
|
constructor(props) {
|
|
35
50
|
this.props = props;
|
|
36
51
|
}
|
|
@@ -47,6 +62,7 @@ export class RigidBody {
|
|
|
47
62
|
/>
|
|
48
63
|
<AutoFields
|
|
49
64
|
defaultProps={RigidBody.defaultProps}
|
|
65
|
+
meta={RigidBody.propertyMeta}
|
|
50
66
|
component={component}
|
|
51
67
|
setComponent={setComponent}
|
|
52
68
|
exclude={['bodyType']}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Panel } from '../engine/ui';
|
|
3
3
|
import { AutoFields } from '../engine/autoInspector';
|
|
4
|
-
import {
|
|
4
|
+
import { PIXELS } from '../engine/propertyRanges';
|
|
5
|
+
import {
|
|
6
|
+
GREEDY_CLAIM,
|
|
7
|
+
TARGETED_CLAIM,
|
|
8
|
+
acquirePress,
|
|
9
|
+
inActorWorldSpace,
|
|
10
|
+
slingLaunch,
|
|
11
|
+
slingPull,
|
|
12
|
+
} from '../engine/physics/controls';
|
|
5
13
|
|
|
6
14
|
// Slingshot: pull back and release to fling the actor the opposite way
|
|
7
15
|
// (angry-birds style) -- the physics feel of castle-client's Sling. While
|
|
@@ -14,6 +22,8 @@ import { inActorWorldSpace, pressOnDraggable, slingLaunch, slingPull } from '../
|
|
|
14
22
|
// like castle-client); set false to require pressing the actor.
|
|
15
23
|
export class Slingshot {
|
|
16
24
|
static behaviorName = 'Slingshot';
|
|
25
|
+
// Launches a body, so it needs one: Collider + dynamic RigidBody.
|
|
26
|
+
static expectedSiblings = ['Collider', 'RigidBody'];
|
|
17
27
|
|
|
18
28
|
static defaultProps = {
|
|
19
29
|
speed: 0.08,
|
|
@@ -21,31 +31,51 @@ export class Slingshot {
|
|
|
21
31
|
grabAnywhere: true,
|
|
22
32
|
};
|
|
23
33
|
|
|
34
|
+
// `speed` is a small multiplier (0.08 default), so whole-number steps were
|
|
35
|
+
// useless here. `maxDrag` is pixels of pull.
|
|
36
|
+
static propertyMeta = {
|
|
37
|
+
speed: { min: 0, step: 0.01 },
|
|
38
|
+
maxDrag: PIXELS,
|
|
39
|
+
};
|
|
40
|
+
|
|
24
41
|
constructor(props) {
|
|
25
42
|
this.props = props;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
update(actor, scene) {
|
|
29
|
-
const p = scene.pointer;
|
|
30
46
|
const rt = actor.runtime;
|
|
31
|
-
const pressed = p.down && !rt._slingWasDown;
|
|
32
|
-
rt._slingWasDown = p.down;
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if (!rt._slinging) {
|
|
49
|
+
const grabAnywhere = this.props.grabAnywhere ?? true;
|
|
50
|
+
const press = acquirePress(scene, actor.id, (p) => {
|
|
51
|
+
// Collider-accurate, at the body's live orientation -- the same pick
|
|
52
|
+
// Draggable uses, so the two agree on what "on this actor" means.
|
|
53
|
+
const onSelf = scene.physics.actorAtPoint(p.x, p.y)?.id === actor.id;
|
|
54
|
+
if (!grabAnywhere && !onSelf) return null;
|
|
55
|
+
// Pressing the body itself is an unambiguous aim; taking any press
|
|
56
|
+
// anywhere is not, so it yields to a control that was actually targeted.
|
|
57
|
+
return onSelf ? TARGETED_CLAIM : GREEDY_CLAIM;
|
|
58
|
+
});
|
|
59
|
+
if (press) {
|
|
60
|
+
rt._slingPointerId = press.id;
|
|
61
|
+
rt._slinging = true;
|
|
62
|
+
// Anchor at the press point (like castle-client): the aim/launch depends
|
|
63
|
+
// on the drag gesture, not where the press landed relative to the actor.
|
|
64
|
+
rt._slingAnchor = { x: press.x, y: press.y };
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
if (!rt._slinging) return;
|
|
48
68
|
|
|
69
|
+
const p = scene.pointers.get(rt._slingPointerId);
|
|
70
|
+
// Outranked mid-aim, or the contact was cancelled: drop the shot rather than
|
|
71
|
+
// launching from an anchor the player has stopped steering.
|
|
72
|
+
if (!p || !scene.ownsPointer(rt._slingPointerId, actor.id)) {
|
|
73
|
+
rt._slinging = false;
|
|
74
|
+
rt._slingPointerId = null;
|
|
75
|
+
rt._slingAim = null;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
if (p.down) {
|
|
50
80
|
// Hold still while aiming, and remember the aim point for draw().
|
|
51
81
|
scene.physics.setVelocity(actor, { x: 0, y: 0 });
|
|
@@ -53,6 +83,7 @@ export class Slingshot {
|
|
|
53
83
|
} else {
|
|
54
84
|
scene.physics.setVelocity(actor, slingLaunch(rt._slingAnchor, p, this.props));
|
|
55
85
|
rt._slinging = false;
|
|
86
|
+
rt._slingPointerId = null;
|
|
56
87
|
rt._slingAim = null;
|
|
57
88
|
}
|
|
58
89
|
}
|
|
@@ -79,6 +110,7 @@ export class Slingshot {
|
|
|
79
110
|
<Panel title="Slingshot" overridden={override?.anyOverridden()}>
|
|
80
111
|
<AutoFields
|
|
81
112
|
defaultProps={Slingshot.defaultProps}
|
|
113
|
+
meta={Slingshot.propertyMeta}
|
|
82
114
|
component={component}
|
|
83
115
|
setComponent={setComponent}
|
|
84
116
|
override={override}
|
|
@@ -4,6 +4,7 @@ import { mediaElement, playMedia, playOneShot, setMediaPan, stopMedia } from '..
|
|
|
4
4
|
import { mediaFilesOfKind } from '../engine/files';
|
|
5
5
|
import { Panel, SelectField } from '../engine/ui';
|
|
6
6
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
7
|
+
import { PAN, UNIT } from '../engine/propertyRanges';
|
|
7
8
|
|
|
8
9
|
// Plays a sound file (.mp3 / .wav / .m4a) from an actor.
|
|
9
10
|
//
|
|
@@ -39,6 +40,15 @@ export class Sound {
|
|
|
39
40
|
playOnStart: true,
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
// Mirrors the clamps `readSettings` already applies at runtime, so the
|
|
44
|
+
// inspector can't author a value the sound would silently ignore -- including
|
|
45
|
+
// playbackRate's 0.01..10, where the classic editor's ceiling lives.
|
|
46
|
+
static propertyMeta = {
|
|
47
|
+
volume: UNIT,
|
|
48
|
+
playbackRate: { min: 0.01, max: 10, step: 0.05 },
|
|
49
|
+
pan: PAN,
|
|
50
|
+
};
|
|
51
|
+
|
|
42
52
|
constructor(props) {
|
|
43
53
|
this.props = props;
|
|
44
54
|
}
|
|
@@ -80,6 +90,7 @@ export class Sound {
|
|
|
80
90
|
/>
|
|
81
91
|
<AutoFields
|
|
82
92
|
defaultProps={Sound.defaultProps}
|
|
93
|
+
meta={Sound.propertyMeta}
|
|
83
94
|
component={component}
|
|
84
95
|
setComponent={setComponent}
|
|
85
96
|
only={['volume', 'playbackRate', 'pan', 'loop', 'polyphonic', 'playOnStart']}
|
|
@@ -38,6 +38,12 @@ export class Sprite {
|
|
|
38
38
|
tileSize: 50,
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// Tile cell size in card units -- a floor of 1 (0 would divide by zero when
|
|
42
|
+
// laying out the repeat), no ceiling since a deck may tile larger than a card.
|
|
43
|
+
static propertyMeta = {
|
|
44
|
+
tileSize: { min: 1, step: 1 },
|
|
45
|
+
};
|
|
46
|
+
|
|
41
47
|
constructor(props) {
|
|
42
48
|
this.props = props;
|
|
43
49
|
}
|
|
@@ -173,6 +179,7 @@ export class Sprite {
|
|
|
173
179
|
/>
|
|
174
180
|
<AutoFields
|
|
175
181
|
defaultProps={Sprite.defaultProps}
|
|
182
|
+
meta={Sprite.propertyMeta}
|
|
176
183
|
component={component}
|
|
177
184
|
setComponent={setComponent}
|
|
178
185
|
only={['playing', 'tint', 'tileSize']}
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { playTone, WAVEFORMS } from '../engine/tone';
|
|
3
3
|
import { Panel, SelectField } from '../engine/ui';
|
|
4
4
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
5
|
+
import { PAN, POSITIVE, UNIT } from '../engine/propertyRanges';
|
|
5
6
|
|
|
6
7
|
// Plays a synthesized note -- no audio file involved. This is the cheapest sound
|
|
7
8
|
// a deck can make: a blip on a pickup, a thud on a miss, a rising scale as a
|
|
@@ -27,6 +28,16 @@ export class Tone {
|
|
|
27
28
|
playOnStart: false,
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
// `note` is MIDI, which is genuinely 0..127. `attack`/`release` are seconds,
|
|
32
|
+
// so they get a floor but no ceiling.
|
|
33
|
+
static propertyMeta = {
|
|
34
|
+
note: { min: 0, max: 127, step: 1 },
|
|
35
|
+
attack: POSITIVE,
|
|
36
|
+
release: POSITIVE,
|
|
37
|
+
volume: UNIT,
|
|
38
|
+
pan: PAN,
|
|
39
|
+
};
|
|
40
|
+
|
|
30
41
|
constructor(props) {
|
|
31
42
|
this.props = props;
|
|
32
43
|
}
|
|
@@ -54,6 +65,7 @@ export class Tone {
|
|
|
54
65
|
/>
|
|
55
66
|
<AutoFields
|
|
56
67
|
defaultProps={Tone.defaultProps}
|
|
68
|
+
meta={Tone.propertyMeta}
|
|
57
69
|
component={component}
|
|
58
70
|
setComponent={setComponent}
|
|
59
71
|
only={['note', 'attack', 'release', 'volume', 'pan', 'playOnStart']}
|
|
@@ -5,6 +5,7 @@ import { mediaFilesOfKind } from '../engine/files';
|
|
|
5
5
|
import { spriteDestRect } from '../engine/spriteGeometry';
|
|
6
6
|
import { Panel, SelectField } from '../engine/ui';
|
|
7
7
|
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
8
|
+
import { UNIT } from '../engine/propertyRanges';
|
|
8
9
|
|
|
9
10
|
// Plays a video file (.mp4 / .webm / .mov / .m4v) inside the actor's Layout box.
|
|
10
11
|
//
|
|
@@ -29,6 +30,10 @@ export class Video {
|
|
|
29
30
|
volume: 1,
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
static propertyMeta = {
|
|
34
|
+
volume: UNIT,
|
|
35
|
+
};
|
|
36
|
+
|
|
32
37
|
constructor(props) {
|
|
33
38
|
this.props = props;
|
|
34
39
|
}
|
|
@@ -100,6 +105,7 @@ export class Video {
|
|
|
100
105
|
/>
|
|
101
106
|
<AutoFields
|
|
102
107
|
defaultProps={Video.defaultProps}
|
|
108
|
+
meta={Video.propertyMeta}
|
|
103
109
|
component={component}
|
|
104
110
|
setComponent={setComponent}
|
|
105
111
|
only={['playing', 'loop', 'muted', 'volume']}
|