@jgengine/core 0.11.0 → 0.12.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 +26 -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.d.ts +1 -0
- package/dist/combat.js +1 -0
- 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/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/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/playableGame.d.ts +34 -435
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +19 -0
- 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/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +24 -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 +130 -0
- 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 +59 -0
- package/dist/runtime/context/scene.js +291 -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 +88 -501
- package/dist/runtime/gameContextTypes.d.ts +467 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +4 -2
- package/dist/runtime/headlessRunner.js +1 -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 +5 -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 +58 -0
- package/dist/scene/colliders.js +108 -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 +10 -14
- 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/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +59 -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/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/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/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/features.d.ts +18 -3
- package/dist/world/features.js +10 -1
- 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/pathNetwork.d.ts +159 -0
- package/dist/world/pathNetwork.js +684 -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/scatter.js +2 -20
- package/dist/world/streets.d.ts +29 -0
- package/dist/world/streets.js +55 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +45 -0
- package/dist/world/terrain.js +78 -0
- 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 +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +5 -1
- package/skills/jgengine-combat/api.md +36 -0
- package/skills/jgengine-combat/capabilities.md +44 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/reference.md +12 -2
- package/skills/jgengine-gameplay/api.md +152 -26
- package/skills/jgengine-gameplay/capabilities.md +78 -2
- package/skills/jgengine-gameplay/reference-systems.md +1 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -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,291 @@
|
|
|
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, 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
|
+
function entityCollidersOf(instanceId) {
|
|
86
|
+
const override = entityColliders.get(instanceId);
|
|
87
|
+
if (override !== undefined)
|
|
88
|
+
return override;
|
|
89
|
+
const entry = catalogEntry(instanceId);
|
|
90
|
+
if (entry?.colliders !== undefined)
|
|
91
|
+
return entry.colliders;
|
|
92
|
+
const kind = entities.get(instanceId)?.name;
|
|
93
|
+
if (kind !== undefined) {
|
|
94
|
+
const fitted = fittedEntitySetFor(kind);
|
|
95
|
+
if (fitted !== null)
|
|
96
|
+
return fitted;
|
|
97
|
+
}
|
|
98
|
+
const scale = entry?.scale;
|
|
99
|
+
if (scale !== undefined && scale !== 1)
|
|
100
|
+
return scaledEntityColliders(scale);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function entityVisualScaleOf(instanceId) {
|
|
104
|
+
return catalogEntry(instanceId)?.scale ?? 1;
|
|
105
|
+
}
|
|
106
|
+
function objectCollidersOf(instanceId) {
|
|
107
|
+
const override = objectColliders.get(instanceId);
|
|
108
|
+
if (override !== undefined)
|
|
109
|
+
return override;
|
|
110
|
+
const catalog = catalogObject(instanceId);
|
|
111
|
+
if (catalog?.colliders !== undefined)
|
|
112
|
+
return catalog.colliders;
|
|
113
|
+
if (catalog?.halfExtents !== undefined)
|
|
114
|
+
return null;
|
|
115
|
+
const object = objects.get(instanceId);
|
|
116
|
+
if (object === null)
|
|
117
|
+
return null;
|
|
118
|
+
const fitted = fittedObjectSetFor(object.catalogId);
|
|
119
|
+
if (fitted !== null)
|
|
120
|
+
return fitted;
|
|
121
|
+
if (object.visual?.scale === undefined)
|
|
122
|
+
return null;
|
|
123
|
+
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
124
|
+
}
|
|
125
|
+
const targeting = notifyAfter(createTargeting({
|
|
126
|
+
candidates: () => [...refreshCandidateIds()],
|
|
127
|
+
classify(_fromId, toId) {
|
|
128
|
+
const role = catalogEntry(toId)?.role;
|
|
129
|
+
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
130
|
+
},
|
|
131
|
+
distance: (fromId, toId) => spatial.distance(fromId, toId),
|
|
132
|
+
}), ["setTarget", "cycleTarget"], signalNotify);
|
|
133
|
+
const combatSpatial = {
|
|
134
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
135
|
+
hasLineOfSight: (from, to) => {
|
|
136
|
+
if (typeof from === "string")
|
|
137
|
+
return spatial.hasLineOfSight(from, to);
|
|
138
|
+
const toPos = entities.get(to)?.position;
|
|
139
|
+
if (toPos === undefined)
|
|
140
|
+
return false;
|
|
141
|
+
if (occluder === undefined)
|
|
142
|
+
return true;
|
|
143
|
+
return !occluder(from, toPos);
|
|
144
|
+
},
|
|
145
|
+
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
146
|
+
};
|
|
147
|
+
const sceneRaycast = createSceneRaycast({
|
|
148
|
+
entities: {
|
|
149
|
+
list: () => entities.list().map((entity) => ({
|
|
150
|
+
id: entity.id,
|
|
151
|
+
position: entity.position,
|
|
152
|
+
rotationY: entity.rotationY,
|
|
153
|
+
name: entity.name,
|
|
154
|
+
})),
|
|
155
|
+
collidersOf: entityCollidersOf,
|
|
156
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
157
|
+
get: (id) => {
|
|
158
|
+
const entity = entities.get(id);
|
|
159
|
+
if (entity === null)
|
|
160
|
+
return null;
|
|
161
|
+
return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
objects: {
|
|
165
|
+
list: () => objects.list(),
|
|
166
|
+
inBox: (min, max) => objects.inBox(min, max),
|
|
167
|
+
collidersOf: objectCollidersOf,
|
|
168
|
+
halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
|
|
169
|
+
},
|
|
170
|
+
terrain: ground,
|
|
171
|
+
});
|
|
172
|
+
const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signalNotify);
|
|
173
|
+
const paintLayer = notifyAfter(createPaintLayer(), ["paint", "clear"], signalNotify);
|
|
174
|
+
let onAfterSpawn = () => { };
|
|
175
|
+
let onAfterDespawn = () => { };
|
|
176
|
+
function spawnEntity(name, spawnOptions) {
|
|
177
|
+
const entry = content.entityById?.(name);
|
|
178
|
+
const walkSpeed = spawnOptions?.movement?.walkSpeed ?? entry?.movement?.walkSpeed;
|
|
179
|
+
const options = walkSpeed === undefined
|
|
180
|
+
? spawnOptions
|
|
181
|
+
: { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
|
|
182
|
+
const instanceId = entities.spawn(name, options);
|
|
183
|
+
onAfterSpawn(instanceId);
|
|
184
|
+
statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedStatValues(entry.stats));
|
|
185
|
+
return instanceId;
|
|
186
|
+
}
|
|
187
|
+
function despawnEntity(instanceId) {
|
|
188
|
+
const existed = entities.despawn(instanceId);
|
|
189
|
+
statsByInstance.delete(instanceId);
|
|
190
|
+
targeting.clearAll(instanceId);
|
|
191
|
+
entityColliders.delete(instanceId);
|
|
192
|
+
onAfterDespawn(instanceId);
|
|
193
|
+
return existed;
|
|
194
|
+
}
|
|
195
|
+
function moveEntityTowardCommit(instanceId, target, options) {
|
|
196
|
+
const next = spatial.moveToward(instanceId, target, options);
|
|
197
|
+
if (next === null)
|
|
198
|
+
return null;
|
|
199
|
+
const current = entities.get(instanceId);
|
|
200
|
+
if (current === null)
|
|
201
|
+
return null;
|
|
202
|
+
let rotationY = current.rotationY;
|
|
203
|
+
if (options.face === true) {
|
|
204
|
+
const dx = next[0] - current.position[0];
|
|
205
|
+
const dz = next[2] - current.position[2];
|
|
206
|
+
if (Math.hypot(dx, dz) > 1e-9)
|
|
207
|
+
rotationY = Math.atan2(dx, dz);
|
|
208
|
+
}
|
|
209
|
+
entities.setPose(instanceId, { position: next, rotationY });
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
212
|
+
function resetAllToSpawn(filter) {
|
|
213
|
+
let count = 0;
|
|
214
|
+
for (const entity of entities.list()) {
|
|
215
|
+
if (filter !== undefined && !filter(entity))
|
|
216
|
+
continue;
|
|
217
|
+
if (entities.resetToSpawn(entity.id))
|
|
218
|
+
count += 1;
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
const bodyBinds = new Map();
|
|
223
|
+
function bind(key) {
|
|
224
|
+
const existing = bodyBinds.get(key);
|
|
225
|
+
if (existing !== undefined)
|
|
226
|
+
return existing;
|
|
227
|
+
const created = createBodyBind({
|
|
228
|
+
has: (id) => entities.get(id) !== null,
|
|
229
|
+
spawn: spawnEntity,
|
|
230
|
+
despawn: despawnEntity,
|
|
231
|
+
setPose: entities.setPose,
|
|
232
|
+
update: entities.update,
|
|
233
|
+
});
|
|
234
|
+
bodyBinds.set(key, created);
|
|
235
|
+
return created;
|
|
236
|
+
}
|
|
237
|
+
const objectSelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signalNotify);
|
|
238
|
+
const sceneObjects = {
|
|
239
|
+
...objects,
|
|
240
|
+
remove(instanceId) {
|
|
241
|
+
const existed = objects.remove(instanceId);
|
|
242
|
+
if (existed)
|
|
243
|
+
objectSelection.remove(instanceId);
|
|
244
|
+
return existed;
|
|
245
|
+
},
|
|
246
|
+
catalog: (instanceId) => catalogObject(instanceId) ?? null,
|
|
247
|
+
raycast: (input) => raycastObjects(objects, input),
|
|
248
|
+
raycastAll: (input) => raycastObjectsAll(objects, input),
|
|
249
|
+
setColliders(instanceId, colliders) {
|
|
250
|
+
if (colliders === null)
|
|
251
|
+
objectColliders.delete(instanceId);
|
|
252
|
+
else
|
|
253
|
+
objectColliders.set(instanceId, colliders);
|
|
254
|
+
signalNotify();
|
|
255
|
+
},
|
|
256
|
+
collidersOf: objectCollidersOf,
|
|
257
|
+
selection: objectSelection,
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
entities,
|
|
261
|
+
objects,
|
|
262
|
+
statsByInstance,
|
|
263
|
+
entityStats,
|
|
264
|
+
ensureInstanceStats,
|
|
265
|
+
catalogEntry,
|
|
266
|
+
catalogObject,
|
|
267
|
+
spatial,
|
|
268
|
+
combatSpatial,
|
|
269
|
+
sceneRaycast,
|
|
270
|
+
targeting,
|
|
271
|
+
entityColliders,
|
|
272
|
+
objectColliders,
|
|
273
|
+
entityCollidersOf,
|
|
274
|
+
objectCollidersOf,
|
|
275
|
+
entityVisualScaleOf,
|
|
276
|
+
forms,
|
|
277
|
+
paintLayer,
|
|
278
|
+
spawnEntity,
|
|
279
|
+
despawnEntity,
|
|
280
|
+
moveEntityTowardCommit,
|
|
281
|
+
resetAllToSpawn,
|
|
282
|
+
bind,
|
|
283
|
+
sceneObjects,
|
|
284
|
+
setOnAfterSpawn(fn) {
|
|
285
|
+
onAfterSpawn = fn;
|
|
286
|
+
},
|
|
287
|
+
setOnAfterDespawn(fn) {
|
|
288
|
+
onAfterDespawn = fn;
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -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 {
|
|
@@ -10,6 +10,7 @@ import { createUnlocks } from "../../game/unlocks.js";
|
|
|
10
10
|
import { createRoster } from "../../scene/roster.js";
|
|
11
11
|
import { createConnectedPlayers } from "../../game/connectedPlayers.js";
|
|
12
12
|
import { notifyAfter } from "../../store/changeSignal.js";
|
|
13
|
+
import { decodeArray, decodeRecord } from "../context/snapshotCodecs.js";
|
|
13
14
|
/** @internal */
|
|
14
15
|
export const featureDescriptors = [
|
|
15
16
|
{
|
|
@@ -22,6 +23,7 @@ export const featureDescriptors = [
|
|
|
22
23
|
save: {
|
|
23
24
|
key: "unlocks",
|
|
24
25
|
snapshot: () => unlocks.snapshotAll(),
|
|
26
|
+
decode: (raw) => decodeRecord(raw),
|
|
25
27
|
hydrate: (data) => unlocks.hydrateAll(data),
|
|
26
28
|
},
|
|
27
29
|
};
|
|
@@ -49,6 +51,10 @@ export const featureDescriptors = [
|
|
|
49
51
|
replicate: {
|
|
50
52
|
key: "social",
|
|
51
53
|
snapshot: () => social.snapshot(),
|
|
54
|
+
decode: (rawVal) => {
|
|
55
|
+
const rec = decodeRecord(rawVal);
|
|
56
|
+
return rec === null ? null : rec;
|
|
57
|
+
},
|
|
52
58
|
hydrate: (data) => social.hydrate(data),
|
|
53
59
|
},
|
|
54
60
|
};
|
|
@@ -74,6 +80,10 @@ export const featureDescriptors = [
|
|
|
74
80
|
replicate: {
|
|
75
81
|
key: "chat",
|
|
76
82
|
snapshot: () => chat.snapshot(),
|
|
83
|
+
decode: (rawVal) => {
|
|
84
|
+
const rec = decodeRecord(rawVal);
|
|
85
|
+
return rec === null ? null : rec;
|
|
86
|
+
},
|
|
77
87
|
hydrate: (data) => chat.hydrate(data),
|
|
78
88
|
},
|
|
79
89
|
};
|
|
@@ -89,6 +99,7 @@ export const featureDescriptors = [
|
|
|
89
99
|
replicate: {
|
|
90
100
|
key: "leaderboard",
|
|
91
101
|
snapshot: () => leaderboard.snapshot(),
|
|
102
|
+
decode: (raw) => decodeArray(raw),
|
|
92
103
|
hydrate: (data) => leaderboard.hydrate(data),
|
|
93
104
|
},
|
|
94
105
|
};
|
|
@@ -104,6 +115,7 @@ export const featureDescriptors = [
|
|
|
104
115
|
save: {
|
|
105
116
|
key: "roster",
|
|
106
117
|
snapshot: () => roster.snapshotAll(),
|
|
118
|
+
decode: (raw) => decodeRecord(raw),
|
|
107
119
|
hydrate: (data) => roster.hydrateAll(data),
|
|
108
120
|
},
|
|
109
121
|
};
|
|
@@ -119,6 +131,7 @@ export const featureDescriptors = [
|
|
|
119
131
|
save: {
|
|
120
132
|
key: "cosmetics",
|
|
121
133
|
snapshot: () => cosmetics.snapshotAll(),
|
|
134
|
+
decode: (raw) => decodeRecord(raw),
|
|
122
135
|
hydrate: (data) => cosmetics.hydrateAll(data),
|
|
123
136
|
},
|
|
124
137
|
};
|
|
@@ -193,6 +206,7 @@ export const featureDescriptors = [
|
|
|
193
206
|
save: {
|
|
194
207
|
key: "quest",
|
|
195
208
|
snapshot: () => quest.snapshotAll(),
|
|
209
|
+
decode: (raw) => decodeRecord(raw),
|
|
196
210
|
hydrate: (data) => quest.hydrateAll(data),
|
|
197
211
|
},
|
|
198
212
|
};
|
|
@@ -239,8 +253,10 @@ export const featureDescriptors = [
|
|
|
239
253
|
out[id] = cardPile.state();
|
|
240
254
|
return out;
|
|
241
255
|
},
|
|
256
|
+
decode: (raw) => decodeRecord(raw),
|
|
242
257
|
hydrate: (data) => {
|
|
243
|
-
|
|
258
|
+
const piles = data;
|
|
259
|
+
for (const [id, state] of Object.entries(piles)) {
|
|
244
260
|
d.cardPiles.get(id)?.reset(state);
|
|
245
261
|
}
|
|
246
262
|
},
|
|
@@ -262,8 +278,10 @@ export const featureDescriptors = [
|
|
|
262
278
|
out[id] = turnLoop.capture();
|
|
263
279
|
return out;
|
|
264
280
|
},
|
|
281
|
+
decode: (raw) => decodeRecord(raw),
|
|
265
282
|
hydrate: (data) => {
|
|
266
|
-
|
|
283
|
+
const loops = data;
|
|
284
|
+
for (const [id, state] of Object.entries(loops)) {
|
|
267
285
|
d.turnLoops.get(id)?.restore(state);
|
|
268
286
|
}
|
|
269
287
|
},
|