@jgengine/core 0.7.0 → 0.8.0
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/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +5 -0
- package/dist/game/race.js +20 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +16 -17
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity-style behaviour lifecycle over a headless id-keyed node tree, ported from
|
|
3
|
+
* vladkrutenyuk/three-start (MIT) and adapted to run without three.js: nodes are plain
|
|
4
|
+
* string ids (use entity instance ids to pair with `entityStore`), and the shell binds
|
|
5
|
+
* render-side hooks separately.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle per behaviour: `onAwake` (once, first activation) → `onEnable` (every
|
|
8
|
+
* inactive→active transition) → `onStart` (once, after the first `onEnable`) →
|
|
9
|
+
* `onUpdate` (per `world.update` while active) → `onDisable` (every active→inactive
|
|
10
|
+
* transition) → `onDestroy` (once, always — even if never activated).
|
|
11
|
+
*
|
|
12
|
+
* `world.start()` bootstraps in passes: modules awake → modules start → modules join
|
|
13
|
+
* update dispatch → ALL behaviours on effectively-active nodes awake (even disabled
|
|
14
|
+
* ones) → enable/start/subscribe. Behaviours attached during the module pass are
|
|
15
|
+
* deferred to the behaviour passes, so module `onUpdate` always dispatches before any
|
|
16
|
+
* behaviour `onUpdate`.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Subclass and override the lifecycle hooks. A behaviour only joins the per-frame
|
|
20
|
+
* update dispatch if it actually overrides `onUpdate` (prototype-identity check at
|
|
21
|
+
* each activation), so hook-only behaviours cost nothing per frame.
|
|
22
|
+
*/
|
|
23
|
+
export class Behaviour {
|
|
24
|
+
/** @internal */ _world = null;
|
|
25
|
+
/** @internal */ _nodeId = null;
|
|
26
|
+
/** @internal */ _awoken = false;
|
|
27
|
+
/** @internal */ _startedOnce = false;
|
|
28
|
+
/** @internal */ _isActive = false;
|
|
29
|
+
_enabled = true;
|
|
30
|
+
_destroyed = false;
|
|
31
|
+
/** The node this behaviour is attached to; available from `onAwake` onward, never in the constructor. */
|
|
32
|
+
get nodeId() {
|
|
33
|
+
if (this._nodeId === null)
|
|
34
|
+
throw new Error("behaviour is not attached to a node");
|
|
35
|
+
return this._nodeId;
|
|
36
|
+
}
|
|
37
|
+
get world() {
|
|
38
|
+
if (this._world === null)
|
|
39
|
+
throw new Error("behaviour is not attached to a world");
|
|
40
|
+
return this._world;
|
|
41
|
+
}
|
|
42
|
+
get modules() {
|
|
43
|
+
return this.world.modules;
|
|
44
|
+
}
|
|
45
|
+
/** Own enabled flag; effective activity also requires the node's hierarchy to be active. */
|
|
46
|
+
get enabled() {
|
|
47
|
+
return this._enabled;
|
|
48
|
+
}
|
|
49
|
+
/** Whether the lifecycle is currently in the active span (`onEnable` fired, `onDisable` not yet). */
|
|
50
|
+
get isActive() {
|
|
51
|
+
return this._isActive;
|
|
52
|
+
}
|
|
53
|
+
onAwake() { }
|
|
54
|
+
onEnable() { }
|
|
55
|
+
onStart() { }
|
|
56
|
+
onUpdate(_dt) { }
|
|
57
|
+
onDisable() { }
|
|
58
|
+
onDestroy() { }
|
|
59
|
+
enable() {
|
|
60
|
+
this.setEnabled(true);
|
|
61
|
+
}
|
|
62
|
+
disable() {
|
|
63
|
+
this.setEnabled(false);
|
|
64
|
+
}
|
|
65
|
+
/** Flips the flag only when the node is hierarchy-inactive or the world hasn't started; lifecycle catches up on the next activation. */
|
|
66
|
+
setEnabled(enabled) {
|
|
67
|
+
if (this._enabled === enabled)
|
|
68
|
+
return;
|
|
69
|
+
this._enabled = enabled;
|
|
70
|
+
const world = this._world;
|
|
71
|
+
const nodeId = this._nodeId;
|
|
72
|
+
if (world === null || nodeId === null)
|
|
73
|
+
return;
|
|
74
|
+
if (!world.started() || !world._isEffectivelyActive(nodeId))
|
|
75
|
+
return;
|
|
76
|
+
if (enabled)
|
|
77
|
+
this._activate();
|
|
78
|
+
else
|
|
79
|
+
this._deactivate();
|
|
80
|
+
}
|
|
81
|
+
/** Detaches from the node and fires `onDisable` (if active) then `onDestroy`. */
|
|
82
|
+
destroy() {
|
|
83
|
+
if (this._world !== null)
|
|
84
|
+
this._world._detachBehaviour(this);
|
|
85
|
+
this._destroy();
|
|
86
|
+
}
|
|
87
|
+
/** @internal */
|
|
88
|
+
_activate() {
|
|
89
|
+
if (this._isActive)
|
|
90
|
+
return;
|
|
91
|
+
if (this._world === null)
|
|
92
|
+
return;
|
|
93
|
+
this._isActive = true;
|
|
94
|
+
if (!this._awoken) {
|
|
95
|
+
this._awoken = true;
|
|
96
|
+
this.onAwake();
|
|
97
|
+
}
|
|
98
|
+
this.onEnable();
|
|
99
|
+
if (!this._startedOnce) {
|
|
100
|
+
this._startedOnce = true;
|
|
101
|
+
this.onStart();
|
|
102
|
+
}
|
|
103
|
+
if (this.onUpdate !== Behaviour.prototype.onUpdate) {
|
|
104
|
+
this._world._subscribeUpdate(this);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** @internal */
|
|
108
|
+
_deactivate() {
|
|
109
|
+
if (!this._isActive)
|
|
110
|
+
return;
|
|
111
|
+
this._isActive = false;
|
|
112
|
+
this._world?._unsubscribeUpdate(this);
|
|
113
|
+
this.onDisable();
|
|
114
|
+
}
|
|
115
|
+
/** @internal */
|
|
116
|
+
_destroy() {
|
|
117
|
+
if (this._destroyed)
|
|
118
|
+
return;
|
|
119
|
+
this._destroyed = true;
|
|
120
|
+
this._deactivate();
|
|
121
|
+
this.onDestroy();
|
|
122
|
+
this._world = null;
|
|
123
|
+
this._nodeId = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A world-lifetime service with typed sibling access via `this.modules`. Modules awake
|
|
128
|
+
* and start before any behaviour during `world.start()`, subscribe to update dispatch
|
|
129
|
+
* first (their `onUpdate` fires before every behaviour's), and have no disable/destroy —
|
|
130
|
+
* they live as long as the world.
|
|
131
|
+
*/
|
|
132
|
+
export class BehaviourModule {
|
|
133
|
+
/** @internal */ _world = null;
|
|
134
|
+
get world() {
|
|
135
|
+
if (this._world === null)
|
|
136
|
+
throw new Error("module is not registered with a world");
|
|
137
|
+
return this._world;
|
|
138
|
+
}
|
|
139
|
+
get modules() {
|
|
140
|
+
return this.world.modules;
|
|
141
|
+
}
|
|
142
|
+
onAwake() { }
|
|
143
|
+
onStart() { }
|
|
144
|
+
onUpdate(_dt) { }
|
|
145
|
+
/** @internal */
|
|
146
|
+
_subscribe() {
|
|
147
|
+
if (this._world === null)
|
|
148
|
+
return;
|
|
149
|
+
if (this.onUpdate !== BehaviourModule.prototype.onUpdate) {
|
|
150
|
+
this._world._subscribeUpdate(this);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function createBehaviourWorld() {
|
|
155
|
+
const nodes = new Map();
|
|
156
|
+
const roots = [];
|
|
157
|
+
const updateSubscribers = [];
|
|
158
|
+
const moduleRecord = {};
|
|
159
|
+
let started = false;
|
|
160
|
+
let bootstrapping = false;
|
|
161
|
+
function mustGet(id) {
|
|
162
|
+
const node = nodes.get(id);
|
|
163
|
+
if (node === undefined)
|
|
164
|
+
throw new Error(`unknown behaviour node "${id}"`);
|
|
165
|
+
return node;
|
|
166
|
+
}
|
|
167
|
+
function ensureNode(id, parentId) {
|
|
168
|
+
const existing = nodes.get(id);
|
|
169
|
+
if (existing !== undefined) {
|
|
170
|
+
if (parentId !== undefined && existing.parent?.id !== parentId)
|
|
171
|
+
world.setParent(id, parentId);
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
const parent = parentId === undefined ? null : ensureNode(parentId);
|
|
175
|
+
const node = { id, parent, children: [], activeSelf: true, behaviours: [] };
|
|
176
|
+
nodes.set(id, node);
|
|
177
|
+
if (parent === null)
|
|
178
|
+
roots.push(node);
|
|
179
|
+
else
|
|
180
|
+
parent.children.push(node);
|
|
181
|
+
return node;
|
|
182
|
+
}
|
|
183
|
+
function parentChainActive(node) {
|
|
184
|
+
let ancestor = node.parent;
|
|
185
|
+
while (ancestor !== null) {
|
|
186
|
+
if (!ancestor.activeSelf)
|
|
187
|
+
return false;
|
|
188
|
+
ancestor = ancestor.parent;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
function isEffectivelyActive(node) {
|
|
193
|
+
return node.activeSelf && parentChainActive(node);
|
|
194
|
+
}
|
|
195
|
+
function traverseActiveSelf(node, visit) {
|
|
196
|
+
if (!node.activeSelf)
|
|
197
|
+
return;
|
|
198
|
+
visit(node);
|
|
199
|
+
for (const child of [...node.children])
|
|
200
|
+
traverseActiveSelf(child, visit);
|
|
201
|
+
}
|
|
202
|
+
function activateNode(node) {
|
|
203
|
+
if (!started)
|
|
204
|
+
return;
|
|
205
|
+
if (!isEffectivelyActive(node))
|
|
206
|
+
return;
|
|
207
|
+
for (const behaviour of [...node.behaviours]) {
|
|
208
|
+
if (behaviour.enabled)
|
|
209
|
+
behaviour._activate();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function deactivateNode(node) {
|
|
213
|
+
for (const behaviour of [...node.behaviours]) {
|
|
214
|
+
if (behaviour._isActive)
|
|
215
|
+
behaviour._deactivate();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function deactivateSubtree(node) {
|
|
219
|
+
deactivateNode(node);
|
|
220
|
+
for (const child of [...node.children])
|
|
221
|
+
traverseActiveSelf(child, deactivateNode);
|
|
222
|
+
}
|
|
223
|
+
function unlink(node) {
|
|
224
|
+
const siblings = node.parent === null ? roots : node.parent.children;
|
|
225
|
+
const index = siblings.indexOf(node);
|
|
226
|
+
if (index !== -1)
|
|
227
|
+
siblings.splice(index, 1);
|
|
228
|
+
}
|
|
229
|
+
function collectSubtree(node, into) {
|
|
230
|
+
into.push(node);
|
|
231
|
+
for (const child of node.children)
|
|
232
|
+
collectSubtree(child, into);
|
|
233
|
+
}
|
|
234
|
+
const world = {
|
|
235
|
+
add(id, parentId) {
|
|
236
|
+
ensureNode(id, parentId);
|
|
237
|
+
},
|
|
238
|
+
has: (id) => nodes.has(id),
|
|
239
|
+
parentOf: (id) => mustGet(id).parent?.id ?? null,
|
|
240
|
+
childrenOf: (id) => mustGet(id).children.map((child) => child.id),
|
|
241
|
+
setParent(id, parentId) {
|
|
242
|
+
const node = mustGet(id);
|
|
243
|
+
const nextParent = parentId === null ? null : mustGet(parentId);
|
|
244
|
+
if (node.parent === nextParent)
|
|
245
|
+
return;
|
|
246
|
+
for (let ancestor = nextParent; ancestor !== null; ancestor = ancestor.parent) {
|
|
247
|
+
if (ancestor === node)
|
|
248
|
+
throw new Error(`cannot parent "${parentId}" under its own descendant "${id}"`);
|
|
249
|
+
}
|
|
250
|
+
const wasEffective = isEffectivelyActive(node);
|
|
251
|
+
unlink(node);
|
|
252
|
+
node.parent = nextParent;
|
|
253
|
+
if (nextParent === null)
|
|
254
|
+
roots.push(node);
|
|
255
|
+
else
|
|
256
|
+
nextParent.children.push(node);
|
|
257
|
+
const nowEffective = isEffectivelyActive(node);
|
|
258
|
+
if (!started || wasEffective === nowEffective)
|
|
259
|
+
return;
|
|
260
|
+
if (nowEffective)
|
|
261
|
+
traverseActiveSelf(node, activateNode);
|
|
262
|
+
else
|
|
263
|
+
deactivateSubtree(node);
|
|
264
|
+
},
|
|
265
|
+
attach(nodeId, behaviour) {
|
|
266
|
+
if (behaviour._world !== null)
|
|
267
|
+
throw new Error("behaviour is already attached");
|
|
268
|
+
const node = ensureNode(nodeId);
|
|
269
|
+
behaviour._world = world;
|
|
270
|
+
behaviour._nodeId = nodeId;
|
|
271
|
+
node.behaviours.push(behaviour);
|
|
272
|
+
if (started && !bootstrapping && behaviour.enabled && isEffectivelyActive(node)) {
|
|
273
|
+
behaviour._activate();
|
|
274
|
+
}
|
|
275
|
+
return behaviour;
|
|
276
|
+
},
|
|
277
|
+
behavioursOf: (nodeId) => [...(nodes.get(nodeId)?.behaviours ?? [])],
|
|
278
|
+
setActive(id, active) {
|
|
279
|
+
const node = ensureNode(id);
|
|
280
|
+
if (node.activeSelf === active)
|
|
281
|
+
return;
|
|
282
|
+
node.activeSelf = active;
|
|
283
|
+
if (!parentChainActive(node))
|
|
284
|
+
return;
|
|
285
|
+
if (active)
|
|
286
|
+
traverseActiveSelf(node, activateNode);
|
|
287
|
+
else
|
|
288
|
+
deactivateSubtree(node);
|
|
289
|
+
},
|
|
290
|
+
isActiveSelf: (id) => nodes.get(id)?.activeSelf ?? true,
|
|
291
|
+
isActive(id) {
|
|
292
|
+
const node = nodes.get(id);
|
|
293
|
+
return node === undefined ? true : isEffectivelyActive(node);
|
|
294
|
+
},
|
|
295
|
+
addModules(modules) {
|
|
296
|
+
if (started)
|
|
297
|
+
throw new Error("cannot add modules after start()");
|
|
298
|
+
for (const [key, module] of Object.entries(modules)) {
|
|
299
|
+
if (module === undefined)
|
|
300
|
+
continue;
|
|
301
|
+
if (moduleRecord[key] !== undefined)
|
|
302
|
+
throw new Error(`module "${key}" is already registered`);
|
|
303
|
+
const instance = module;
|
|
304
|
+
instance._world = world;
|
|
305
|
+
moduleRecord[key] = instance;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
modules: moduleRecord,
|
|
309
|
+
start() {
|
|
310
|
+
if (started)
|
|
311
|
+
return;
|
|
312
|
+
started = true;
|
|
313
|
+
bootstrapping = true;
|
|
314
|
+
const modules = Object.values(moduleRecord);
|
|
315
|
+
for (const module of modules)
|
|
316
|
+
module.onAwake();
|
|
317
|
+
for (const module of modules)
|
|
318
|
+
module.onStart();
|
|
319
|
+
for (const module of modules)
|
|
320
|
+
module._subscribe();
|
|
321
|
+
bootstrapping = false;
|
|
322
|
+
for (const root of [...roots]) {
|
|
323
|
+
traverseActiveSelf(root, (node) => {
|
|
324
|
+
for (const behaviour of [...node.behaviours]) {
|
|
325
|
+
if (!behaviour._awoken) {
|
|
326
|
+
behaviour._awoken = true;
|
|
327
|
+
behaviour.onAwake();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
for (const root of [...roots]) {
|
|
333
|
+
traverseActiveSelf(root, activateNode);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
started: () => started,
|
|
337
|
+
update(dt) {
|
|
338
|
+
for (const subscriber of [...updateSubscribers])
|
|
339
|
+
subscriber.onUpdate(dt);
|
|
340
|
+
},
|
|
341
|
+
destroyNode(id) {
|
|
342
|
+
const node = nodes.get(id);
|
|
343
|
+
if (node === undefined)
|
|
344
|
+
return false;
|
|
345
|
+
const subtree = [];
|
|
346
|
+
collectSubtree(node, subtree);
|
|
347
|
+
for (const member of subtree) {
|
|
348
|
+
for (const behaviour of [...member.behaviours])
|
|
349
|
+
behaviour._destroy();
|
|
350
|
+
member.behaviours.length = 0;
|
|
351
|
+
}
|
|
352
|
+
unlink(node);
|
|
353
|
+
for (const member of subtree)
|
|
354
|
+
nodes.delete(member.id);
|
|
355
|
+
return true;
|
|
356
|
+
},
|
|
357
|
+
destroy() {
|
|
358
|
+
for (const root of [...roots])
|
|
359
|
+
world.destroyNode(root.id);
|
|
360
|
+
updateSubscribers.length = 0;
|
|
361
|
+
},
|
|
362
|
+
_subscribeUpdate(subscriber) {
|
|
363
|
+
if (!updateSubscribers.includes(subscriber))
|
|
364
|
+
updateSubscribers.push(subscriber);
|
|
365
|
+
},
|
|
366
|
+
_unsubscribeUpdate(subscriber) {
|
|
367
|
+
const index = updateSubscribers.indexOf(subscriber);
|
|
368
|
+
if (index !== -1)
|
|
369
|
+
updateSubscribers.splice(index, 1);
|
|
370
|
+
},
|
|
371
|
+
_isEffectivelyActive: (nodeId) => {
|
|
372
|
+
const node = nodes.get(nodeId);
|
|
373
|
+
return node === undefined ? true : isEffectivelyActive(node);
|
|
374
|
+
},
|
|
375
|
+
_detachBehaviour(behaviour) {
|
|
376
|
+
const nodeId = behaviour._nodeId;
|
|
377
|
+
if (nodeId === null)
|
|
378
|
+
return;
|
|
379
|
+
const node = nodes.get(nodeId);
|
|
380
|
+
if (node === undefined)
|
|
381
|
+
return;
|
|
382
|
+
const index = node.behaviours.indexOf(behaviour);
|
|
383
|
+
if (index !== -1)
|
|
384
|
+
node.behaviours.splice(index, 1);
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
return world;
|
|
388
|
+
}
|
package/dist/cards/cardPile.d.ts
CHANGED
package/dist/cards/cardPile.js
CHANGED
|
@@ -130,6 +130,7 @@ export function createCardPile(config, initial) {
|
|
|
130
130
|
zoneOf: (cardId) => zoneOf(state, cardId),
|
|
131
131
|
shuffle(zone, seed) {
|
|
132
132
|
state = shuffleZone(state, zone ?? drawFrom, seed);
|
|
133
|
+
config.onChange?.();
|
|
133
134
|
},
|
|
134
135
|
draw(n, options) {
|
|
135
136
|
const result = draw(state, n, {
|
|
@@ -139,28 +140,36 @@ export function createCardPile(config, initial) {
|
|
|
139
140
|
reshuffleFrom: config.reshuffleFrom,
|
|
140
141
|
});
|
|
141
142
|
state = result.state;
|
|
143
|
+
config.onChange?.();
|
|
142
144
|
return result.drawn;
|
|
143
145
|
},
|
|
144
146
|
discard(ids, options) {
|
|
145
147
|
const result = moveCards(state, ids, options?.from ?? handZone, options?.to ?? discardTo, "top");
|
|
146
|
-
if (result.status === "ok")
|
|
148
|
+
if (result.status === "ok") {
|
|
147
149
|
state = result.state;
|
|
150
|
+
config.onChange?.();
|
|
151
|
+
}
|
|
148
152
|
return result;
|
|
149
153
|
},
|
|
150
154
|
exhaust(ids, exhaustZone, from) {
|
|
151
155
|
const result = moveCards(state, ids, from ?? handZone, exhaustZone, "top");
|
|
152
|
-
if (result.status === "ok")
|
|
156
|
+
if (result.status === "ok") {
|
|
153
157
|
state = result.state;
|
|
158
|
+
config.onChange?.();
|
|
159
|
+
}
|
|
154
160
|
return result;
|
|
155
161
|
},
|
|
156
162
|
move(ids, from, to, position) {
|
|
157
163
|
const result = moveCards(state, ids, from, to, position);
|
|
158
|
-
if (result.status === "ok")
|
|
164
|
+
if (result.status === "ok") {
|
|
159
165
|
state = result.state;
|
|
166
|
+
config.onChange?.();
|
|
167
|
+
}
|
|
160
168
|
return result;
|
|
161
169
|
},
|
|
162
170
|
reset(next) {
|
|
163
171
|
state = next;
|
|
172
|
+
config.onChange?.();
|
|
164
173
|
},
|
|
165
174
|
};
|
|
166
175
|
}
|
|
@@ -21,6 +21,10 @@ export interface AbilitySlotSnapshot {
|
|
|
21
21
|
ready: boolean;
|
|
22
22
|
}
|
|
23
23
|
export type AbilityCastReason = "unknown-slot" | "cooldown" | "no-resource";
|
|
24
|
+
export interface AbilitySlotRetune {
|
|
25
|
+
cooldownMs?: number;
|
|
26
|
+
resourceCost?: number;
|
|
27
|
+
}
|
|
24
28
|
export type AbilityCastResult = {
|
|
25
29
|
ok: true;
|
|
26
30
|
slot: AbilitySlotSnapshot;
|
|
@@ -38,5 +42,6 @@ export interface AbilityKit {
|
|
|
38
42
|
cast(slotId: string, resourceAvailable?: number): AbilityCastResult;
|
|
39
43
|
tick(dtSeconds: number): void;
|
|
40
44
|
reset(slotId?: string): void;
|
|
45
|
+
retuneSlot(slotId: string, patch: AbilitySlotRetune): boolean;
|
|
41
46
|
}
|
|
42
47
|
export declare function createAbilityKit(configs: readonly AbilitySlotConfig[]): AbilityKit;
|
|
@@ -125,5 +125,15 @@ export function createAbilityKit(configs) {
|
|
|
125
125
|
runtime.flashRemainingMs = 0;
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
|
+
retuneSlot(slotId, patch) {
|
|
129
|
+
const runtime = runtimes.get(slotId);
|
|
130
|
+
if (runtime === undefined)
|
|
131
|
+
return false;
|
|
132
|
+
if (patch.cooldownMs !== undefined)
|
|
133
|
+
runtime.config.cooldownMs = Math.max(0, patch.cooldownMs);
|
|
134
|
+
if (patch.resourceCost !== undefined)
|
|
135
|
+
runtime.config.resourceCost = Math.max(0, patch.resourceCost);
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
128
138
|
};
|
|
129
139
|
}
|
package/dist/combat/effects.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface EffectSystemDeps {
|
|
|
65
65
|
onLethal?(instanceId: string, ctx: LethalContext): void;
|
|
66
66
|
}
|
|
67
67
|
export interface EffectSystem {
|
|
68
|
-
canReceive(instanceId: string, effect: string): string | null;
|
|
68
|
+
canReceive(instanceId: string, effect: string, magnitude?: number): string | null;
|
|
69
69
|
preview(input: SingleTargetEffectInput): number;
|
|
70
70
|
applyEffect(input: EffectInput): EffectResult[];
|
|
71
71
|
}
|
package/dist/combat/effects.js
CHANGED
|
@@ -40,18 +40,21 @@ export function createEffectSystem(deps) {
|
|
|
40
40
|
}
|
|
41
41
|
return result;
|
|
42
42
|
}
|
|
43
|
-
function canReceive(instanceId, effect) {
|
|
43
|
+
function canReceive(instanceId, effect, magnitude) {
|
|
44
44
|
const rule = resolveRule(instanceId, effect);
|
|
45
45
|
if (rule === null)
|
|
46
46
|
return "not-receivable";
|
|
47
47
|
const stats = deps.resolveStats(instanceId);
|
|
48
48
|
if (stats === undefined)
|
|
49
49
|
return "unknown-instance";
|
|
50
|
-
const
|
|
50
|
+
const restorative = magnitude !== undefined && magnitude < 0;
|
|
51
|
+
const anyPoolHasHeadroom = rule.order.some((statId) => {
|
|
51
52
|
const stat = stats[statId];
|
|
52
|
-
|
|
53
|
+
if (stat === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return restorative ? stat.current < stat.max : stat.current > stat.min;
|
|
53
56
|
});
|
|
54
|
-
if (!
|
|
57
|
+
if (!anyPoolHasHeadroom)
|
|
55
58
|
return "pools-depleted";
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
@@ -80,13 +83,13 @@ export function createEffectSystem(deps) {
|
|
|
80
83
|
return { instanceId, effect, applied, lethal };
|
|
81
84
|
}
|
|
82
85
|
function applyTo(instanceId, effect, via, from, scale) {
|
|
83
|
-
if (canReceive(instanceId, effect) !== null)
|
|
84
|
-
return null;
|
|
85
86
|
const rule = resolveRule(instanceId, effect);
|
|
86
87
|
const stats = deps.resolveStats(instanceId);
|
|
87
88
|
if (rule === null || stats === undefined)
|
|
88
89
|
return null;
|
|
89
90
|
const drainMagnitude = modifiedDrainMagnitude(baseDrainMagnitude(effect, via) * scale, rule);
|
|
91
|
+
if (canReceive(instanceId, effect, drainMagnitude) !== null)
|
|
92
|
+
return null;
|
|
90
93
|
const result = drainPools(instanceId, effect, rule, stats, drainMagnitude);
|
|
91
94
|
if (result.lethal)
|
|
92
95
|
deps.onLethal?.(instanceId, { from, via, effect });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BallisticSweep } from "../physics/ballisticSweep.js";
|
|
1
2
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
3
|
import type { Aim } from "../scene/spatial.js";
|
|
3
4
|
import type { CombatSpatialDeps, EffectResult, EffectSystem, EffectVia } from "./effects.js";
|
|
@@ -7,11 +8,22 @@ export interface ProjectileShotInput {
|
|
|
7
8
|
aim: Aim;
|
|
8
9
|
effect: string;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
/** A raycast hit against a scene entity — the ray reached `instanceId` at `distance` along its length. */
|
|
12
|
+
export interface EntityRaycastHit {
|
|
13
|
+
kind: "entity";
|
|
11
14
|
instanceId: string;
|
|
12
15
|
distance: number;
|
|
13
16
|
at: EntityPosition;
|
|
14
17
|
}
|
|
18
|
+
/** A raycast hit against a placed scene object's bounding box — see `ProjectileObjectsDeps.halfExtents`. */
|
|
19
|
+
export interface ObjectRaycastHit {
|
|
20
|
+
kind: "object";
|
|
21
|
+
instanceId: string;
|
|
22
|
+
catalogId: string;
|
|
23
|
+
distance: number;
|
|
24
|
+
at: EntityPosition;
|
|
25
|
+
}
|
|
26
|
+
export type RaycastHit = EntityRaycastHit | ObjectRaycastHit;
|
|
15
27
|
export type Raycast = (from: string, aim: Aim, range: number) => RaycastHit[];
|
|
16
28
|
export interface ProjectileSettleReport {
|
|
17
29
|
from: string;
|
|
@@ -20,19 +32,44 @@ export interface ProjectileSettleReport {
|
|
|
20
32
|
effect: string;
|
|
21
33
|
hit: boolean;
|
|
22
34
|
}
|
|
35
|
+
/** Read-only scene object access for the default `raycast`; matches `ObjectStore.list()`/a catalog lookup structurally. */
|
|
36
|
+
export interface ProjectileObjectsDeps {
|
|
37
|
+
list(): readonly {
|
|
38
|
+
instanceId: string;
|
|
39
|
+
catalogId: string;
|
|
40
|
+
position: [number, number, number];
|
|
41
|
+
}[];
|
|
42
|
+
/** Half-extents of the object's axis-aligned bounding box; `null`/omitted falls back to `[0.5, 0.5, 0.5]`. */
|
|
43
|
+
halfExtents?(catalogId: string): [number, number, number] | null;
|
|
44
|
+
}
|
|
23
45
|
export interface ProjectileSystemDeps {
|
|
24
46
|
effects: EffectSystem;
|
|
25
47
|
spatial: CombatSpatialDeps;
|
|
26
48
|
getStat(itemId: string, stat: string): number | null;
|
|
27
49
|
raycast?: Raycast;
|
|
50
|
+
/** Optional object awareness for the default `raycast`; when set, placed objects can block or absorb a shot. */
|
|
51
|
+
objects?: ProjectileObjectsDeps;
|
|
52
|
+
/**
|
|
53
|
+
* Optional collision-aware arc test for ballistic shots (see `createBallisticSweep` in
|
|
54
|
+
* `physics/ballisticSweep`). A hit settles the shot at the impact point; `null` or omission
|
|
55
|
+
* falls back to the closed-form landing.
|
|
56
|
+
*/
|
|
57
|
+
sweepBallistic?: BallisticSweep;
|
|
28
58
|
now?: () => number;
|
|
29
59
|
onSettle?(report: ProjectileSettleReport): void;
|
|
30
60
|
}
|
|
61
|
+
export type ProjectileHit = {
|
|
62
|
+
kind: "entity";
|
|
63
|
+
instanceId: string;
|
|
64
|
+
distance: number;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "object";
|
|
67
|
+
instanceId: string;
|
|
68
|
+
catalogId: string;
|
|
69
|
+
distance: number;
|
|
70
|
+
};
|
|
31
71
|
export interface ProjectilePrediction {
|
|
32
|
-
hits:
|
|
33
|
-
instanceId: string;
|
|
34
|
-
distance: number;
|
|
35
|
-
}[];
|
|
72
|
+
hits: ProjectileHit[];
|
|
36
73
|
blocked?: boolean;
|
|
37
74
|
}
|
|
38
75
|
export type SettleResult = {
|