castle-web-cli 0.4.83 → 0.4.85
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-failures.d.ts +17 -0
- package/dist/agent-failures.js +151 -0
- package/dist/agent.d.ts +26 -0
- package/dist/agent.js +317 -74
- package/dist/ide.js +35 -13
- package/dist/native/loop.js +35 -0
- package/dist/native/openrouter.d.ts +7 -0
- package/dist/native/openrouter.js +25 -1
- package/dist/native/types.d.ts +2 -0
- package/dist/native/types.js +0 -38
- package/dist/openrouter-catalog.d.ts +28 -0
- package/dist/openrouter-catalog.js +299 -0
- package/dist/shell/assets/{index-C9Zhmien.js → index-BJLaUTJE.js} +60 -58
- package/dist/shell/assets/{index-BMkQt27u.css → index-DonnH--m.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/.prettierrc +8 -0
- package/kits/physics-2d/CLAUDE.md +329 -0
- package/kits/physics-2d/behaviors/Camera.jsx +43 -0
- package/kits/physics-2d/behaviors/Collider.jsx +199 -0
- package/kits/physics-2d/behaviors/Goal.jsx +29 -0
- package/kits/physics-2d/behaviors/Layout.jsx +53 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +352 -0
- package/kits/physics-2d/behaviors/tint.js +47 -0
- package/kits/physics-2d/blueprints/ball.scene +14 -0
- package/kits/physics-2d/blueprints/block.scene +12 -0
- package/kits/physics-2d/blueprints/cauldron.scene +18 -0
- package/kits/physics-2d/blueprints/crate.scene +14 -0
- package/kits/physics-2d/blueprints/goal.scene +12 -0
- package/kits/physics-2d/castle.json +13 -0
- package/kits/physics-2d/docs/pxart-format.md +377 -0
- package/kits/physics-2d/drawings/block.pxart +25 -0
- package/kits/physics-2d/drawings/cauldron.pxart +113 -0
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +247 -0
- package/kits/physics-2d/editors/ErrorBoundary.jsx +59 -0
- package/kits/physics-2d/editors/PlayOnly.jsx +31 -0
- package/kits/physics-2d/editors/PxArtEditor.jsx +954 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +1681 -0
- package/kits/physics-2d/editors/SelectionOverlay.jsx +909 -0
- package/kits/physics-2d/editors/SingleEditor.jsx +122 -0
- package/kits/physics-2d/editors/behaviorRegistry.js +30 -0
- package/kits/physics-2d/editors/editorHistory.js +157 -0
- package/kits/physics-2d/editors/inspectorSheet.js +13 -0
- package/kits/physics-2d/editors/pixelCanvas.js +11 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +74 -0
- package/kits/physics-2d/editors/pixelGeometry.js +140 -0
- package/kits/physics-2d/editors/pixelInspector.jsx +633 -0
- package/kits/physics-2d/editors/pxArtEditorModel.js +732 -0
- package/kits/physics-2d/editors/pxArtPlayback.js +92 -0
- package/kits/physics-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/physics-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/physics-2d/editors/pxArtTools.js +232 -0
- package/kits/physics-2d/editors/useArtboardFit.js +102 -0
- package/kits/physics-2d/engine/ScenePlayer.jsx +196 -0
- package/kits/physics-2d/engine/SceneUI.jsx +59 -0
- package/kits/physics-2d/engine/assets.js +15 -0
- package/kits/physics-2d/engine/autoInspector.jsx +70 -0
- package/kits/physics-2d/engine/blueprint.js +521 -0
- package/kits/physics-2d/engine/collider.js +196 -0
- package/kits/physics-2d/engine/files.js +117 -0
- package/kits/physics-2d/engine/liveReload.js +88 -0
- package/kits/physics-2d/engine/pxart.js +1032 -0
- package/kits/physics-2d/engine/pxartSmooth.js +222 -0
- package/kits/physics-2d/engine/scene.js +686 -0
- package/kits/physics-2d/engine/spriteGeometry.js +32 -0
- package/kits/physics-2d/engine/ui.jsx +688 -0
- package/kits/physics-2d/engine/ui.module.css +2287 -0
- package/kits/physics-2d/eslint.config.js +71 -0
- package/kits/physics-2d/index.html +24 -0
- package/kits/physics-2d/main.jsx +24 -0
- package/kits/physics-2d/package-lock.json +2706 -0
- package/kits/physics-2d/package.json +42 -0
- package/kits/physics-2d/physics/PhysicsSystem.js +290 -0
- package/kits/physics-2d/physics/behaviors/AnalogStick.jsx +101 -0
- package/kits/physics-2d/physics/behaviors/Draggable.jsx +79 -0
- package/kits/physics-2d/physics/behaviors/RigidBody.jsx +55 -0
- package/kits/physics-2d/physics/behaviors/Slingshot.jsx +118 -0
- package/kits/physics-2d/physics/controls.js +79 -0
- package/kits/physics-2d/physics/index.js +26 -0
- package/kits/physics-2d/physics/matterBridge.js +126 -0
- package/kits/physics-2d/pnpm-lock.yaml +1761 -0
- package/kits/physics-2d/scenes/main.scene +12 -0
- package/kits/physics-2d/scenes/sandbox.scene +13 -0
- package/kits/physics-2d/scripts/draw.mjs +121 -0
- package/kits/physics-2d/vite.config.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "physics-2d",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"draw": "node scripts/draw.mjs",
|
|
7
|
+
"restart": "node ../../cli/dist/index.js restart .",
|
|
8
|
+
"screenshot": "node ../../cli/dist/index.js screenshot .",
|
|
9
|
+
"check": "eslint . && jscpd && node --input-type=module -e \"const { bundleProject } = await import('../../cli/dist/bundle.js'); await bundleProject('.');\""
|
|
10
|
+
},
|
|
11
|
+
"jscpd": {
|
|
12
|
+
"path": [
|
|
13
|
+
"engine",
|
|
14
|
+
"editors",
|
|
15
|
+
"behaviors"
|
|
16
|
+
],
|
|
17
|
+
"threshold": 0,
|
|
18
|
+
"reporters": [
|
|
19
|
+
"consoleFull"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@codemirror/commands": "^6.10.3",
|
|
24
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
25
|
+
"@codemirror/language": "^6.12.3",
|
|
26
|
+
"@codemirror/state": "^6.6.0",
|
|
27
|
+
"@codemirror/view": "^6.41.1",
|
|
28
|
+
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
|
29
|
+
"@lezer/highlight": "^1.2.3",
|
|
30
|
+
"castle-web-sdk": "file:../../sdk",
|
|
31
|
+
"codemirror": "^6.0.2",
|
|
32
|
+
"matter-js": "^0.20.0",
|
|
33
|
+
"react": "^19.2.4",
|
|
34
|
+
"react-dom": "^19.2.4"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"eslint": "^9.0.0",
|
|
38
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
39
|
+
"jscpd": "^4.0.5",
|
|
40
|
+
"prettier": "^3.8.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// The physics system: one matter-js engine per scene runtime. Registered as a
|
|
2
|
+
// runtime system (see engine/scene.js `registerSystem`), it runs once per frame
|
|
3
|
+
// in `afterBehaviors` -- after behaviors have expressed intent (velocities,
|
|
4
|
+
// forces) -- to reconcile bodies, step the simulation on a fixed timestep, and
|
|
5
|
+
// write results back into each dynamic actor's Layout.
|
|
6
|
+
//
|
|
7
|
+
// The matter engine is created lazily on first step, so draw-only editor
|
|
8
|
+
// previews (which never call update) pay nothing.
|
|
9
|
+
|
|
10
|
+
import Matter from 'matter-js';
|
|
11
|
+
import { getColliderRect } from '../engine/collider';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_GRAVITY,
|
|
14
|
+
FIXED_STEP_MS,
|
|
15
|
+
FIXED_STEP_S,
|
|
16
|
+
MAX_STEPS_PER_FRAME,
|
|
17
|
+
RAD_TO_DEG,
|
|
18
|
+
applyAngularDrag,
|
|
19
|
+
applyWorldGravity,
|
|
20
|
+
bodySignature,
|
|
21
|
+
createBody,
|
|
22
|
+
isPhysicsActor,
|
|
23
|
+
patchBody,
|
|
24
|
+
rectCenter,
|
|
25
|
+
syncBodyToPoint,
|
|
26
|
+
} from './matterBridge';
|
|
27
|
+
|
|
28
|
+
export class PhysicsSystem {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.engine = null;
|
|
31
|
+
this.bodies = new Map(); // actorId -> { body, sig, offset: {dx, dy} }
|
|
32
|
+
this.intents = new Map(); // actorId -> { velocity?, impulse?, force? }
|
|
33
|
+
this.gravity = { ...DEFAULT_GRAVITY };
|
|
34
|
+
this.acc = 0;
|
|
35
|
+
this.enterPairs = []; // [aId, bId] collected during this frame's steps
|
|
36
|
+
this.exitPairs = [];
|
|
37
|
+
this.active = new Set(); // "aId|bId" (sorted) currently-touching pairs
|
|
38
|
+
this.ndActive = new Set(); // non-dynamic overlaps from last frame (for diff)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Expose the author-facing API on the runtime so behaviors can call
|
|
42
|
+
// `scene.physics.setVelocity(actor, v)` from their `update`.
|
|
43
|
+
attach(runtime) {
|
|
44
|
+
runtime.physics = this.makeApi();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ensureEngine() {
|
|
48
|
+
if (this.engine) return;
|
|
49
|
+
this.engine = Matter.Engine.create();
|
|
50
|
+
// We apply gravity ourselves (per-body gravityScale), so silence matter's.
|
|
51
|
+
this.engine.gravity.scale = 0;
|
|
52
|
+
// Sensors (isTrigger colliders) still emit these, so triggers work too.
|
|
53
|
+
Matter.Events.on(this.engine, 'collisionStart', (e) => this.collectPairs(e, this.enterPairs));
|
|
54
|
+
Matter.Events.on(this.engine, 'collisionEnd', (e) => this.collectPairs(e, this.exitPairs));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
collectPairs(event, sink) {
|
|
58
|
+
for (const pair of event.pairs) {
|
|
59
|
+
const a = pair.bodyA.plugin.actorId;
|
|
60
|
+
const b = pair.bodyB.plugin.actorId;
|
|
61
|
+
if (a && b) sink.push([a, b]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
afterBehaviors(scene, dt) {
|
|
66
|
+
this.ensureEngine();
|
|
67
|
+
this.readGravity(scene);
|
|
68
|
+
this.reconcile(scene);
|
|
69
|
+
this.applyIntents();
|
|
70
|
+
this.syncStaticBodies(scene);
|
|
71
|
+
this.step(dt);
|
|
72
|
+
this.writeBack(scene);
|
|
73
|
+
this.detectNonDynamicContacts();
|
|
74
|
+
this.dispatchCollisions(scene);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
readGravity(scene) {
|
|
78
|
+
const p = scene.data?.physics ?? {};
|
|
79
|
+
this.gravity.x = p.gravityX ?? 0;
|
|
80
|
+
this.gravity.y = p.gravity ?? DEFAULT_GRAVITY.y;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
rectOf(scene, actor) {
|
|
84
|
+
return getColliderRect(actor, scene.sprites);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create/rebuild/remove bodies to match the current physics actors.
|
|
88
|
+
reconcile(scene) {
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
for (const actor of scene.getActors()) {
|
|
91
|
+
if (!isPhysicsActor(actor)) continue;
|
|
92
|
+
const rect = this.rectOf(scene, actor);
|
|
93
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
94
|
+
seen.add(actor.id);
|
|
95
|
+
const entry = this.bodies.get(actor.id);
|
|
96
|
+
const sig = bodySignature(actor, rect);
|
|
97
|
+
if (!entry) this.addBody(actor, rect);
|
|
98
|
+
else if (entry.sig !== sig) {
|
|
99
|
+
this.removeBody(actor.id);
|
|
100
|
+
this.addBody(actor, rect);
|
|
101
|
+
} else patchBody(entry.body, actor);
|
|
102
|
+
}
|
|
103
|
+
for (const id of [...this.bodies.keys()]) if (!seen.has(id)) this.removeBody(id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addBody(actor, rect) {
|
|
107
|
+
const body = createBody(actor, rect);
|
|
108
|
+
const center = rectCenter(rect);
|
|
109
|
+
const layout = actor.components.Layout;
|
|
110
|
+
const offset = { dx: center.x - layout.x, dy: center.y - layout.y };
|
|
111
|
+
this.bodies.set(actor.id, { body, sig: bodySignature(actor, rect), offset });
|
|
112
|
+
Matter.Composite.add(this.engine.world, body);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
removeBody(id) {
|
|
116
|
+
const entry = this.bodies.get(id);
|
|
117
|
+
if (!entry) return;
|
|
118
|
+
Matter.Composite.remove(this.engine.world, entry.body);
|
|
119
|
+
this.bodies.delete(id);
|
|
120
|
+
// Drop live contacts involving this actor so isColliding doesn't go stale
|
|
121
|
+
// (matter doesn't always emit collisionEnd when a body is removed).
|
|
122
|
+
for (const set of [this.active, this.ndActive]) {
|
|
123
|
+
for (const key of [...set]) {
|
|
124
|
+
const [a, b] = unpairKey(key);
|
|
125
|
+
if (a === id || b === id) set.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
applyIntents() {
|
|
131
|
+
for (const [id, intent] of this.intents) {
|
|
132
|
+
const body = this.bodies.get(id)?.body;
|
|
133
|
+
if (!body) continue;
|
|
134
|
+
if (intent.velocity) Matter.Body.setVelocity(body, intent.velocity);
|
|
135
|
+
if (intent.impulse) {
|
|
136
|
+
Matter.Body.setVelocity(body, {
|
|
137
|
+
x: body.velocity.x + intent.impulse.x,
|
|
138
|
+
y: body.velocity.y + intent.impulse.y,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (intent.force) Matter.Body.applyForce(body, body.position, intent.force);
|
|
142
|
+
}
|
|
143
|
+
this.intents.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Keep static + kinematic bodies wherever Layout says (editor edits, or a
|
|
147
|
+
// behavior moving a platform by writing Layout).
|
|
148
|
+
syncStaticBodies(scene) {
|
|
149
|
+
for (const [id, entry] of this.bodies) {
|
|
150
|
+
if (!entry.body.isStatic) continue;
|
|
151
|
+
const actor = scene.getActor(id);
|
|
152
|
+
if (!actor) continue;
|
|
153
|
+
const layout = actor.components.Layout;
|
|
154
|
+
syncBodyToPoint(entry.body, { x: layout.x + entry.offset.dx, y: layout.y + entry.offset.dy }, layout.rotation);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
step(dt) {
|
|
159
|
+
this.acc += dt;
|
|
160
|
+
const dynamic = this.dynamicBodies();
|
|
161
|
+
let steps = 0;
|
|
162
|
+
while (this.acc >= FIXED_STEP_S && steps < MAX_STEPS_PER_FRAME) {
|
|
163
|
+
applyWorldGravity(dynamic, this.gravity);
|
|
164
|
+
applyAngularDrag(dynamic);
|
|
165
|
+
Matter.Engine.update(this.engine, FIXED_STEP_MS);
|
|
166
|
+
this.acc -= FIXED_STEP_S;
|
|
167
|
+
steps += 1;
|
|
168
|
+
}
|
|
169
|
+
if (steps >= MAX_STEPS_PER_FRAME) this.acc = 0; // drop backlog, avoid spiral
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeBack(scene) {
|
|
173
|
+
for (const [id, entry] of this.bodies) {
|
|
174
|
+
if (entry.body.isStatic) continue;
|
|
175
|
+
const actor = scene.getActor(id);
|
|
176
|
+
if (!actor) continue;
|
|
177
|
+
const layout = actor.components.Layout;
|
|
178
|
+
layout.x = entry.body.position.x - entry.offset.dx;
|
|
179
|
+
layout.y = entry.body.position.y - entry.offset.dy;
|
|
180
|
+
layout.rotation = entry.body.angle * RAD_TO_DEG;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// matter-js only generates contacts when at least one body is dynamic, so it
|
|
185
|
+
// never reports overlaps between two non-dynamic bodies (static-static, or
|
|
186
|
+
// any pair involving an emulated-kinematic body, which is matter-static).
|
|
187
|
+
// Detect those ourselves by narrow-phase testing every non-dynamic pair, and
|
|
188
|
+
// feed enter/exit into the same pipeline as matter's events. This closes the
|
|
189
|
+
// gap so onCollisionEnter/Exit + isColliding fire for moving kinematic bodies
|
|
190
|
+
// (platforms, moving sensors) and static trigger zones too.
|
|
191
|
+
detectNonDynamicContacts() {
|
|
192
|
+
const bodies = [];
|
|
193
|
+
for (const entry of this.bodies.values()) if (entry.body.isStatic) bodies.push(entry.body);
|
|
194
|
+
const current = new Set();
|
|
195
|
+
for (let i = 0; i < bodies.length - 1; i++) {
|
|
196
|
+
const collisions = Matter.Query.collides(bodies[i], bodies.slice(i + 1));
|
|
197
|
+
for (const c of collisions) {
|
|
198
|
+
const ida = c.bodyA.plugin.actorId;
|
|
199
|
+
const idb = c.bodyB.plugin.actorId;
|
|
200
|
+
if (ida && idb && ida !== idb) current.add(pairKey(ida, idb));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const key of current) if (!this.ndActive.has(key)) this.enterPairs.push(unpairKey(key));
|
|
204
|
+
for (const key of this.ndActive) if (!current.has(key)) this.exitPairs.push(unpairKey(key));
|
|
205
|
+
this.ndActive = current;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fire onCollisionEnter/Exit(actor, scene, other) on both actors of each
|
|
209
|
+
// pair, and maintain the live `active` set for isColliding queries.
|
|
210
|
+
dispatchCollisions(scene) {
|
|
211
|
+
for (const [a, b] of this.enterPairs) {
|
|
212
|
+
this.active.add(pairKey(a, b));
|
|
213
|
+
this.fireContact(scene, a, b, 'onCollisionEnter');
|
|
214
|
+
}
|
|
215
|
+
for (const [a, b] of this.exitPairs) {
|
|
216
|
+
this.active.delete(pairKey(a, b));
|
|
217
|
+
this.fireContact(scene, a, b, 'onCollisionExit');
|
|
218
|
+
}
|
|
219
|
+
this.enterPairs.length = 0;
|
|
220
|
+
this.exitPairs.length = 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fireContact(scene, aId, bId, hook) {
|
|
224
|
+
const a = scene.getActor(aId);
|
|
225
|
+
const b = scene.getActor(bId);
|
|
226
|
+
if (!a || !b) return;
|
|
227
|
+
scene.forEachBehavior(a, (inst) => inst[hook]?.(a, scene, b));
|
|
228
|
+
scene.forEachBehavior(b, (inst) => inst[hook]?.(b, scene, a));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
dynamicBodies() {
|
|
232
|
+
const out = [];
|
|
233
|
+
for (const entry of this.bodies.values()) if (!entry.body.isStatic) out.push(entry.body);
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bodyOf(actorOrId) {
|
|
238
|
+
const id = typeof actorOrId === 'string' ? actorOrId : actorOrId?.id;
|
|
239
|
+
return this.bodies.get(id)?.body ?? null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
queue(actorOrId, key, value) {
|
|
243
|
+
const id = typeof actorOrId === 'string' ? actorOrId : actorOrId?.id;
|
|
244
|
+
if (!id) return;
|
|
245
|
+
const intent = this.intents.get(id) ?? {};
|
|
246
|
+
intent[key] = value;
|
|
247
|
+
this.intents.set(id, intent);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
queueAdd(actorOrId, key, value) {
|
|
251
|
+
const id = typeof actorOrId === 'string' ? actorOrId : actorOrId?.id;
|
|
252
|
+
if (!id) return;
|
|
253
|
+
const intent = this.intents.get(id) ?? {};
|
|
254
|
+
const prev = intent[key] ?? { x: 0, y: 0 };
|
|
255
|
+
intent[key] = { x: prev.x + value.x, y: prev.y + value.y };
|
|
256
|
+
this.intents.set(id, intent);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
makeApi() {
|
|
260
|
+
return {
|
|
261
|
+
// Set velocity directly (px per fixed step; ~*60 for px/s).
|
|
262
|
+
setVelocity: (actor, v) => this.queue(actor, 'velocity', v),
|
|
263
|
+
// Add to velocity this frame (velocity-space delta).
|
|
264
|
+
applyImpulse: (actor, v) => this.queueAdd(actor, 'impulse', v),
|
|
265
|
+
// Add a matter-space force to the accumulator (advanced).
|
|
266
|
+
applyForce: (actor, f) => this.queueAdd(actor, 'force', f),
|
|
267
|
+
getVelocity: (actor) => {
|
|
268
|
+
const body = this.bodyOf(actor);
|
|
269
|
+
return body ? { x: body.velocity.x, y: body.velocity.y } : { x: 0, y: 0 };
|
|
270
|
+
},
|
|
271
|
+
// True while two actors are touching (updated by collision events).
|
|
272
|
+
isColliding: (a, b) => {
|
|
273
|
+
const aId = typeof a === 'string' ? a : a?.id;
|
|
274
|
+
const bId = typeof b === 'string' ? b : b?.id;
|
|
275
|
+
return this.active.has(pairKey(aId, bId));
|
|
276
|
+
},
|
|
277
|
+
bodyFor: (actor) => this.bodyOf(actor),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function pairKey(a, b) {
|
|
283
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Inverse of pairKey. Actor ids never contain '|', so split on the first one.
|
|
287
|
+
function unpairKey(key) {
|
|
288
|
+
const i = key.indexOf('|');
|
|
289
|
+
return [key.slice(0, i), key.slice(i + 1)];
|
|
290
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Panel, SelectField } from '../../engine/ui';
|
|
3
|
+
import { AutoFields, overrideProps } from '../../engine/autoInspector';
|
|
4
|
+
import { analogVelocity, inActorWorldSpace, stickVector } from '../controls';
|
|
5
|
+
|
|
6
|
+
// AnalogStick: an on-screen virtual joystick that drives the actor it's on --
|
|
7
|
+
// the look and feel of castle-client's Analog Stick. Press anywhere to place
|
|
8
|
+
// the stick; drag to steer. Draws the stick base ring + knob while held. Best
|
|
9
|
+
// for top-down movement; needs a RigidBody (dynamic) + Collider.
|
|
10
|
+
//
|
|
11
|
+
// speed -> velocity at full deflection (px per fixed step).
|
|
12
|
+
// maxRadius -> stick throw radius in pixels.
|
|
13
|
+
// axes -> restrict to 'horizontal' / 'vertical' / 'both'.
|
|
14
|
+
export class AnalogStick {
|
|
15
|
+
static behaviorName = 'AnalogStick';
|
|
16
|
+
|
|
17
|
+
static defaultProps = {
|
|
18
|
+
speed: 6,
|
|
19
|
+
maxRadius: 60,
|
|
20
|
+
axes: 'both',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
constructor(props) {
|
|
24
|
+
this.props = props;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update(actor, scene) {
|
|
28
|
+
const p = scene.pointer;
|
|
29
|
+
const rt = actor.runtime;
|
|
30
|
+
const pressed = p.down && !rt._stickWasDown;
|
|
31
|
+
rt._stickWasDown = p.down;
|
|
32
|
+
if (pressed) rt._stickOrigin = { x: p.x, y: p.y };
|
|
33
|
+
|
|
34
|
+
// Only drive velocity while the stick is actually held.
|
|
35
|
+
if (p.down && rt._stickOrigin) {
|
|
36
|
+
rt._stickKnob = stickVector(p, rt._stickOrigin, this.props.maxRadius ?? 60);
|
|
37
|
+
scene.physics.setVelocity(actor, analogVelocity(p, rt._stickOrigin, this.props));
|
|
38
|
+
rt._stickActive = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
rt._stickOrigin = null;
|
|
43
|
+
rt._stickKnob = null;
|
|
44
|
+
// Stop ONCE on release, and only on the driven axes (so a top-down mover
|
|
45
|
+
// doesn't drift). Critically, do NOT touch velocity while idle -- doing that
|
|
46
|
+
// every frame zeroes the velocity gravity builds up, so the actor barely
|
|
47
|
+
// falls. An idle AnalogStick must leave physics (gravity, momentum) alone.
|
|
48
|
+
if (rt._stickActive) {
|
|
49
|
+
rt._stickActive = false;
|
|
50
|
+
const axes = this.props.axes ?? 'both';
|
|
51
|
+
const cur = scene.physics.getVelocity(actor);
|
|
52
|
+
scene.physics.setVelocity(actor, {
|
|
53
|
+
x: axes === 'vertical' ? cur.x : 0,
|
|
54
|
+
y: axes === 'horizontal' ? cur.y : 0,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Base ring anchored where the user pressed + knob at the current (clamped)
|
|
60
|
+
// deflection. Drawn in world space so it stays put instead of rotating/sliding
|
|
61
|
+
// with the actor.
|
|
62
|
+
draw(actor, scene, ctx) {
|
|
63
|
+
const rt = actor.runtime;
|
|
64
|
+
if (!rt._stickOrigin) return;
|
|
65
|
+
const o = rt._stickOrigin;
|
|
66
|
+
const k = rt._stickKnob ?? { x: 0, y: 0 };
|
|
67
|
+
const radius = this.props.maxRadius ?? 60;
|
|
68
|
+
inActorWorldSpace(ctx, actor.components.Layout, (c) => {
|
|
69
|
+
c.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
|
70
|
+
c.lineWidth = 3;
|
|
71
|
+
c.beginPath();
|
|
72
|
+
c.arc(o.x, o.y, radius, 0, Math.PI * 2);
|
|
73
|
+
c.stroke();
|
|
74
|
+
c.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
75
|
+
c.beginPath();
|
|
76
|
+
c.arc(o.x + k.x, o.y + k.y, 20, 0, Math.PI * 2);
|
|
77
|
+
c.fill();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static Inspector({ component, setComponent, override }) {
|
|
82
|
+
return (
|
|
83
|
+
<Panel title="AnalogStick" overridden={override?.anyOverridden()}>
|
|
84
|
+
<SelectField
|
|
85
|
+
label="Axes"
|
|
86
|
+
value={component.axes}
|
|
87
|
+
onChange={(axes) => setComponent({ axes })}
|
|
88
|
+
options={['both', 'horizontal', 'vertical']}
|
|
89
|
+
{...overrideProps(override, 'axes')}
|
|
90
|
+
/>
|
|
91
|
+
<AutoFields
|
|
92
|
+
defaultProps={AnalogStick.defaultProps}
|
|
93
|
+
component={component}
|
|
94
|
+
setComponent={setComponent}
|
|
95
|
+
exclude={['axes']}
|
|
96
|
+
override={override}
|
|
97
|
+
/>
|
|
98
|
+
</Panel>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Panel } from '../../engine/ui';
|
|
3
|
+
import { AutoFields } from '../../engine/autoInspector';
|
|
4
|
+
import { centerOf, chaseVelocity, inActorWorldSpace } from '../controls';
|
|
5
|
+
|
|
6
|
+
// Draggable: grab the actor with the pointer and fling it around. Chases the
|
|
7
|
+
// pointer with a springy velocity (via the physics system) so it still collides
|
|
8
|
+
// with the world -- the same feel as castle-client's Drag. Grabs the topmost
|
|
9
|
+
// actor under the press (uses scene.actorAt), and keeps dragging until release
|
|
10
|
+
// even if the pointer slips off. Needs a RigidBody (dynamic) + Collider.
|
|
11
|
+
//
|
|
12
|
+
// stiffness -> how hard it chases the pointer (fraction of the gap per step).
|
|
13
|
+
export class Draggable {
|
|
14
|
+
static behaviorName = 'Draggable';
|
|
15
|
+
|
|
16
|
+
static defaultProps = {
|
|
17
|
+
stiffness: 0.1,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
constructor(props) {
|
|
21
|
+
this.props = props;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
update(actor, scene) {
|
|
25
|
+
const p = scene.pointer;
|
|
26
|
+
const rt = actor.runtime;
|
|
27
|
+
const pressed = p.down && !rt._dragWasDown;
|
|
28
|
+
rt._dragWasDown = p.down;
|
|
29
|
+
if (pressed && scene.actorAt(p.x, p.y)?.id === actor.id) rt._dragging = true;
|
|
30
|
+
if (!p.down) rt._dragging = false;
|
|
31
|
+
if (!rt._dragging) return;
|
|
32
|
+
|
|
33
|
+
scene.physics.setVelocity(actor, chaseVelocity(p, actor.components.Layout, this.props));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Feedback while held (castle-client's Drag overlay): a circle on the grabbed
|
|
37
|
+
// object + a circle at the touch, connected by a line -- both translucent white
|
|
38
|
+
// with a solid white border. Drawn in world space so it doesn't spin with the
|
|
39
|
+
// actor.
|
|
40
|
+
draw(actor, scene, ctx) {
|
|
41
|
+
if (!actor.runtime._dragging) return;
|
|
42
|
+
const handle = centerOf(actor.components.Layout);
|
|
43
|
+
const touch = { x: scene.pointer.x, y: scene.pointer.y };
|
|
44
|
+
inActorWorldSpace(ctx, actor.components.Layout, (g) => drawDrag(g, handle, touch));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static Inspector({ component, setComponent, override }) {
|
|
48
|
+
return (
|
|
49
|
+
<Panel title="Draggable" overridden={override?.anyOverridden()}>
|
|
50
|
+
<AutoFields
|
|
51
|
+
defaultProps={Draggable.defaultProps}
|
|
52
|
+
component={component}
|
|
53
|
+
setComponent={setComponent}
|
|
54
|
+
override={override}
|
|
55
|
+
/>
|
|
56
|
+
</Panel>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Matches sling/drag.cpp: line first, then a circle at each end -- translucent
|
|
62
|
+
// white fill (0.3) + solid white border (0.8), radius 18.
|
|
63
|
+
function drawDrag(ctx, a, b) {
|
|
64
|
+
const radius = 18;
|
|
65
|
+
ctx.lineWidth = 1.5;
|
|
66
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
67
|
+
ctx.beginPath();
|
|
68
|
+
ctx.moveTo(a.x, a.y);
|
|
69
|
+
ctx.lineTo(b.x, b.y);
|
|
70
|
+
ctx.stroke();
|
|
71
|
+
for (const p of [a, b]) {
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
|
|
74
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
75
|
+
ctx.fill();
|
|
76
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
77
|
+
ctx.stroke();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Panel, SelectField } from '../../engine/ui';
|
|
3
|
+
import { AutoFields, overrideProps } from '../../engine/autoInspector';
|
|
4
|
+
|
|
5
|
+
// RigidBody makes an actor a physics object. Pair it with a Collider (which
|
|
6
|
+
// gives the shape + material). The simulation reads these props each frame; the
|
|
7
|
+
// behavior itself does no per-frame work -- author code drives motion through
|
|
8
|
+
// `scene.physics` (setVelocity / applyImpulse / applyForce).
|
|
9
|
+
//
|
|
10
|
+
// bodyType 'dynamic' -> simulated: gravity, collisions, forces move it.
|
|
11
|
+
// 'static' -> never moves (a wall); a Collider alone is also this.
|
|
12
|
+
// 'kinematic' -> unpushable, but you move it by writing its Layout.
|
|
13
|
+
// gravityScale -> multiplies world gravity for this body (0 = float).
|
|
14
|
+
// drag -> linear damping (matter frictionAir).
|
|
15
|
+
// angularDrag -> extra spin damping.
|
|
16
|
+
// freezeRotation -> lock rotation (stays upright).
|
|
17
|
+
// velocityX/velocityY -> initial velocity, applied once when play starts.
|
|
18
|
+
export class RigidBody {
|
|
19
|
+
static behaviorName = 'RigidBody';
|
|
20
|
+
|
|
21
|
+
static defaultProps = {
|
|
22
|
+
bodyType: 'dynamic',
|
|
23
|
+
gravityScale: 1,
|
|
24
|
+
drag: 0.01,
|
|
25
|
+
angularDrag: 0,
|
|
26
|
+
freezeRotation: false,
|
|
27
|
+
velocityX: 0,
|
|
28
|
+
velocityY: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
constructor(props) {
|
|
32
|
+
this.props = props;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static Inspector({ component, setComponent, override }) {
|
|
36
|
+
return (
|
|
37
|
+
<Panel title="RigidBody" overridden={override?.anyOverridden()}>
|
|
38
|
+
<SelectField
|
|
39
|
+
label="Body type"
|
|
40
|
+
value={component.bodyType}
|
|
41
|
+
onChange={(bodyType) => setComponent({ bodyType })}
|
|
42
|
+
options={['dynamic', 'static', 'kinematic']}
|
|
43
|
+
{...overrideProps(override, 'bodyType')}
|
|
44
|
+
/>
|
|
45
|
+
<AutoFields
|
|
46
|
+
defaultProps={RigidBody.defaultProps}
|
|
47
|
+
component={component}
|
|
48
|
+
setComponent={setComponent}
|
|
49
|
+
exclude={['bodyType']}
|
|
50
|
+
override={override}
|
|
51
|
+
/>
|
|
52
|
+
</Panel>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Panel } from '../../engine/ui';
|
|
3
|
+
import { AutoFields } from '../../engine/autoInspector';
|
|
4
|
+
import { inActorWorldSpace, slingLaunch, slingPull } from '../controls';
|
|
5
|
+
|
|
6
|
+
// Slingshot: pull back and release to fling the actor the opposite way
|
|
7
|
+
// (angry-birds style) -- the physics feel of castle-client's Sling. While
|
|
8
|
+
// aiming it draws an arrow showing the launch direction and power. Needs a
|
|
9
|
+
// RigidBody (dynamic) + Collider.
|
|
10
|
+
//
|
|
11
|
+
// speed -> launch velocity per pixel of pull.
|
|
12
|
+
// maxDrag -> pull distance cap (also caps launch power).
|
|
13
|
+
// grabAnywhere -> start the sling from a press ANYWHERE on screen (default,
|
|
14
|
+
// like castle-client); set false to require pressing the actor.
|
|
15
|
+
export class Slingshot {
|
|
16
|
+
static behaviorName = 'Slingshot';
|
|
17
|
+
|
|
18
|
+
static defaultProps = {
|
|
19
|
+
speed: 0.08,
|
|
20
|
+
maxDrag: 160,
|
|
21
|
+
grabAnywhere: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
constructor(props) {
|
|
25
|
+
this.props = props;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(actor, scene) {
|
|
29
|
+
const p = scene.pointer;
|
|
30
|
+
const rt = actor.runtime;
|
|
31
|
+
const pressed = p.down && !rt._slingWasDown;
|
|
32
|
+
rt._slingWasDown = p.down;
|
|
33
|
+
|
|
34
|
+
const grabAnywhere = this.props.grabAnywhere ?? true;
|
|
35
|
+
if (pressed && (grabAnywhere || scene.actorAt(p.x, p.y)?.id === actor.id)) {
|
|
36
|
+
rt._slinging = true;
|
|
37
|
+
// Anchor at the press point (like castle-client): the aim/launch depends
|
|
38
|
+
// on the drag gesture, not where the press landed relative to the actor.
|
|
39
|
+
rt._slingAnchor = { x: p.x, y: p.y };
|
|
40
|
+
}
|
|
41
|
+
if (!rt._slinging) return;
|
|
42
|
+
|
|
43
|
+
if (p.down) {
|
|
44
|
+
// Hold still while aiming, and remember the aim point for draw().
|
|
45
|
+
scene.physics.setVelocity(actor, { x: 0, y: 0 });
|
|
46
|
+
rt._slingAim = { x: p.x, y: p.y };
|
|
47
|
+
} else {
|
|
48
|
+
scene.physics.setVelocity(actor, slingLaunch(rt._slingAnchor, p, this.props));
|
|
49
|
+
rt._slinging = false;
|
|
50
|
+
rt._slingAim = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Aim overlay in the castle-client style: a translucent grab circle plus a
|
|
55
|
+
// line + arrowhead pointing in the launch direction (opposite the pull).
|
|
56
|
+
draw(actor, scene, ctx) {
|
|
57
|
+
const rt = actor.runtime;
|
|
58
|
+
if (!rt._slinging || !rt._slingAim) return;
|
|
59
|
+
const a = rt._slingAnchor;
|
|
60
|
+
const pull = slingPull(a, rt._slingAim, this.props.maxDrag ?? 160);
|
|
61
|
+
const len = Math.hypot(pull.x, pull.y);
|
|
62
|
+
if (len < 0.001) return;
|
|
63
|
+
const dir = { x: -pull.x / len, y: -pull.y / len }; // launch direction (opposite pull)
|
|
64
|
+
const end = { x: a.x - pull.x * 0.8, y: a.y - pull.y * 0.8 };
|
|
65
|
+
|
|
66
|
+
// The launch is absolute, so draw the aim in world space too (castle-client
|
|
67
|
+
// draws the sling as a screen overlay for the same reason).
|
|
68
|
+
inActorWorldSpace(ctx, actor.components.Layout, (c) => drawSling(c, a, end, dir));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static Inspector({ component, setComponent, override }) {
|
|
72
|
+
return (
|
|
73
|
+
<Panel title="Slingshot" overridden={override?.anyOverridden()}>
|
|
74
|
+
<AutoFields
|
|
75
|
+
defaultProps={Slingshot.defaultProps}
|
|
76
|
+
component={component}
|
|
77
|
+
setComponent={setComponent}
|
|
78
|
+
override={override}
|
|
79
|
+
/>
|
|
80
|
+
</Panel>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// castle-client's sling overlay: a white grab circle (solid outline + faint
|
|
86
|
+
// fill) at `start`, a line to the base of a filled triangle arrowhead at `end`,
|
|
87
|
+
// pointing along `dir`. Sizes mirror sling.cpp (circle 18, arrowhead 25x10).
|
|
88
|
+
function drawSling(ctx, start, end, dir) {
|
|
89
|
+
const radius = 18;
|
|
90
|
+
const triLen = 25;
|
|
91
|
+
const triW = 10;
|
|
92
|
+
const baseX = end.x - dir.x * triLen;
|
|
93
|
+
const baseY = end.y - dir.y * triLen;
|
|
94
|
+
const perpX = -dir.y;
|
|
95
|
+
const perpY = dir.x;
|
|
96
|
+
|
|
97
|
+
ctx.lineWidth = 1.5;
|
|
98
|
+
// Grab circle.
|
|
99
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
100
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
101
|
+
ctx.beginPath();
|
|
102
|
+
ctx.arc(start.x, start.y, radius, 0, Math.PI * 2);
|
|
103
|
+
ctx.fill();
|
|
104
|
+
ctx.stroke();
|
|
105
|
+
// Line from the grab circle to the arrowhead base.
|
|
106
|
+
ctx.beginPath();
|
|
107
|
+
ctx.moveTo(start.x, start.y);
|
|
108
|
+
ctx.lineTo(baseX, baseY);
|
|
109
|
+
ctx.stroke();
|
|
110
|
+
// Filled triangle arrowhead.
|
|
111
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
ctx.moveTo(end.x, end.y);
|
|
114
|
+
ctx.lineTo(baseX + perpX * triW, baseY + perpY * triW);
|
|
115
|
+
ctx.lineTo(baseX - perpX * triW, baseY - perpY * triW);
|
|
116
|
+
ctx.closePath();
|
|
117
|
+
ctx.fill();
|
|
118
|
+
}
|