@jgengine/core 0.11.0 → 0.13.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 +52 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +5 -2
- package/dist/combat.js +4 -1
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +60 -0
- package/dist/runtime/context/scene.js +329 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +7 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +79 -0
- package/dist/scene/colliders.js +170 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +19 -15
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { balance as walletBalance, charge as walletCharge, createEmptyWallet, grant as walletGrant, isOverdrawn as walletIsOverdrawn, } from "../../economy/wallet.js";
|
|
2
|
+
import { createLoadouts } from "../../game/loadout.js";
|
|
3
|
+
import { createInventorySet, putItem, } from "../../inventory/inventoryModel.js";
|
|
4
|
+
import { createPoseState } from "../../movement/poseState.js";
|
|
5
|
+
import { createPossession } from "../../scene/possession.js";
|
|
6
|
+
import { setStatValue } from "../../scene/entityStats.js";
|
|
7
|
+
import { createStats } from "../../stats/statModifiers.js";
|
|
8
|
+
import { notifyAfter } from "../../store/changeSignal.js";
|
|
9
|
+
import { createMotionIntents } from "../motionIntents.js";
|
|
10
|
+
/** @internal */
|
|
11
|
+
export function createPlayerSubsystem(d) {
|
|
12
|
+
const { signalNotify, entities, events, catalogEntry, ensureInstanceStats, feature } = d;
|
|
13
|
+
const playerStatsByUser = new Map();
|
|
14
|
+
function playerStatsFor(userId) {
|
|
15
|
+
let stats = playerStatsByUser.get(userId);
|
|
16
|
+
if (stats === undefined) {
|
|
17
|
+
stats = createStats({});
|
|
18
|
+
playerStatsByUser.set(userId, stats);
|
|
19
|
+
}
|
|
20
|
+
return stats;
|
|
21
|
+
}
|
|
22
|
+
const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
|
|
23
|
+
const possession = notifyAfter(createPossession({ entities, events }), ["possess", "own", "disown"], signalNotify);
|
|
24
|
+
const inventoryDeclarations = d.inventoryDeclarations;
|
|
25
|
+
const inventoryIds = Object.keys(inventoryDeclarations);
|
|
26
|
+
const layouts = {};
|
|
27
|
+
for (const [inventoryId, declaration] of Object.entries(inventoryDeclarations)) {
|
|
28
|
+
layouts[inventoryId] = { slots: declaration.slots, accepts: declaration.accepts };
|
|
29
|
+
}
|
|
30
|
+
const traits = Object.values(inventoryDeclarations).find((declaration) => declaration.traits !== undefined)
|
|
31
|
+
?.traits ?? { stackLimit: () => Number.POSITIVE_INFINITY };
|
|
32
|
+
const inventoryByUser = new Map();
|
|
33
|
+
function inventoryFor(userId) {
|
|
34
|
+
let set = inventoryByUser.get(userId);
|
|
35
|
+
if (set === undefined) {
|
|
36
|
+
set = notifyAfter(createInventorySet(layouts, traits), ["put", "take", "move", "replaceState"], signalNotify);
|
|
37
|
+
inventoryByUser.set(userId, set);
|
|
38
|
+
}
|
|
39
|
+
return set;
|
|
40
|
+
}
|
|
41
|
+
const wallets = new Map();
|
|
42
|
+
const walletOf = (userId) => wallets.get(userId) ?? createEmptyWallet();
|
|
43
|
+
const economy = {
|
|
44
|
+
balance: (userId, currencyId) => walletBalance(walletOf(userId), currencyId),
|
|
45
|
+
grant(userId, currencyId, amount) {
|
|
46
|
+
wallets.set(userId, walletGrant(walletOf(userId), currencyId, amount));
|
|
47
|
+
signalNotify();
|
|
48
|
+
},
|
|
49
|
+
charge(userId, currencyId, amount, options) {
|
|
50
|
+
const result = walletCharge(walletOf(userId), currencyId, amount, options);
|
|
51
|
+
if (result.status === "rejected")
|
|
52
|
+
return { reason: result.reason };
|
|
53
|
+
wallets.set(userId, result.state);
|
|
54
|
+
signalNotify();
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
isOverdrawn: (userId, currencyId) => walletIsOverdrawn(walletOf(userId), currencyId),
|
|
58
|
+
};
|
|
59
|
+
function putIntoAnyInventory(userId, itemId, count) {
|
|
60
|
+
const inventory = inventoryFor(userId);
|
|
61
|
+
for (const inventoryId of inventoryIds) {
|
|
62
|
+
if (inventory.put(inventoryId, itemId, count).status === "ok")
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function seedUserPool(userId, statId, pool) {
|
|
67
|
+
const map = ensureInstanceStats(userId);
|
|
68
|
+
const next = setStatValue(map, statId, pool);
|
|
69
|
+
map[statId] = next[statId];
|
|
70
|
+
}
|
|
71
|
+
const loadouts = notifyAfter(createLoadouts({
|
|
72
|
+
inventory: {
|
|
73
|
+
begin(userId) {
|
|
74
|
+
const inventory = inventoryFor(userId);
|
|
75
|
+
const staged = new Map();
|
|
76
|
+
return {
|
|
77
|
+
put(inventoryId, itemId, count, slot) {
|
|
78
|
+
const layout = layouts[inventoryId];
|
|
79
|
+
if (layout === undefined)
|
|
80
|
+
return { reason: `unknown inventory "${inventoryId}"` };
|
|
81
|
+
const state = staged.get(inventoryId) ?? inventory.state(inventoryId);
|
|
82
|
+
const result = putItem(state, layout, traits, itemId, count, slot === undefined ? undefined : { slot });
|
|
83
|
+
if (result.status === "rejected")
|
|
84
|
+
return { reason: result.reason };
|
|
85
|
+
staged.set(inventoryId, result.state);
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
commit() {
|
|
89
|
+
for (const [inventoryId, state] of staged)
|
|
90
|
+
inventory.replaceState(inventoryId, state);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
stats: { seed: seedUserPool },
|
|
96
|
+
economy: { grant: economy.grant },
|
|
97
|
+
unlocks: { grant: (userId, unlockId) => feature("unlocks")?.grant(userId, unlockId) },
|
|
98
|
+
}), ["applyLoadout"], signalNotify);
|
|
99
|
+
const motionByUser = new Map();
|
|
100
|
+
function motionFor(userId) {
|
|
101
|
+
let queue = motionByUser.get(userId);
|
|
102
|
+
if (queue === undefined) {
|
|
103
|
+
queue = createMotionIntents();
|
|
104
|
+
motionByUser.set(userId, queue);
|
|
105
|
+
}
|
|
106
|
+
return queue;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
inventoryIds,
|
|
110
|
+
inventoryByUser,
|
|
111
|
+
inventoryFor,
|
|
112
|
+
layouts,
|
|
113
|
+
wallets,
|
|
114
|
+
walletOf,
|
|
115
|
+
economy,
|
|
116
|
+
putIntoAnyInventory,
|
|
117
|
+
playerStatsFor,
|
|
118
|
+
pose,
|
|
119
|
+
possession,
|
|
120
|
+
motionByUser,
|
|
121
|
+
motionFor,
|
|
122
|
+
loadouts,
|
|
123
|
+
seedUserPool,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CombatSpatialDeps } from "../../combat/effects.js";
|
|
2
|
+
import type { GameEvents } from "../../game/events.js";
|
|
3
|
+
import { type BodyBind } from "../../scene/bodyBind.js";
|
|
4
|
+
import { type EntityStatsApi, type StatValueMap } from "../../scene/entityStats.js";
|
|
5
|
+
import { type EntityPosition, type EntityStore, type SceneEntity, type SpawnOptions } from "../../scene/entityStore.js";
|
|
6
|
+
import { type Forms } from "../../scene/form.js";
|
|
7
|
+
import { type EntityColliderSet, type MeasuredBounds } from "../../scene/colliders.js";
|
|
8
|
+
import { type ObjectStore } from "../../scene/objectStore.js";
|
|
9
|
+
import { type PaintLayer } from "../../scene/paintLayer.js";
|
|
10
|
+
import { type SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
11
|
+
import { type SpatialApi } from "../../scene/spatial.js";
|
|
12
|
+
import { type Targeting } from "../../scene/targeting.js";
|
|
13
|
+
import type { TerrainField } from "../../world/terrain.js";
|
|
14
|
+
import type { SimClock } from "../../time/simClock.js";
|
|
15
|
+
import type { GameContextContent, GameContextEntityEntry, GameContextModels, GameContextObjectEntry, MoveTowardCommitOptions, SceneObjectContext } from "../gameContext.js";
|
|
16
|
+
/** @internal Live scene stores + spatial/collider/raycast wiring handed in by `createGameContext`. */
|
|
17
|
+
export interface SceneSubsystemDeps {
|
|
18
|
+
content: GameContextContent;
|
|
19
|
+
signalNotify: () => void;
|
|
20
|
+
ground: TerrainField;
|
|
21
|
+
events: GameEvents;
|
|
22
|
+
/** Full sim clock — forms need `after` for timed shapeshift reversion. */
|
|
23
|
+
time: SimClock;
|
|
24
|
+
occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
|
|
25
|
+
models?: GameContextModels;
|
|
26
|
+
}
|
|
27
|
+
/** @internal Always-on scene surface: entities, objects, colliders, spatial, raycast, targeting. */
|
|
28
|
+
export interface SceneSubsystem {
|
|
29
|
+
entities: EntityStore;
|
|
30
|
+
objects: ObjectStore;
|
|
31
|
+
statsByInstance: Map<string, StatValueMap>;
|
|
32
|
+
entityStats: EntityStatsApi;
|
|
33
|
+
ensureInstanceStats: (instanceId: string) => StatValueMap;
|
|
34
|
+
catalogEntry: (instanceId: string) => GameContextEntityEntry | null | undefined;
|
|
35
|
+
catalogObject: (instanceId: string) => GameContextObjectEntry | null | undefined;
|
|
36
|
+
spatial: SpatialApi;
|
|
37
|
+
combatSpatial: CombatSpatialDeps;
|
|
38
|
+
sceneRaycast: SceneRaycastApi;
|
|
39
|
+
targeting: Targeting;
|
|
40
|
+
entityColliders: Map<string, EntityColliderSet>;
|
|
41
|
+
objectColliders: Map<string, EntityColliderSet>;
|
|
42
|
+
entityCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
43
|
+
objectCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
44
|
+
reportEntityBounds: (kind: string, bounds: MeasuredBounds | null) => boolean;
|
|
45
|
+
entityVisualScaleOf: (instanceId: string) => number;
|
|
46
|
+
forms: Forms;
|
|
47
|
+
paintLayer: PaintLayer;
|
|
48
|
+
spawnEntity: (name: string, spawnOptions?: SpawnOptions) => string;
|
|
49
|
+
despawnEntity: (instanceId: string) => boolean;
|
|
50
|
+
moveEntityTowardCommit: (instanceId: string, target: EntityPosition | string, options: MoveTowardCommitOptions) => EntityPosition | null;
|
|
51
|
+
resetAllToSpawn: (filter?: (entity: SceneEntity) => boolean) => number;
|
|
52
|
+
bind: (key: string) => BodyBind;
|
|
53
|
+
sceneObjects: SceneObjectContext;
|
|
54
|
+
/** Install post-spawn side effects (death.revive) after combat is constructed. */
|
|
55
|
+
setOnAfterSpawn: (fn: (instanceId: string) => void) => void;
|
|
56
|
+
/** Install post-despawn side effects (pose.clear) after player is constructed. */
|
|
57
|
+
setOnAfterDespawn: (fn: (instanceId: string) => void) => void;
|
|
58
|
+
}
|
|
59
|
+
/** @internal */
|
|
60
|
+
export declare function createSceneSubsystem(d: SceneSubsystemDeps): SceneSubsystem;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { createBodyBind } from "../../scene/bodyBind.js";
|
|
2
|
+
import { createEntityStatsApi, seedStatValues, } from "../../scene/entityStats.js";
|
|
3
|
+
import { createEntityStore, } from "../../scene/entityStore.js";
|
|
4
|
+
import { createForms } from "../../scene/form.js";
|
|
5
|
+
import { fittedEntityColliders, fittedObjectColliders, measuredEntityColliders, measuredObjectColliders, scaledEntityColliders, scaledObjectColliders, } from "../../scene/colliders.js";
|
|
6
|
+
import { raycastObjects, raycastObjectsAll } from "../../scene/objectQuery.js";
|
|
7
|
+
import { createObjectStore, objectVisualScale } from "../../scene/objectStore.js";
|
|
8
|
+
import { createPaintLayer } from "../../scene/paintLayer.js";
|
|
9
|
+
import { createSelectionSet } from "../../scene/selection.js";
|
|
10
|
+
import { createSceneRaycast, } from "../../scene/sceneRaycast.js";
|
|
11
|
+
import { createSpatialApi } from "../../scene/spatial.js";
|
|
12
|
+
import { createTargeting } from "../../scene/targeting.js";
|
|
13
|
+
import { notifyAfter } from "../../store/changeSignal.js";
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function createSceneSubsystem(d) {
|
|
16
|
+
const { content, signalNotify, ground, events, time, occluder } = d;
|
|
17
|
+
const entities = createEntityStore();
|
|
18
|
+
const objects = createObjectStore();
|
|
19
|
+
entities.subscribe(signalNotify);
|
|
20
|
+
objects.subscribe(signalNotify);
|
|
21
|
+
const statsByInstance = new Map();
|
|
22
|
+
const entityStats = notifyAfter(createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)), ["set", "delta"], signalNotify);
|
|
23
|
+
function ensureInstanceStats(instanceId) {
|
|
24
|
+
let map = statsByInstance.get(instanceId);
|
|
25
|
+
if (map === undefined) {
|
|
26
|
+
map = {};
|
|
27
|
+
statsByInstance.set(instanceId, map);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
function catalogEntry(instanceId) {
|
|
32
|
+
const entity = entities.get(instanceId);
|
|
33
|
+
return entity === null ? undefined : content.entityById?.(entity.name);
|
|
34
|
+
}
|
|
35
|
+
function catalogObject(instanceId) {
|
|
36
|
+
const object = objects.get(instanceId);
|
|
37
|
+
return object === null ? undefined : content.objectById?.(object.catalogId);
|
|
38
|
+
}
|
|
39
|
+
let spatialGeneration = 0;
|
|
40
|
+
const candidateIds = [];
|
|
41
|
+
let candidateIdsDirty = true;
|
|
42
|
+
function refreshCandidateIds() {
|
|
43
|
+
if (!candidateIdsDirty)
|
|
44
|
+
return candidateIds;
|
|
45
|
+
candidateIds.length = 0;
|
|
46
|
+
for (const entity of entities.list())
|
|
47
|
+
candidateIds.push(entity.id);
|
|
48
|
+
candidateIdsDirty = false;
|
|
49
|
+
return candidateIds;
|
|
50
|
+
}
|
|
51
|
+
const spatial = createSpatialApi({
|
|
52
|
+
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
53
|
+
candidates: refreshCandidateIds,
|
|
54
|
+
grid: { cellSize: 8 },
|
|
55
|
+
getVersion: () => spatialGeneration,
|
|
56
|
+
...(occluder !== undefined ? { occluder } : {}),
|
|
57
|
+
});
|
|
58
|
+
entities.subscribe(() => {
|
|
59
|
+
candidateIdsDirty = true;
|
|
60
|
+
spatialGeneration += 1;
|
|
61
|
+
spatial.invalidate();
|
|
62
|
+
});
|
|
63
|
+
const entityColliders = new Map();
|
|
64
|
+
const objectColliders = new Map();
|
|
65
|
+
const fittedEntityByKind = new Map();
|
|
66
|
+
const fittedObjectByCatalogId = new Map();
|
|
67
|
+
function fittedEntitySetFor(kind) {
|
|
68
|
+
const cached = fittedEntityByKind.get(kind);
|
|
69
|
+
if (cached !== undefined)
|
|
70
|
+
return cached;
|
|
71
|
+
const model = d.models?.entity?.(kind);
|
|
72
|
+
const fitted = model === null || model === undefined ? null : fittedEntityColliders(model);
|
|
73
|
+
fittedEntityByKind.set(kind, fitted);
|
|
74
|
+
return fitted;
|
|
75
|
+
}
|
|
76
|
+
function fittedObjectSetFor(catalogId) {
|
|
77
|
+
const cached = fittedObjectByCatalogId.get(catalogId);
|
|
78
|
+
if (cached !== undefined)
|
|
79
|
+
return cached;
|
|
80
|
+
const model = d.models?.object?.(catalogId);
|
|
81
|
+
const fitted = model === null || model === undefined ? null : fittedObjectColliders(model);
|
|
82
|
+
fittedObjectByCatalogId.set(catalogId, fitted);
|
|
83
|
+
return fitted;
|
|
84
|
+
}
|
|
85
|
+
const measuredEntityByKind = new Map();
|
|
86
|
+
const measuredObjectByCatalogId = new Map();
|
|
87
|
+
function reportEntityBounds(kind, bounds) {
|
|
88
|
+
if (bounds === null) {
|
|
89
|
+
const existed = measuredEntityByKind.delete(kind);
|
|
90
|
+
if (existed)
|
|
91
|
+
signalNotify();
|
|
92
|
+
return existed;
|
|
93
|
+
}
|
|
94
|
+
const measured = measuredEntityColliders(bounds);
|
|
95
|
+
if (measured === null)
|
|
96
|
+
return false;
|
|
97
|
+
measuredEntityByKind.set(kind, measured);
|
|
98
|
+
signalNotify();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function reportObjectBounds(catalogId, bounds) {
|
|
102
|
+
if (bounds === null) {
|
|
103
|
+
const existed = measuredObjectByCatalogId.delete(catalogId);
|
|
104
|
+
if (existed)
|
|
105
|
+
signalNotify();
|
|
106
|
+
return existed;
|
|
107
|
+
}
|
|
108
|
+
const measured = measuredObjectColliders(bounds);
|
|
109
|
+
if (measured === null)
|
|
110
|
+
return false;
|
|
111
|
+
measuredObjectByCatalogId.set(catalogId, measured);
|
|
112
|
+
signalNotify();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function entityCollidersOf(instanceId) {
|
|
116
|
+
const override = entityColliders.get(instanceId);
|
|
117
|
+
if (override !== undefined)
|
|
118
|
+
return override;
|
|
119
|
+
const entry = catalogEntry(instanceId);
|
|
120
|
+
if (entry?.colliders !== undefined)
|
|
121
|
+
return entry.colliders;
|
|
122
|
+
const kind = entities.get(instanceId)?.name;
|
|
123
|
+
if (kind !== undefined) {
|
|
124
|
+
const fitted = fittedEntitySetFor(kind);
|
|
125
|
+
if (fitted !== null)
|
|
126
|
+
return fitted;
|
|
127
|
+
const measured = measuredEntityByKind.get(kind);
|
|
128
|
+
if (measured !== undefined)
|
|
129
|
+
return measured;
|
|
130
|
+
}
|
|
131
|
+
const scale = entry?.scale;
|
|
132
|
+
if (scale !== undefined && scale !== 1)
|
|
133
|
+
return scaledEntityColliders(scale);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function entityVisualScaleOf(instanceId) {
|
|
137
|
+
return catalogEntry(instanceId)?.scale ?? 1;
|
|
138
|
+
}
|
|
139
|
+
function objectCollidersOf(instanceId) {
|
|
140
|
+
const override = objectColliders.get(instanceId);
|
|
141
|
+
if (override !== undefined)
|
|
142
|
+
return override;
|
|
143
|
+
const catalog = catalogObject(instanceId);
|
|
144
|
+
if (catalog?.colliders !== undefined)
|
|
145
|
+
return catalog.colliders;
|
|
146
|
+
if (catalog?.halfExtents !== undefined)
|
|
147
|
+
return null;
|
|
148
|
+
const object = objects.get(instanceId);
|
|
149
|
+
if (object === null)
|
|
150
|
+
return null;
|
|
151
|
+
const fitted = fittedObjectSetFor(object.catalogId);
|
|
152
|
+
if (fitted !== null)
|
|
153
|
+
return fitted;
|
|
154
|
+
const measured = measuredObjectByCatalogId.get(object.catalogId);
|
|
155
|
+
if (measured !== undefined)
|
|
156
|
+
return measured;
|
|
157
|
+
if (object.visual?.scale === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
160
|
+
}
|
|
161
|
+
const targeting = notifyAfter(createTargeting({
|
|
162
|
+
candidates: () => [...refreshCandidateIds()],
|
|
163
|
+
classify(_fromId, toId) {
|
|
164
|
+
const role = catalogEntry(toId)?.role;
|
|
165
|
+
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
166
|
+
},
|
|
167
|
+
distance: (fromId, toId) => spatial.distance(fromId, toId),
|
|
168
|
+
}), ["setTarget", "cycleTarget"], signalNotify);
|
|
169
|
+
const combatSpatial = {
|
|
170
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
171
|
+
hasLineOfSight: (from, to) => {
|
|
172
|
+
if (typeof from === "string")
|
|
173
|
+
return spatial.hasLineOfSight(from, to);
|
|
174
|
+
const toPos = entities.get(to)?.position;
|
|
175
|
+
if (toPos === undefined)
|
|
176
|
+
return false;
|
|
177
|
+
if (occluder === undefined)
|
|
178
|
+
return true;
|
|
179
|
+
return !occluder(from, toPos);
|
|
180
|
+
},
|
|
181
|
+
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
182
|
+
};
|
|
183
|
+
const sceneRaycast = createSceneRaycast({
|
|
184
|
+
entities: {
|
|
185
|
+
list: () => entities.list().map((entity) => ({
|
|
186
|
+
id: entity.id,
|
|
187
|
+
position: entity.position,
|
|
188
|
+
rotationY: entity.rotationY,
|
|
189
|
+
name: entity.name,
|
|
190
|
+
})),
|
|
191
|
+
collidersOf: entityCollidersOf,
|
|
192
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
193
|
+
get: (id) => {
|
|
194
|
+
const entity = entities.get(id);
|
|
195
|
+
if (entity === null)
|
|
196
|
+
return null;
|
|
197
|
+
return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
objects: {
|
|
201
|
+
list: () => objects.list(),
|
|
202
|
+
inBox: (min, max) => objects.inBox(min, max),
|
|
203
|
+
collidersOf: objectCollidersOf,
|
|
204
|
+
halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
|
|
205
|
+
},
|
|
206
|
+
terrain: ground,
|
|
207
|
+
});
|
|
208
|
+
const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signalNotify);
|
|
209
|
+
const paintLayer = notifyAfter(createPaintLayer(), ["paint", "clear"], signalNotify);
|
|
210
|
+
let onAfterSpawn = () => { };
|
|
211
|
+
let onAfterDespawn = () => { };
|
|
212
|
+
function spawnEntity(name, spawnOptions) {
|
|
213
|
+
const entry = content.entityById?.(name);
|
|
214
|
+
const walkSpeed = spawnOptions?.movement?.walkSpeed ?? entry?.movement?.walkSpeed;
|
|
215
|
+
const options = walkSpeed === undefined
|
|
216
|
+
? spawnOptions
|
|
217
|
+
: { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
|
|
218
|
+
const instanceId = entities.spawn(name, options);
|
|
219
|
+
onAfterSpawn(instanceId);
|
|
220
|
+
statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedStatValues(entry.stats));
|
|
221
|
+
return instanceId;
|
|
222
|
+
}
|
|
223
|
+
function despawnEntity(instanceId) {
|
|
224
|
+
const existed = entities.despawn(instanceId);
|
|
225
|
+
statsByInstance.delete(instanceId);
|
|
226
|
+
targeting.clearAll(instanceId);
|
|
227
|
+
entityColliders.delete(instanceId);
|
|
228
|
+
onAfterDespawn(instanceId);
|
|
229
|
+
return existed;
|
|
230
|
+
}
|
|
231
|
+
function moveEntityTowardCommit(instanceId, target, options) {
|
|
232
|
+
const next = spatial.moveToward(instanceId, target, options);
|
|
233
|
+
if (next === null)
|
|
234
|
+
return null;
|
|
235
|
+
const current = entities.get(instanceId);
|
|
236
|
+
if (current === null)
|
|
237
|
+
return null;
|
|
238
|
+
let rotationY = current.rotationY;
|
|
239
|
+
if (options.face === true) {
|
|
240
|
+
const dx = next[0] - current.position[0];
|
|
241
|
+
const dz = next[2] - current.position[2];
|
|
242
|
+
if (Math.hypot(dx, dz) > 1e-9)
|
|
243
|
+
rotationY = Math.atan2(dx, dz);
|
|
244
|
+
}
|
|
245
|
+
entities.setPose(instanceId, { position: next, rotationY });
|
|
246
|
+
return next;
|
|
247
|
+
}
|
|
248
|
+
function resetAllToSpawn(filter) {
|
|
249
|
+
let count = 0;
|
|
250
|
+
for (const entity of entities.list()) {
|
|
251
|
+
if (filter !== undefined && !filter(entity))
|
|
252
|
+
continue;
|
|
253
|
+
if (entities.resetToSpawn(entity.id))
|
|
254
|
+
count += 1;
|
|
255
|
+
}
|
|
256
|
+
return count;
|
|
257
|
+
}
|
|
258
|
+
const bodyBinds = new Map();
|
|
259
|
+
function bind(key) {
|
|
260
|
+
const existing = bodyBinds.get(key);
|
|
261
|
+
if (existing !== undefined)
|
|
262
|
+
return existing;
|
|
263
|
+
const created = createBodyBind({
|
|
264
|
+
has: (id) => entities.get(id) !== null,
|
|
265
|
+
spawn: spawnEntity,
|
|
266
|
+
despawn: despawnEntity,
|
|
267
|
+
setPose: entities.setPose,
|
|
268
|
+
update: entities.update,
|
|
269
|
+
});
|
|
270
|
+
bodyBinds.set(key, created);
|
|
271
|
+
return created;
|
|
272
|
+
}
|
|
273
|
+
const objectSelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signalNotify);
|
|
274
|
+
const sceneObjects = {
|
|
275
|
+
...objects,
|
|
276
|
+
remove(instanceId) {
|
|
277
|
+
const existed = objects.remove(instanceId);
|
|
278
|
+
if (existed)
|
|
279
|
+
objectSelection.remove(instanceId);
|
|
280
|
+
return existed;
|
|
281
|
+
},
|
|
282
|
+
catalog: (instanceId) => catalogObject(instanceId) ?? null,
|
|
283
|
+
raycast: (input) => raycastObjects(objects, input),
|
|
284
|
+
raycastAll: (input) => raycastObjectsAll(objects, input),
|
|
285
|
+
setColliders(instanceId, colliders) {
|
|
286
|
+
if (colliders === null)
|
|
287
|
+
objectColliders.delete(instanceId);
|
|
288
|
+
else
|
|
289
|
+
objectColliders.set(instanceId, colliders);
|
|
290
|
+
signalNotify();
|
|
291
|
+
},
|
|
292
|
+
collidersOf: objectCollidersOf,
|
|
293
|
+
reportBounds: reportObjectBounds,
|
|
294
|
+
selection: objectSelection,
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
entities,
|
|
298
|
+
objects,
|
|
299
|
+
statsByInstance,
|
|
300
|
+
entityStats,
|
|
301
|
+
ensureInstanceStats,
|
|
302
|
+
catalogEntry,
|
|
303
|
+
catalogObject,
|
|
304
|
+
spatial,
|
|
305
|
+
combatSpatial,
|
|
306
|
+
sceneRaycast,
|
|
307
|
+
targeting,
|
|
308
|
+
entityColliders,
|
|
309
|
+
objectColliders,
|
|
310
|
+
entityCollidersOf,
|
|
311
|
+
objectCollidersOf,
|
|
312
|
+
reportEntityBounds,
|
|
313
|
+
entityVisualScaleOf,
|
|
314
|
+
forms,
|
|
315
|
+
paintLayer,
|
|
316
|
+
spawnEntity,
|
|
317
|
+
despawnEntity,
|
|
318
|
+
moveEntityTowardCommit,
|
|
319
|
+
resetAllToSpawn,
|
|
320
|
+
bind,
|
|
321
|
+
sceneObjects,
|
|
322
|
+
setOnAfterSpawn(fn) {
|
|
323
|
+
onAfterSpawn = fn;
|
|
324
|
+
},
|
|
325
|
+
setOnAfterDespawn(fn) {
|
|
326
|
+
onAfterDespawn = fn;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow wire codecs for {@link SnapshotModule.decode} — reject obviously wrong shapes so hydrate
|
|
3
|
+
* never receives bare casts of garbage. Fail soft (return `null`); deeper field validation stays with
|
|
4
|
+
* each subsystem's hydrate.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
/** @internal Reject non-arrays; return a typed array or null. */
|
|
8
|
+
export declare function decodeArray<T = unknown>(raw: unknown): T[] | null;
|
|
9
|
+
/** @internal Reject non-plain objects; return a typed record or null. */
|
|
10
|
+
export declare function decodeRecord<T = unknown>(raw: unknown): Record<string, T> | null;
|
|
11
|
+
/** @internal Reject non-[string, T][] entry lists; return entries or null. */
|
|
12
|
+
export declare function decodeEntries<T = unknown>(raw: unknown): readonly (readonly [string, T])[] | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow wire codecs for {@link SnapshotModule.decode} — reject obviously wrong shapes so hydrate
|
|
3
|
+
* never receives bare casts of garbage. Fail soft (return `null`); deeper field validation stays with
|
|
4
|
+
* each subsystem's hydrate.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
/** @internal Reject non-arrays; return a typed array or null. */
|
|
8
|
+
export function decodeArray(raw) {
|
|
9
|
+
return Array.isArray(raw) ? raw : null;
|
|
10
|
+
}
|
|
11
|
+
/** @internal Reject non-plain objects; return a typed record or null. */
|
|
12
|
+
export function decodeRecord(raw) {
|
|
13
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
14
|
+
return null;
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
/** @internal Reject non-[string, T][] entry lists; return entries or null. */
|
|
18
|
+
export function decodeEntries(raw) {
|
|
19
|
+
if (!Array.isArray(raw))
|
|
20
|
+
return null;
|
|
21
|
+
for (const entry of raw) {
|
|
22
|
+
if (!Array.isArray(entry) || entry.length < 2 || typeof entry[0] !== "string")
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hydrateEntityStats, snapshotEntityStats, } from "../../scene/entityStats.js";
|
|
2
|
+
import { decodeArray, decodeEntries, decodeRecord } from "../context/snapshotCodecs.js";
|
|
2
3
|
/** @internal */
|
|
3
4
|
export const baselineDescriptors = [
|
|
4
5
|
{
|
|
@@ -7,6 +8,7 @@ export const baselineDescriptors = [
|
|
|
7
8
|
replicate: {
|
|
8
9
|
key: "entities",
|
|
9
10
|
snapshot: () => d.entities.snapshot(),
|
|
11
|
+
decode: (raw) => decodeArray(raw),
|
|
10
12
|
hydrate: (data) => d.entities.hydrate(data),
|
|
11
13
|
},
|
|
12
14
|
}),
|
|
@@ -17,6 +19,7 @@ export const baselineDescriptors = [
|
|
|
17
19
|
replicate: {
|
|
18
20
|
key: "stats",
|
|
19
21
|
snapshot: () => snapshotEntityStats(d.statsByInstance),
|
|
22
|
+
decode: (raw) => decodeRecord(raw),
|
|
20
23
|
hydrate: (data) => hydrateEntityStats(d.statsByInstance, data),
|
|
21
24
|
},
|
|
22
25
|
}),
|
|
@@ -27,6 +30,7 @@ export const baselineDescriptors = [
|
|
|
27
30
|
replicate: {
|
|
28
31
|
key: "store",
|
|
29
32
|
snapshot: () => d.store.snapshot(),
|
|
33
|
+
decode: (raw) => decodeEntries(raw),
|
|
30
34
|
hydrate: (data) => d.store.hydrate(data),
|
|
31
35
|
},
|
|
32
36
|
}),
|
|
@@ -37,6 +41,7 @@ export const baselineDescriptors = [
|
|
|
37
41
|
replicate: {
|
|
38
42
|
key: "feed",
|
|
39
43
|
snapshot: () => d.feed.snapshot(),
|
|
44
|
+
decode: (raw) => decodeRecord(raw),
|
|
40
45
|
hydrate: (data) => d.feed.hydrate(data),
|
|
41
46
|
},
|
|
42
47
|
}),
|
|
@@ -56,8 +61,10 @@ export const baselineDescriptors = [
|
|
|
56
61
|
}
|
|
57
62
|
return byUser;
|
|
58
63
|
},
|
|
64
|
+
decode: (raw) => decodeRecord(raw),
|
|
59
65
|
hydrate: (data) => {
|
|
60
|
-
|
|
66
|
+
const byUser = data;
|
|
67
|
+
for (const [userId, states] of Object.entries(byUser)) {
|
|
61
68
|
const set = d.inventoryFor(userId);
|
|
62
69
|
for (const [inventoryId, state] of Object.entries(states))
|
|
63
70
|
set.replaceState(inventoryId, state);
|
|
@@ -72,9 +79,11 @@ export const baselineDescriptors = [
|
|
|
72
79
|
save: {
|
|
73
80
|
key: "economy",
|
|
74
81
|
snapshot: () => Object.fromEntries(d.wallets),
|
|
82
|
+
decode: (raw) => decodeRecord(raw),
|
|
75
83
|
hydrate: (data) => {
|
|
84
|
+
const wallets = data;
|
|
76
85
|
d.wallets.clear();
|
|
77
|
-
for (const [userId, state] of Object.entries(
|
|
86
|
+
for (const [userId, state] of Object.entries(wallets)) {
|
|
78
87
|
d.wallets.set(userId, state);
|
|
79
88
|
}
|
|
80
89
|
d.signalNotify();
|
|
@@ -88,6 +97,10 @@ export const baselineDescriptors = [
|
|
|
88
97
|
save: {
|
|
89
98
|
key: "time",
|
|
90
99
|
snapshot: () => d.time.snapshot(),
|
|
100
|
+
decode: (raw) => {
|
|
101
|
+
const rec = decodeRecord(raw);
|
|
102
|
+
return rec === null ? null : rec;
|
|
103
|
+
},
|
|
91
104
|
hydrate: (data) => d.time.hydrate(data),
|
|
92
105
|
},
|
|
93
106
|
}),
|
|
@@ -98,6 +111,10 @@ export const baselineDescriptors = [
|
|
|
98
111
|
save: {
|
|
99
112
|
key: "pose",
|
|
100
113
|
snapshot: () => d.pose.snapshotAll(),
|
|
114
|
+
decode: (raw) => {
|
|
115
|
+
const rec = decodeRecord(raw);
|
|
116
|
+
return rec === null ? null : rec;
|
|
117
|
+
},
|
|
101
118
|
hydrate: (data) => d.pose.hydrateAll(data),
|
|
102
119
|
},
|
|
103
120
|
}),
|
|
@@ -108,6 +125,10 @@ export const baselineDescriptors = [
|
|
|
108
125
|
save: {
|
|
109
126
|
key: "possession",
|
|
110
127
|
snapshot: () => d.possession.snapshotAll(),
|
|
128
|
+
decode: (raw) => {
|
|
129
|
+
const rec = decodeRecord(raw);
|
|
130
|
+
return rec === null ? null : rec;
|
|
131
|
+
},
|
|
111
132
|
hydrate: (data) => d.possession.hydrateAll(data),
|
|
112
133
|
},
|
|
113
134
|
}),
|
|
@@ -123,8 +144,10 @@ export const baselineDescriptors = [
|
|
|
123
144
|
out[userId] = queue.snapshot();
|
|
124
145
|
return out;
|
|
125
146
|
},
|
|
147
|
+
decode: (raw) => decodeRecord(raw),
|
|
126
148
|
hydrate: (data) => {
|
|
127
|
-
|
|
149
|
+
const batches = data;
|
|
150
|
+
for (const [userId, batch] of Object.entries(batches)) {
|
|
128
151
|
d.motionFor(userId).hydrate(batch);
|
|
129
152
|
}
|
|
130
153
|
},
|
|
@@ -63,6 +63,11 @@ export interface FeatureBuild {
|
|
|
63
63
|
* {@link FeatureDeps}, and whether it replicates or persists. `createGameContext` iterates the
|
|
64
64
|
* descriptor list instead of hand-wiring each `features.x ? create : undefined` branch — the seam a
|
|
65
65
|
* new feature extends through.
|
|
66
|
+
*
|
|
67
|
+
* Feature activation has **one path**: `definition.features` after `defineGame` has already
|
|
68
|
+
* OR-merged explicit `features: { quest: true }` sugar with system-implied flags via
|
|
69
|
+
* `mergeSystemFeatures`. Descriptors only call `enabled(features)` against that final map — never
|
|
70
|
+
* re-check systems or a second flag source.
|
|
66
71
|
* @internal
|
|
67
72
|
*/
|
|
68
73
|
export interface FeatureDescriptor {
|