@jgengine/core 0.5.0 → 0.7.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 +191 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +5 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +291 -5
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +53 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +219 -0
- package/dist/physics/physicsWorld.js +749 -0
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +66 -2
- package/dist/runtime/gameContext.js +157 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/assetCatalog.d.ts +13 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +7 -1
- package/dist/scene/entityStore.js +15 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +62 -0
- package/dist/time/simClock.js +132 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/buildingIndex.d.ts +16 -0
- package/dist/world/buildingIndex.js +63 -0
- package/dist/world/buildings.d.ts +98 -0
- package/dist/world/buildings.js +320 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/features.d.ts +105 -1
- package/dist/world/features.js +95 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/regions.d.ts +66 -0
- package/dist/world/regions.js +158 -0
- package/dist/world/scatter.d.ts +24 -0
- package/dist/world/scatter.js +126 -0
- package/dist/world/scatterItems.d.ts +38 -0
- package/dist/world/scatterItems.js +85 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +54 -0
- package/dist/world/terrain.js +181 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/water.d.ts +27 -0
- package/dist/world/water.js +125 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/dist/world/wind.d.ts +17 -0
- package/dist/world/wind.js +40 -0
- package/package.json +1 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { createDeathSystem, deathReasonFromEffect } from "../combat/death.js";
|
|
1
|
+
import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath } from "../combat/death.js";
|
|
2
2
|
import { createEffectSystem, } from "../combat/effects.js";
|
|
3
3
|
import { createProjectileSystem } from "../combat/projectiles.js";
|
|
4
|
+
import { resolveHitReaction, } from "../combat/hitReaction.js";
|
|
5
|
+
import { pointInTelegraph, } from "../combat/telegraph.js";
|
|
4
6
|
import { createCommandRegistry, } from "../commands/commandRegistry.js";
|
|
5
7
|
import { balance as walletBalance, canAfford as walletCanAfford, charge as walletCharge, chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, } from "../economy/wallet.js";
|
|
8
|
+
import { createCosmetics } from "../game/cosmetics.js";
|
|
6
9
|
import { createGameEvents } from "../game/events.js";
|
|
7
10
|
import { createGameFeed } from "../game/feed.js";
|
|
8
11
|
import { createLeaderboard } from "../game/leaderboard.js";
|
|
9
12
|
import { createLoadouts } from "../game/loadout.js";
|
|
10
13
|
import { createLootRegistry, grantDrops } from "../game/lootTable.js";
|
|
11
14
|
import { createQuestJournal } from "../game/quest.js";
|
|
15
|
+
import { createWorldItemStore, resolveDeathDrops, DEFAULT_RARITY, WORLD_ITEM_ENTITY_NAME, } from "../game/worldItem.js";
|
|
12
16
|
import { createSocial } from "../game/social.js";
|
|
13
17
|
import { createTradeSystem } from "../game/trade.js";
|
|
14
18
|
import { createUnlocks } from "../game/unlocks.js";
|
|
@@ -17,15 +21,20 @@ import { createItemUse, } from "../item/use.js";
|
|
|
17
21
|
import { createWeaponStats } from "../item/weapon.js";
|
|
18
22
|
import { createPoseState } from "../movement/poseState.js";
|
|
19
23
|
import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
|
|
24
|
+
import { createForms } from "../scene/form.js";
|
|
20
25
|
import { createObjectStore } from "../scene/objectStore.js";
|
|
26
|
+
import { createRoster } from "../scene/roster.js";
|
|
27
|
+
import { createPossession } from "../scene/possession.js";
|
|
21
28
|
import { createSpatialApi } from "../scene/spatial.js";
|
|
22
29
|
import { createTargeting } from "../scene/targeting.js";
|
|
23
30
|
import { createStats } from "../stats/statModifiers.js";
|
|
24
31
|
import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
|
|
32
|
+
import { createSimClock } from "../time/simClock.js";
|
|
25
33
|
export function createGameContext(options) {
|
|
26
34
|
const { definition, content, player } = options;
|
|
27
35
|
const now = options.now ?? Date.now;
|
|
28
36
|
const signal = createChangeSignal();
|
|
37
|
+
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
29
38
|
const entities = definition.scene;
|
|
30
39
|
const objects = createObjectStore();
|
|
31
40
|
entities.subscribe(signal.notify);
|
|
@@ -78,17 +87,29 @@ export function createGameContext(options) {
|
|
|
78
87
|
const feed = createGameFeed();
|
|
79
88
|
const lootRegistry = createLootRegistry();
|
|
80
89
|
const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
|
|
81
|
-
const rawSocial = createSocial({
|
|
90
|
+
const rawSocial = createSocial({
|
|
91
|
+
events,
|
|
92
|
+
now,
|
|
93
|
+
emotes: {
|
|
94
|
+
entities: { get: (id) => entities.get(id) },
|
|
95
|
+
spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
82
98
|
const social = {
|
|
83
99
|
friends: notifyAfter(rawSocial.friends, ["request", "accept", "remove", "block", "hydrate"], signal.notify),
|
|
84
100
|
party: notifyAfter(rawSocial.party, ["invite", "accept", "kick", "leave", "promote"], signal.notify),
|
|
85
101
|
presence: rawSocial.presence,
|
|
102
|
+
emotes: rawSocial.emotes,
|
|
86
103
|
};
|
|
87
104
|
const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify);
|
|
105
|
+
const roster = notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify);
|
|
88
106
|
const playerStats = createStats({});
|
|
89
107
|
const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
|
|
90
108
|
const commandRegistry = createCommandRegistry();
|
|
91
109
|
const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
|
|
110
|
+
const possession = notifyAfter(createPossession({ entities, events }), ["possess", "own", "disown"], signal.notify);
|
|
111
|
+
const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signal.notify);
|
|
112
|
+
const cosmetics = notifyAfter(createCosmetics({ events }), ["apply", "equip", "hydrate"], signal.notify);
|
|
92
113
|
const inventoryDeclarations = definition.inventories ?? {};
|
|
93
114
|
const inventoryIds = Object.keys(inventoryDeclarations);
|
|
94
115
|
const layouts = {};
|
|
@@ -240,6 +261,37 @@ export function createGameContext(options) {
|
|
|
240
261
|
pose.clear(instanceId);
|
|
241
262
|
return existed;
|
|
242
263
|
}
|
|
264
|
+
const worldItems = notifyAfter(createWorldItemStore({
|
|
265
|
+
spawnEntity: (position) => entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }),
|
|
266
|
+
despawnEntity,
|
|
267
|
+
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
268
|
+
}), ["spawn", "take"], signal.notify);
|
|
269
|
+
function spawnWorldItem(input) {
|
|
270
|
+
const record = worldItems.spawn(input);
|
|
271
|
+
events.emit("worldItem.dropped", {
|
|
272
|
+
instanceId: record.instanceId,
|
|
273
|
+
itemId: record.itemId,
|
|
274
|
+
rarity: record.rarity,
|
|
275
|
+
count: record.count,
|
|
276
|
+
position: [input.position[0], input.position[1], input.position[2]],
|
|
277
|
+
...(record.source !== undefined ? { source: record.source } : {}),
|
|
278
|
+
});
|
|
279
|
+
return record;
|
|
280
|
+
}
|
|
281
|
+
function pickupWorldItem(instanceId, userId) {
|
|
282
|
+
const record = worldItems.take(instanceId);
|
|
283
|
+
if (record === null)
|
|
284
|
+
return { status: "rejected", reason: "not-found" };
|
|
285
|
+
loot.grantToPlayer(userId, [{ item: record.itemId, count: record.count }], "worldItem.pickup");
|
|
286
|
+
events.emit("worldItem.picked_up", {
|
|
287
|
+
instanceId,
|
|
288
|
+
userId,
|
|
289
|
+
itemId: record.itemId,
|
|
290
|
+
rarity: record.rarity,
|
|
291
|
+
count: record.count,
|
|
292
|
+
});
|
|
293
|
+
return { status: "ok", record };
|
|
294
|
+
}
|
|
243
295
|
const death = createDeathSystem({
|
|
244
296
|
resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
|
|
245
297
|
resolveIdentity(instanceId) {
|
|
@@ -266,7 +318,10 @@ export function createGameContext(options) {
|
|
|
266
318
|
getStat: weapon.getStat,
|
|
267
319
|
spatial: combatSpatial,
|
|
268
320
|
onLethal(instanceId, lethalCtx) {
|
|
269
|
-
const
|
|
321
|
+
const dyingEntity = entities.get(instanceId);
|
|
322
|
+
const catalogId = dyingEntity?.name;
|
|
323
|
+
const position = dyingEntity?.position;
|
|
324
|
+
const normalizedOnDeath = normalizeOnDeath(catalogEntry(instanceId)?.onDeath);
|
|
270
325
|
const reason = deathReasonFromEffect({
|
|
271
326
|
...lethalCtx,
|
|
272
327
|
userIdOf: (id) => (id === player.userId ? player.userId : undefined),
|
|
@@ -276,7 +331,23 @@ export function createGameContext(options) {
|
|
|
276
331
|
resolution.drops.length > 0 &&
|
|
277
332
|
reason.kind === "player_kill" &&
|
|
278
333
|
reason.killerUserId === player.userId) {
|
|
279
|
-
|
|
334
|
+
if (normalizedOnDeath.dropMode === "world" && position !== undefined) {
|
|
335
|
+
const resolved = resolveDeathDrops(resolution.drops, {
|
|
336
|
+
mode: "world",
|
|
337
|
+
origin: position,
|
|
338
|
+
resolveRarity: (itemId) => content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
|
|
339
|
+
resolveBaseType: (itemId) => content.itemById?.(itemId)?.baseType ?? itemId,
|
|
340
|
+
scatter: normalizedOnDeath.scatter,
|
|
341
|
+
...(catalogId !== undefined ? { source: catalogId } : {}),
|
|
342
|
+
});
|
|
343
|
+
for (const spawn of resolved.worldSpawns)
|
|
344
|
+
spawnWorldItem(spawn);
|
|
345
|
+
if (resolved.grants.length > 0)
|
|
346
|
+
loot.grantToPlayer(player.userId, resolved.grants, catalogId);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
loot.grantToPlayer(player.userId, resolution.drops, catalogId);
|
|
350
|
+
}
|
|
280
351
|
}
|
|
281
352
|
},
|
|
282
353
|
}), ["applyEffect"], signal.notify);
|
|
@@ -295,8 +366,76 @@ export function createGameContext(options) {
|
|
|
295
366
|
event.instanceId = input.instanceId;
|
|
296
367
|
if (input.amount !== undefined)
|
|
297
368
|
event.amount = input.amount;
|
|
369
|
+
if (input.hitType !== undefined)
|
|
370
|
+
event.hitType = input.hitType;
|
|
371
|
+
if (input.element !== undefined)
|
|
372
|
+
event.element = input.element;
|
|
373
|
+
if (input.crit !== undefined)
|
|
374
|
+
event.crit = input.crit;
|
|
375
|
+
if (input.scale !== undefined)
|
|
376
|
+
event.scale = input.scale;
|
|
298
377
|
events.emit("entity.floatText", event);
|
|
299
378
|
}
|
|
379
|
+
let telegraphSeq = 0;
|
|
380
|
+
function fireTelegraph(input) {
|
|
381
|
+
const id = telegraphSeq++;
|
|
382
|
+
const telegraphEvent = {
|
|
383
|
+
id,
|
|
384
|
+
shape: input.shape,
|
|
385
|
+
position: [input.at[0], input.at[1], input.at[2]],
|
|
386
|
+
windupMs: input.windupMs,
|
|
387
|
+
kind: input.kind ?? "danger",
|
|
388
|
+
};
|
|
389
|
+
if (input.dir !== undefined)
|
|
390
|
+
telegraphEvent.dir = input.dir;
|
|
391
|
+
events.emit("combat.telegraph", telegraphEvent);
|
|
392
|
+
const bound = input.effect;
|
|
393
|
+
if (bound === undefined)
|
|
394
|
+
return () => { };
|
|
395
|
+
const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
|
|
396
|
+
if (input.dir !== undefined)
|
|
397
|
+
config.dir = input.dir;
|
|
398
|
+
return time.after(input.windupMs / 1000, () => {
|
|
399
|
+
const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
|
|
400
|
+
for (const target of targets) {
|
|
401
|
+
applyEffectAndFloat({
|
|
402
|
+
from: input.from,
|
|
403
|
+
to: target.id,
|
|
404
|
+
effect: bound.effect,
|
|
405
|
+
...(bound.via === undefined ? {} : { via: bound.via }),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function applyHitReaction(input) {
|
|
411
|
+
const attacker = entities.get(input.from);
|
|
412
|
+
const target = entities.get(input.to);
|
|
413
|
+
if (target === null)
|
|
414
|
+
return null;
|
|
415
|
+
const attackerPos = attacker?.position ?? target.position;
|
|
416
|
+
const reaction = resolveHitReaction(input.config, {
|
|
417
|
+
attackerPos,
|
|
418
|
+
targetPos: target.position,
|
|
419
|
+
...(input.power === undefined ? {} : { power: input.power }),
|
|
420
|
+
});
|
|
421
|
+
entities.setPose(input.to, {
|
|
422
|
+
position: [
|
|
423
|
+
target.position[0] + reaction.impulse[0],
|
|
424
|
+
target.position[1] + reaction.impulse[1],
|
|
425
|
+
target.position[2] + reaction.impulse[2],
|
|
426
|
+
],
|
|
427
|
+
rotationY: target.rotationY,
|
|
428
|
+
});
|
|
429
|
+
const reactionEvent = {
|
|
430
|
+
instanceId: input.to,
|
|
431
|
+
position: [target.position[0], target.position[1], target.position[2]],
|
|
432
|
+
hitstopMs: reaction.hitstopMs,
|
|
433
|
+
};
|
|
434
|
+
if (reaction.shake !== null)
|
|
435
|
+
reactionEvent.shake = reaction.shake;
|
|
436
|
+
events.emit("combat.hitReaction", reactionEvent);
|
|
437
|
+
return reaction;
|
|
438
|
+
}
|
|
300
439
|
function applyEffectAndFloat(input) {
|
|
301
440
|
const positionsBefore = new Map();
|
|
302
441
|
for (const entity of entities.list())
|
|
@@ -357,6 +496,8 @@ export function createGameContext(options) {
|
|
|
357
496
|
list: entities.list,
|
|
358
497
|
stats: entityStats,
|
|
359
498
|
floatText: emitFloatText,
|
|
499
|
+
telegraph: fireTelegraph,
|
|
500
|
+
hitReaction: applyHitReaction,
|
|
360
501
|
setTarget: targeting.setTarget,
|
|
361
502
|
getTarget: targeting.getTarget,
|
|
362
503
|
cycleTarget: targeting.cycleTarget,
|
|
@@ -371,6 +512,14 @@ export function createGameContext(options) {
|
|
|
371
512
|
hasLineOfSight: spatial.hasLineOfSight,
|
|
372
513
|
queryArc: spatial.queryArc,
|
|
373
514
|
moveToward: spatial.moveToward,
|
|
515
|
+
form: forms,
|
|
516
|
+
},
|
|
517
|
+
worldItem: {
|
|
518
|
+
spawn: spawnWorldItem,
|
|
519
|
+
get: worldItems.get,
|
|
520
|
+
list: worldItems.list,
|
|
521
|
+
nearestInRadius: worldItems.nearestInRadius,
|
|
522
|
+
pickup: pickupWorldItem,
|
|
374
523
|
},
|
|
375
524
|
},
|
|
376
525
|
game: {
|
|
@@ -406,6 +555,7 @@ export function createGameContext(options) {
|
|
|
406
555
|
unlocks,
|
|
407
556
|
economy,
|
|
408
557
|
leaderboard,
|
|
558
|
+
roster,
|
|
409
559
|
},
|
|
410
560
|
player: {
|
|
411
561
|
userId: player.userId,
|
|
@@ -415,6 +565,8 @@ export function createGameContext(options) {
|
|
|
415
565
|
loadout: loadouts,
|
|
416
566
|
applyLoadout: loadouts.applyLoadout,
|
|
417
567
|
movement: pose,
|
|
568
|
+
possession,
|
|
569
|
+
cosmetics,
|
|
418
570
|
},
|
|
419
571
|
item: {
|
|
420
572
|
use: {
|
|
@@ -429,6 +581,7 @@ export function createGameContext(options) {
|
|
|
429
581
|
},
|
|
430
582
|
weapon,
|
|
431
583
|
},
|
|
584
|
+
time,
|
|
432
585
|
subscribe: signal.subscribe,
|
|
433
586
|
version: signal.version,
|
|
434
587
|
};
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { LeaderboardScope } from "../game/leaderboard.js";
|
|
2
|
+
import type { NormalizedScenarioReset } from "./persistenceScope.js";
|
|
2
3
|
import type { SaveConfig } from "./save.js";
|
|
3
4
|
import type { GameRuntimeSnapshot, RuntimeChunkRow, RuntimePlayerRow, RuntimeServerRow } from "./snapshot.js";
|
|
4
5
|
export type GameServerStatus = "open" | "running" | "closed";
|
|
6
|
+
export type SessionVisibility = "public" | "private";
|
|
7
|
+
export type SessionAttributes = {
|
|
8
|
+
label?: string;
|
|
9
|
+
mode?: string;
|
|
10
|
+
visibility?: SessionVisibility;
|
|
11
|
+
joinCode?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
};
|
|
5
14
|
export type GameServerRecord = {
|
|
6
15
|
serverId: string;
|
|
7
16
|
gameId: string;
|
|
8
17
|
status: GameServerStatus;
|
|
9
18
|
mode?: string;
|
|
10
19
|
modeConfig?: unknown;
|
|
20
|
+
label?: string;
|
|
21
|
+
visibility?: SessionVisibility;
|
|
22
|
+
joinCode?: string;
|
|
23
|
+
tags?: string[];
|
|
11
24
|
memberUserIds: string[];
|
|
12
25
|
slotsPerServer: number;
|
|
13
26
|
save: SaveConfig;
|
|
@@ -58,11 +71,16 @@ export type ServerListing = {
|
|
|
58
71
|
memberCount: number;
|
|
59
72
|
slotsPerServer: number;
|
|
60
73
|
mode?: string;
|
|
74
|
+
label?: string;
|
|
75
|
+
visibility?: SessionVisibility;
|
|
76
|
+
joinCode?: string;
|
|
77
|
+
tags?: string[];
|
|
61
78
|
updatedAt: number;
|
|
62
79
|
};
|
|
63
80
|
export declare function toServerListing(record: GameServerRecord): ServerListing;
|
|
64
81
|
export type HostPersistence = {
|
|
65
82
|
savePlan?: (plan: ServerPersistPlan) => Promise<void>;
|
|
83
|
+
resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>;
|
|
66
84
|
loadServer: (serverId: string) => Promise<GameServerRecord | null>;
|
|
67
85
|
saveServer: (record: GameServerRecord) => Promise<void>;
|
|
68
86
|
listServers: (gameId: string) => Promise<GameServerRecord[]>;
|
|
@@ -42,6 +42,10 @@ export function toServerListing(record) {
|
|
|
42
42
|
memberCount: record.memberUserIds.length,
|
|
43
43
|
slotsPerServer: record.slotsPerServer,
|
|
44
44
|
mode: record.mode,
|
|
45
|
+
label: record.label,
|
|
46
|
+
visibility: record.visibility,
|
|
47
|
+
joinCode: record.joinCode,
|
|
48
|
+
tags: record.tags,
|
|
45
49
|
updatedAt: record.updatedAt,
|
|
46
50
|
};
|
|
47
51
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PlayerProfileRecord } from "./hostPersistence.js";
|
|
2
|
+
import type { RuntimePlayerRow } from "./snapshot.js";
|
|
3
|
+
export type PersistenceScope = "run" | "meta";
|
|
4
|
+
export interface ScopeSchema {
|
|
5
|
+
run: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ScopedState<T = Record<string, unknown>> {
|
|
8
|
+
meta: Partial<T>;
|
|
9
|
+
run: Partial<T>;
|
|
10
|
+
}
|
|
11
|
+
export declare function partitionScopes<T extends Record<string, unknown>>(state: T, schema: ScopeSchema): ScopedState<T>;
|
|
12
|
+
export declare function resetRun<T extends Record<string, unknown>>(scoped: ScopedState<T>): ScopedState<T>;
|
|
13
|
+
export declare function mergeScopes<T extends Record<string, unknown>>(scoped: ScopedState<T>): Partial<T>;
|
|
14
|
+
export declare function clearRunFields(player: RuntimePlayerRow, runFields: readonly string[]): RuntimePlayerRow;
|
|
15
|
+
export declare function applyRunReset(profile: PlayerProfileRecord, runFields: readonly string[], now: number): PlayerProfileRecord;
|
|
16
|
+
export interface ScenarioReset {
|
|
17
|
+
gameId: string;
|
|
18
|
+
serverId?: string;
|
|
19
|
+
wipeChunks?: boolean;
|
|
20
|
+
wipeServerSession?: boolean;
|
|
21
|
+
resetPlayers?: PersistenceScope | "none";
|
|
22
|
+
runFields?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface NormalizedScenarioReset {
|
|
25
|
+
gameId: string;
|
|
26
|
+
serverId: string | null;
|
|
27
|
+
wipeChunks: boolean;
|
|
28
|
+
wipeServerSession: boolean;
|
|
29
|
+
resetPlayers: "run" | "none";
|
|
30
|
+
runFields: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
export declare function planScenarioReset(reset: ScenarioReset): NormalizedScenarioReset;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function partitionScopes(state, schema) {
|
|
2
|
+
const runKeys = new Set(schema.run);
|
|
3
|
+
const meta = {};
|
|
4
|
+
const run = {};
|
|
5
|
+
for (const key of Object.keys(state)) {
|
|
6
|
+
if (runKeys.has(key))
|
|
7
|
+
run[key] = state[key];
|
|
8
|
+
else
|
|
9
|
+
meta[key] = state[key];
|
|
10
|
+
}
|
|
11
|
+
return { meta, run };
|
|
12
|
+
}
|
|
13
|
+
export function resetRun(scoped) {
|
|
14
|
+
return { meta: scoped.meta, run: {} };
|
|
15
|
+
}
|
|
16
|
+
export function mergeScopes(scoped) {
|
|
17
|
+
return { ...scoped.meta, ...scoped.run };
|
|
18
|
+
}
|
|
19
|
+
const RUN_FIELD_DEFAULTS = {
|
|
20
|
+
inventories: {},
|
|
21
|
+
economy: {},
|
|
22
|
+
unlocks: [],
|
|
23
|
+
quests: undefined,
|
|
24
|
+
social: undefined,
|
|
25
|
+
leaderboard: {},
|
|
26
|
+
session: {},
|
|
27
|
+
};
|
|
28
|
+
export function clearRunFields(player, runFields) {
|
|
29
|
+
const next = { ...player };
|
|
30
|
+
for (const field of runFields) {
|
|
31
|
+
if (field === "userId")
|
|
32
|
+
continue;
|
|
33
|
+
const key = field;
|
|
34
|
+
if (key in RUN_FIELD_DEFAULTS) {
|
|
35
|
+
const template = RUN_FIELD_DEFAULTS[key];
|
|
36
|
+
if (template === undefined)
|
|
37
|
+
delete next[field];
|
|
38
|
+
else
|
|
39
|
+
next[field] = Array.isArray(template) ? [...template] : { ...template };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
delete next[field];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
export function applyRunReset(profile, runFields, now) {
|
|
48
|
+
return {
|
|
49
|
+
...profile,
|
|
50
|
+
playerState: clearRunFields(profile.playerState, runFields),
|
|
51
|
+
revision: profile.revision + 1,
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function planScenarioReset(reset) {
|
|
56
|
+
return {
|
|
57
|
+
gameId: reset.gameId,
|
|
58
|
+
serverId: reset.serverId ?? null,
|
|
59
|
+
wipeChunks: reset.wipeChunks ?? true,
|
|
60
|
+
wipeServerSession: reset.wipeServerSession ?? true,
|
|
61
|
+
resetPlayers: reset.resetPlayers === "run" ? "run" : "none",
|
|
62
|
+
runFields: reset.runFields ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
export interface ModelDims {
|
|
2
|
+
footprint: {
|
|
3
|
+
w: number;
|
|
4
|
+
d: number;
|
|
5
|
+
};
|
|
6
|
+
center: {
|
|
7
|
+
x: number;
|
|
8
|
+
z: number;
|
|
9
|
+
};
|
|
10
|
+
minY: number;
|
|
11
|
+
}
|
|
1
12
|
export interface ModelAssetRef {
|
|
2
13
|
url: string;
|
|
14
|
+
/** Measured at asset reindex: horizontal footprint, footprint center, and lowest Y in model space (pre-scale). Lets the shell auto-center and ground-snap corner-pivot kit models. */
|
|
15
|
+
dims?: ModelDims;
|
|
3
16
|
}
|
|
4
17
|
export interface AssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef> {
|
|
5
18
|
register(id: string, asset: TMeta): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type AutoTargetPolicy = "nearest" | "farthest" | "random" | "strongest" | "weakest" | "first" | "last";
|
|
2
|
+
export interface AutoTargetDeps {
|
|
3
|
+
candidates: (fromId: string) => readonly string[];
|
|
4
|
+
distance: (fromId: string, toId: string) => number | null;
|
|
5
|
+
strength?: (toId: string) => number;
|
|
6
|
+
progress?: (toId: string) => number;
|
|
7
|
+
rng?: () => number;
|
|
8
|
+
}
|
|
9
|
+
export declare function selectAutoTarget(policy: AutoTargetPolicy, fromId: string, deps: AutoTargetDeps): string | null;
|
|
10
|
+
export interface AutoTargeter {
|
|
11
|
+
policy(): AutoTargetPolicy;
|
|
12
|
+
setPolicy(policy: AutoTargetPolicy): void;
|
|
13
|
+
pick(fromId: string): string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function createAutoTargeter(policy: AutoTargetPolicy, deps: AutoTargetDeps): AutoTargeter;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function metric(policy, fromId, toId, deps) {
|
|
2
|
+
switch (policy) {
|
|
3
|
+
case "nearest":
|
|
4
|
+
case "farthest":
|
|
5
|
+
return deps.distance(fromId, toId);
|
|
6
|
+
case "strongest":
|
|
7
|
+
case "weakest":
|
|
8
|
+
return deps.strength?.(toId) ?? 0;
|
|
9
|
+
case "first":
|
|
10
|
+
case "last":
|
|
11
|
+
return deps.progress?.(toId) ?? 0;
|
|
12
|
+
case "random":
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function prefersHigher(policy) {
|
|
17
|
+
return policy === "farthest" || policy === "strongest" || policy === "first";
|
|
18
|
+
}
|
|
19
|
+
export function selectAutoTarget(policy, fromId, deps) {
|
|
20
|
+
const candidates = deps.candidates(fromId).filter((id) => id !== fromId);
|
|
21
|
+
if (candidates.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
if (policy === "random") {
|
|
24
|
+
const rng = deps.rng ?? Math.random;
|
|
25
|
+
return candidates[Math.min(candidates.length - 1, Math.floor(rng() * candidates.length))];
|
|
26
|
+
}
|
|
27
|
+
const higher = prefersHigher(policy);
|
|
28
|
+
let best = null;
|
|
29
|
+
let bestScore = higher ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
30
|
+
for (const id of candidates) {
|
|
31
|
+
const score = metric(policy, fromId, id, deps);
|
|
32
|
+
if (score === null)
|
|
33
|
+
continue;
|
|
34
|
+
if (best === null || (higher ? score > bestScore : score < bestScore)) {
|
|
35
|
+
best = id;
|
|
36
|
+
bestScore = score;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best;
|
|
40
|
+
}
|
|
41
|
+
export function createAutoTargeter(policy, deps) {
|
|
42
|
+
let current = policy;
|
|
43
|
+
return {
|
|
44
|
+
policy() {
|
|
45
|
+
return current;
|
|
46
|
+
},
|
|
47
|
+
setPolicy(next) {
|
|
48
|
+
current = next;
|
|
49
|
+
},
|
|
50
|
+
pick(fromId) {
|
|
51
|
+
return selectAutoTarget(current, fromId, deps);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
|
|
2
|
+
import type { Waypoint } from "../nav/pathFollow.js";
|
|
2
3
|
export interface WanderBehavior {
|
|
3
4
|
kind: "wander";
|
|
4
5
|
radius: number;
|
|
5
6
|
}
|
|
7
|
+
export interface PatrolBehavior {
|
|
8
|
+
kind: "patrol";
|
|
9
|
+
waypoints: readonly Waypoint[];
|
|
10
|
+
speed: number;
|
|
11
|
+
loop: boolean;
|
|
12
|
+
}
|
|
6
13
|
export interface PromptableBehavior {
|
|
7
14
|
kind: "promptable";
|
|
8
15
|
prompt: ProximityPrompt;
|
|
@@ -10,10 +17,15 @@ export interface PromptableBehavior {
|
|
|
10
17
|
export interface PlayerBehavior {
|
|
11
18
|
kind: "player";
|
|
12
19
|
}
|
|
13
|
-
export type BehaviorDescriptor = WanderBehavior | PromptableBehavior | PlayerBehavior;
|
|
20
|
+
export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior;
|
|
14
21
|
export declare function wander({ radius }: {
|
|
15
22
|
radius: number;
|
|
16
23
|
}): WanderBehavior;
|
|
24
|
+
export declare function patrol({ waypoints, speed, loop, }: {
|
|
25
|
+
waypoints: readonly Waypoint[];
|
|
26
|
+
speed: number;
|
|
27
|
+
loop?: boolean;
|
|
28
|
+
}): PatrolBehavior;
|
|
17
29
|
export declare function promptable(prompt: ProximityPrompt): PromptableBehavior;
|
|
18
30
|
export declare function talkable(dialogueId: string): PromptableBehavior;
|
|
19
31
|
export declare function player(): PlayerBehavior;
|
package/dist/scene/behaviors.js
CHANGED
|
@@ -2,6 +2,9 @@ import { command, keybind, proximityPrompt, } from "../interaction/proximityProm
|
|
|
2
2
|
export function wander({ radius }) {
|
|
3
3
|
return { kind: "wander", radius };
|
|
4
4
|
}
|
|
5
|
+
export function patrol({ waypoints, speed, loop = true, }) {
|
|
6
|
+
return { kind: "patrol", waypoints, speed, loop };
|
|
7
|
+
}
|
|
5
8
|
export function promptable(prompt) {
|
|
6
9
|
return { kind: "promptable", prompt };
|
|
7
10
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CaptureCheckInput {
|
|
2
|
+
hpFraction: number;
|
|
3
|
+
catchPower: number;
|
|
4
|
+
difficulty?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function captureChance(input: CaptureCheckInput): number;
|
|
7
|
+
export declare function rollCapture(input: CaptureCheckInput, rng?: () => number): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function clamp01(value) {
|
|
2
|
+
return Math.min(1, Math.max(0, value));
|
|
3
|
+
}
|
|
4
|
+
export function captureChance(input) {
|
|
5
|
+
const difficulty = input.difficulty ?? 1;
|
|
6
|
+
const hpFactor = 1 - clamp01(input.hpFraction) * 0.6;
|
|
7
|
+
return clamp01((input.catchPower * hpFactor) / Math.max(difficulty, 0.0001));
|
|
8
|
+
}
|
|
9
|
+
export function rollCapture(input, rng = Math.random) {
|
|
10
|
+
return rng() < captureChance(input);
|
|
11
|
+
}
|
|
@@ -16,11 +16,15 @@ export interface SceneEntity<TMeta = undefined> {
|
|
|
16
16
|
rotationY: number;
|
|
17
17
|
rotationX: number;
|
|
18
18
|
rotationZ: number;
|
|
19
|
+
/** World units per second, derived from consecutive setPose calls that carry `dt`. Zero until the entity moves under a dt. */
|
|
20
|
+
velocity: EntityPosition;
|
|
19
21
|
role: EntityRole;
|
|
20
22
|
movement: EntityMovement;
|
|
21
23
|
behaviors: readonly BehaviorDescriptor[];
|
|
22
24
|
meta: TMeta;
|
|
23
25
|
}
|
|
26
|
+
/** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
|
|
27
|
+
export declare function groundSpeed(entity: SceneEntity<unknown>): number;
|
|
24
28
|
export interface SpawnOptions<TMeta = undefined> {
|
|
25
29
|
id?: string;
|
|
26
30
|
position?: SpawnPositionInput;
|
|
@@ -37,11 +41,13 @@ export interface EntityPose {
|
|
|
37
41
|
rotationY?: number;
|
|
38
42
|
rotationX?: number;
|
|
39
43
|
rotationZ?: number;
|
|
44
|
+
/** Seconds since the previous pose; when > 0 the store derives `velocity` from the position delta. Omit for teleports (velocity unchanged). */
|
|
45
|
+
dt?: number;
|
|
40
46
|
}
|
|
41
47
|
export interface EntityStore<TMeta = undefined> {
|
|
42
48
|
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
43
49
|
despawn(id: string): boolean;
|
|
44
|
-
update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
|
|
50
|
+
update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "name" | "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
|
|
45
51
|
setPose(id: string, pose: EntityPose): boolean;
|
|
46
52
|
get(id: string): SceneEntity<TMeta> | null;
|
|
47
53
|
list(): readonly SceneEntity<TMeta>[];
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
/** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
|
|
3
|
+
export function groundSpeed(entity) {
|
|
4
|
+
return Math.hypot(entity.velocity[0], entity.velocity[2]);
|
|
5
|
+
}
|
|
2
6
|
function toEntityPosition(position) {
|
|
3
7
|
if (position === undefined)
|
|
4
8
|
return [0, 0, 0];
|
|
@@ -31,6 +35,7 @@ export function createEntityStore() {
|
|
|
31
35
|
rotationY: options.rotationY ?? 0,
|
|
32
36
|
rotationX: options.rotationX ?? 0,
|
|
33
37
|
rotationZ: options.rotationZ ?? 0,
|
|
38
|
+
velocity: [0, 0, 0],
|
|
34
39
|
role: options.role ?? "prop",
|
|
35
40
|
movement: options.movement ?? {},
|
|
36
41
|
behaviors: options.behaviors ?? [],
|
|
@@ -54,9 +59,18 @@ export function createEntityStore() {
|
|
|
54
59
|
const current = store.get(id);
|
|
55
60
|
if (!current)
|
|
56
61
|
return false;
|
|
62
|
+
const position = toEntityPosition(pose.position);
|
|
63
|
+
const velocity = pose.dt !== undefined && pose.dt > 0
|
|
64
|
+
? [
|
|
65
|
+
(position[0] - current.position[0]) / pose.dt,
|
|
66
|
+
(position[1] - current.position[1]) / pose.dt,
|
|
67
|
+
(position[2] - current.position[2]) / pose.dt,
|
|
68
|
+
]
|
|
69
|
+
: current.velocity;
|
|
57
70
|
store.set(id, {
|
|
58
71
|
...current,
|
|
59
|
-
position
|
|
72
|
+
position,
|
|
73
|
+
velocity,
|
|
60
74
|
rotationY: pose.rotationY ?? current.rotationY,
|
|
61
75
|
rotationX: pose.rotationX ?? current.rotationX,
|
|
62
76
|
rotationZ: pose.rotationZ ?? current.rotationZ,
|