@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
|
@@ -1,47 +1,15 @@
|
|
|
1
|
-
import {} from "../
|
|
2
|
-
import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath } from "../combat/death.js";
|
|
3
|
-
import { createEffectSystem, } from "../combat/effects.js";
|
|
4
|
-
import { createProjectileSystem } from "../combat/projectiles.js";
|
|
5
|
-
import {} from "../combat/hitReaction.js";
|
|
6
|
-
import {} from "../combat/telegraph.js";
|
|
7
|
-
import { createCommandRegistry, } from "../commands/commandRegistry.js";
|
|
8
|
-
import { balance as walletBalance, charge as walletCharge, createEmptyWallet, grant as walletGrant, isOverdrawn as walletIsOverdrawn, } from "../economy/wallet.js";
|
|
1
|
+
import { createCommandRegistry } from "../commands/commandRegistry.js";
|
|
9
2
|
import {} from "../game/cosmetics.js";
|
|
10
3
|
import { groundFieldFor } from "../world/terrain.js";
|
|
11
4
|
import { createGameEvents } from "../game/events.js";
|
|
12
|
-
import { createVfxInstanceStore } from "../game/vfxInstance.js";
|
|
13
5
|
import { createGameFeed } from "../game/feed.js";
|
|
14
6
|
import { setGamePhase } from "../game/gamePhase.js";
|
|
15
|
-
import {} from "../game/leaderboard.js";
|
|
16
|
-
import { createLoadouts } from "../game/loadout.js";
|
|
17
7
|
import { createLootRegistry, grantDrops } from "../game/lootTable.js";
|
|
18
|
-
import {} from "../game/dialogue.js";
|
|
19
|
-
import {} from "../game/quest.js";
|
|
20
|
-
import { resolveDeathDrops, DEFAULT_RARITY, } from "../game/worldItem.js";
|
|
21
|
-
import {} from "../game/chat.js";
|
|
22
8
|
import { createSocial } from "../game/social.js";
|
|
23
|
-
import {} from "../
|
|
24
|
-
import {} from "../game/unlocks.js";
|
|
25
|
-
import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
|
|
26
|
-
import { createItemUse, } from "../item/use.js";
|
|
9
|
+
import { createItemUse } from "../item/use.js";
|
|
27
10
|
import { createWeaponStats } from "../item/weapon.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { createPaintLayer } from "../scene/paintLayer.js";
|
|
31
|
-
import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
|
|
32
|
-
import { createEntityStore, } from "../scene/entityStore.js";
|
|
33
|
-
import { createForms } from "../scene/form.js";
|
|
34
|
-
import { scaledEntityColliders, scaledObjectColliders } from "../scene/colliders.js";
|
|
35
|
-
import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
|
|
36
|
-
import { createObjectStore, objectVisualScale } from "../scene/objectStore.js";
|
|
37
|
-
import {} from "../scene/roster.js";
|
|
38
|
-
import { createSelectionSet } from "../scene/selection.js";
|
|
39
|
-
import {} from "../game/connectedPlayers.js";
|
|
40
|
-
import { createPossession } from "../scene/possession.js";
|
|
41
|
-
import { createSceneRaycast, } from "../scene/sceneRaycast.js";
|
|
42
|
-
import { createSpatialApi } from "../scene/spatial.js";
|
|
43
|
-
import { createTargeting } from "../scene/targeting.js";
|
|
44
|
-
import { createStats } from "../stats/statModifiers.js";
|
|
11
|
+
import {} from "../scene/entityStats.js";
|
|
12
|
+
import {} from "../scene/entityStore.js";
|
|
45
13
|
import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
|
|
46
14
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
47
15
|
import { applyWorldSnapshot, composeWorldSnapshot, } from "./worldSnapshot.js";
|
|
@@ -50,15 +18,14 @@ import { createRuntimeSave } from "./runtimeSave.js";
|
|
|
50
18
|
import { isOffline } from "./adapter.js";
|
|
51
19
|
import { localSaveBackend, memorySaveBackend } from "../game/saveStore.js";
|
|
52
20
|
import { createSimClock } from "../time/simClock.js";
|
|
53
|
-
import {} from "../turn/turnLoop.js";
|
|
54
|
-
import { RaceState } from "../game/race.js";
|
|
55
21
|
import { createCameraDirector } from "./cameraDirector.js";
|
|
56
22
|
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
57
|
-
import { createMotionIntents } from "./motionIntents.js";
|
|
58
23
|
import { baselineDescriptors } from "./descriptors/baseline.js";
|
|
59
24
|
import { featureDescriptors } from "./descriptors/features.js";
|
|
60
|
-
import {
|
|
25
|
+
import { createCombatSubsystem } from "./context/combat.js";
|
|
61
26
|
import { createContextRegistries } from "./context/registries.js";
|
|
27
|
+
import { createPlayerSubsystem } from "./context/player.js";
|
|
28
|
+
import { createSceneSubsystem } from "./context/scene.js";
|
|
62
29
|
import { createWorldItemContext } from "./context/worldItems.js";
|
|
63
30
|
export function createGameContext(options) {
|
|
64
31
|
const { definition, content, player } = options;
|
|
@@ -69,131 +36,6 @@ export function createGameContext(options) {
|
|
|
69
36
|
const activeUserId = () => actingUserId ?? player.userId;
|
|
70
37
|
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
71
38
|
const ground = groundFieldFor(definition.world);
|
|
72
|
-
const entities = createEntityStore();
|
|
73
|
-
const objects = createObjectStore();
|
|
74
|
-
entities.subscribe(signal.notify);
|
|
75
|
-
objects.subscribe(signal.notify);
|
|
76
|
-
const statsByInstance = new Map();
|
|
77
|
-
const entityStats = notifyAfter(createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)), ["set", "delta"], signal.notify);
|
|
78
|
-
function ensureInstanceStats(instanceId) {
|
|
79
|
-
let map = statsByInstance.get(instanceId);
|
|
80
|
-
if (map === undefined) {
|
|
81
|
-
map = {};
|
|
82
|
-
statsByInstance.set(instanceId, map);
|
|
83
|
-
}
|
|
84
|
-
return map;
|
|
85
|
-
}
|
|
86
|
-
function catalogEntry(instanceId) {
|
|
87
|
-
const entity = entities.get(instanceId);
|
|
88
|
-
return entity === null ? undefined : content.entityById?.(entity.name);
|
|
89
|
-
}
|
|
90
|
-
function catalogObject(instanceId) {
|
|
91
|
-
const object = objects.get(instanceId);
|
|
92
|
-
return object === null ? undefined : content.objectById?.(object.catalogId);
|
|
93
|
-
}
|
|
94
|
-
let spatialGeneration = 0;
|
|
95
|
-
const candidateIds = [];
|
|
96
|
-
let candidateIdsDirty = true;
|
|
97
|
-
function refreshCandidateIds() {
|
|
98
|
-
if (!candidateIdsDirty)
|
|
99
|
-
return candidateIds;
|
|
100
|
-
candidateIds.length = 0;
|
|
101
|
-
for (const entity of entities.list())
|
|
102
|
-
candidateIds.push(entity.id);
|
|
103
|
-
candidateIdsDirty = false;
|
|
104
|
-
return candidateIds;
|
|
105
|
-
}
|
|
106
|
-
const spatial = createSpatialApi({
|
|
107
|
-
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
108
|
-
candidates: refreshCandidateIds,
|
|
109
|
-
grid: { cellSize: 8 },
|
|
110
|
-
getVersion: () => spatialGeneration,
|
|
111
|
-
...(occluder !== undefined ? { occluder } : {}),
|
|
112
|
-
});
|
|
113
|
-
entities.subscribe(() => {
|
|
114
|
-
candidateIdsDirty = true;
|
|
115
|
-
spatialGeneration += 1;
|
|
116
|
-
spatial.invalidate();
|
|
117
|
-
});
|
|
118
|
-
const entityColliders = new Map();
|
|
119
|
-
const objectColliders = new Map();
|
|
120
|
-
function entityCollidersOf(instanceId) {
|
|
121
|
-
const override = entityColliders.get(instanceId);
|
|
122
|
-
if (override !== undefined)
|
|
123
|
-
return override;
|
|
124
|
-
const entry = catalogEntry(instanceId);
|
|
125
|
-
if (entry?.colliders !== undefined)
|
|
126
|
-
return entry.colliders;
|
|
127
|
-
const scale = entry?.scale;
|
|
128
|
-
if (scale !== undefined && scale !== 1)
|
|
129
|
-
return scaledEntityColliders(scale);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
function entityVisualScaleOf(instanceId) {
|
|
133
|
-
return catalogEntry(instanceId)?.scale ?? 1;
|
|
134
|
-
}
|
|
135
|
-
function objectCollidersOf(instanceId) {
|
|
136
|
-
const override = objectColliders.get(instanceId);
|
|
137
|
-
if (override !== undefined)
|
|
138
|
-
return override;
|
|
139
|
-
const catalog = catalogObject(instanceId);
|
|
140
|
-
if (catalog?.colliders !== undefined)
|
|
141
|
-
return catalog.colliders;
|
|
142
|
-
if (catalog?.halfExtents !== undefined)
|
|
143
|
-
return null;
|
|
144
|
-
const object = objects.get(instanceId);
|
|
145
|
-
if (object === null || object.visual?.scale === undefined)
|
|
146
|
-
return null;
|
|
147
|
-
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
148
|
-
}
|
|
149
|
-
const targeting = notifyAfter(createTargeting({
|
|
150
|
-
candidates: () => [...refreshCandidateIds()],
|
|
151
|
-
classify(_fromId, toId) {
|
|
152
|
-
const role = catalogEntry(toId)?.role;
|
|
153
|
-
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
154
|
-
},
|
|
155
|
-
distance: (fromId, toId) => spatial.distance(fromId, toId),
|
|
156
|
-
}), ["setTarget", "cycleTarget"], signal.notify);
|
|
157
|
-
const combatSpatial = {
|
|
158
|
-
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
159
|
-
hasLineOfSight: (from, to) => {
|
|
160
|
-
if (typeof from === "string")
|
|
161
|
-
return spatial.hasLineOfSight(from, to);
|
|
162
|
-
const toPos = entities.get(to)?.position;
|
|
163
|
-
if (toPos === undefined)
|
|
164
|
-
return false;
|
|
165
|
-
if (occluder === undefined)
|
|
166
|
-
return true;
|
|
167
|
-
return !occluder(from, toPos);
|
|
168
|
-
},
|
|
169
|
-
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
170
|
-
};
|
|
171
|
-
const sceneRaycast = createSceneRaycast({
|
|
172
|
-
entities: {
|
|
173
|
-
list: () => entities.list().map((entity) => ({
|
|
174
|
-
id: entity.id,
|
|
175
|
-
position: entity.position,
|
|
176
|
-
rotationY: entity.rotationY,
|
|
177
|
-
name: entity.name,
|
|
178
|
-
})),
|
|
179
|
-
collidersOf: entityCollidersOf,
|
|
180
|
-
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
181
|
-
get: (id) => {
|
|
182
|
-
const entity = entities.get(id);
|
|
183
|
-
if (entity === null)
|
|
184
|
-
return null;
|
|
185
|
-
return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
objects: {
|
|
189
|
-
list: () => objects.list(),
|
|
190
|
-
inBox: (min, max) => objects.inBox(min, max),
|
|
191
|
-
collidersOf: objectCollidersOf,
|
|
192
|
-
halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
|
|
193
|
-
},
|
|
194
|
-
terrain: ground,
|
|
195
|
-
});
|
|
196
|
-
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
197
39
|
const rawEvents = createGameEvents();
|
|
198
40
|
const events = {
|
|
199
41
|
on: rawEvents.on,
|
|
@@ -203,8 +45,27 @@ export function createGameContext(options) {
|
|
|
203
45
|
signal.notify();
|
|
204
46
|
},
|
|
205
47
|
};
|
|
48
|
+
// --- Scene installer (entities, objects, colliders, spatial, raycast, targeting) ---
|
|
49
|
+
const scene = createSceneSubsystem({
|
|
50
|
+
content,
|
|
51
|
+
signalNotify: signal.notify,
|
|
52
|
+
ground,
|
|
53
|
+
events,
|
|
54
|
+
time,
|
|
55
|
+
...(occluder !== undefined ? { occluder } : {}),
|
|
56
|
+
...(options.models !== undefined ? { models: options.models } : {}),
|
|
57
|
+
});
|
|
58
|
+
const { entities, objects, statsByInstance, entityStats, ensureInstanceStats, catalogEntry, spatial, combatSpatial, sceneRaycast, targeting, entityColliders, entityCollidersOf, objectCollidersOf, entityVisualScaleOf, forms, paintLayer, spawnEntity, despawnEntity, moveEntityTowardCommit, resetAllToSpawn, bind, sceneObjects, } = scene;
|
|
59
|
+
// --- Shared game services (weapon, feed, features bag, commands, item use) ---
|
|
60
|
+
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
206
61
|
const feed = createGameFeed(definition.feed);
|
|
207
62
|
const lootRegistry = createLootRegistry();
|
|
63
|
+
/**
|
|
64
|
+
* Single feature-activation path (G9): `definition.features` is already the final map after
|
|
65
|
+
* `defineGame` OR-merged explicit boolean sugar (`features: { quest: true }`) with system-implied
|
|
66
|
+
* flags via `mergeSystemFeatures`. Descriptor install below is the only place that decides
|
|
67
|
+
* "feature on" — never a second enable check against systems.
|
|
68
|
+
*/
|
|
208
69
|
const features = definition.features ?? {};
|
|
209
70
|
const featureRegistry = new Map();
|
|
210
71
|
const featureValue = (key) => featureRegistry.get(key);
|
|
@@ -222,16 +83,6 @@ export function createGameContext(options) {
|
|
|
222
83
|
}
|
|
223
84
|
return rawSocial;
|
|
224
85
|
};
|
|
225
|
-
const playerStatsByUser = new Map();
|
|
226
|
-
function playerStatsFor(userId) {
|
|
227
|
-
let stats = playerStatsByUser.get(userId);
|
|
228
|
-
if (stats === undefined) {
|
|
229
|
-
stats = createStats({});
|
|
230
|
-
playerStatsByUser.set(userId, stats);
|
|
231
|
-
}
|
|
232
|
-
return stats;
|
|
233
|
-
}
|
|
234
|
-
const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
|
|
235
86
|
const commandRegistry = createCommandRegistry();
|
|
236
87
|
if (definition.lifecycle !== undefined) {
|
|
237
88
|
const lifecycle = definition.lifecycle;
|
|
@@ -251,50 +102,18 @@ export function createGameContext(options) {
|
|
|
251
102
|
});
|
|
252
103
|
}
|
|
253
104
|
const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const inventoryByUser =
|
|
265
|
-
|
|
266
|
-
let set = inventoryByUser.get(userId);
|
|
267
|
-
if (set === undefined) {
|
|
268
|
-
set = notifyAfter(createInventorySet(layouts, traits), ["put", "take", "move", "replaceState"], signal.notify);
|
|
269
|
-
inventoryByUser.set(userId, set);
|
|
270
|
-
}
|
|
271
|
-
return set;
|
|
272
|
-
}
|
|
273
|
-
const wallets = new Map();
|
|
274
|
-
const walletOf = (userId) => wallets.get(userId) ?? createEmptyWallet();
|
|
275
|
-
const economy = {
|
|
276
|
-
balance: (userId, currencyId) => walletBalance(walletOf(userId), currencyId),
|
|
277
|
-
grant(userId, currencyId, amount) {
|
|
278
|
-
wallets.set(userId, walletGrant(walletOf(userId), currencyId, amount));
|
|
279
|
-
signal.notify();
|
|
280
|
-
},
|
|
281
|
-
charge(userId, currencyId, amount, options) {
|
|
282
|
-
const result = walletCharge(walletOf(userId), currencyId, amount, options);
|
|
283
|
-
if (result.status === "rejected")
|
|
284
|
-
return { reason: result.reason };
|
|
285
|
-
wallets.set(userId, result.state);
|
|
286
|
-
signal.notify();
|
|
287
|
-
return null;
|
|
288
|
-
},
|
|
289
|
-
isOverdrawn: (userId, currencyId) => walletIsOverdrawn(walletOf(userId), currencyId),
|
|
290
|
-
};
|
|
291
|
-
function putIntoAnyInventory(userId, itemId, count) {
|
|
292
|
-
const inventory = inventoryFor(userId);
|
|
293
|
-
for (const inventoryId of inventoryIds) {
|
|
294
|
-
if (inventory.put(inventoryId, itemId, count).status === "ok")
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
105
|
+
// --- Player installer (inventory, wallets, pose, motion, loadout) ---
|
|
106
|
+
const playerSys = createPlayerSubsystem({
|
|
107
|
+
signalNotify: signal.notify,
|
|
108
|
+
inventoryDeclarations: definition.inventories ?? {},
|
|
109
|
+
entities,
|
|
110
|
+
events,
|
|
111
|
+
catalogEntry,
|
|
112
|
+
ensureInstanceStats,
|
|
113
|
+
feature: featureValue,
|
|
114
|
+
});
|
|
115
|
+
const { inventoryIds, inventoryByUser, inventoryFor, layouts, wallets, walletOf, economy, putIntoAnyInventory, playerStatsFor, pose, possession, motionByUser, motionFor, loadouts, seedUserPool, } = playerSys;
|
|
116
|
+
scene.setOnAfterDespawn((instanceId) => pose.clear(instanceId));
|
|
298
117
|
const loot = {
|
|
299
118
|
register: lootRegistry.register,
|
|
300
119
|
has: lootRegistry.has,
|
|
@@ -310,100 +129,6 @@ export function createGameContext(options) {
|
|
|
310
129
|
events.emit("loot.granted", event);
|
|
311
130
|
},
|
|
312
131
|
};
|
|
313
|
-
function seedUserPool(userId, statId, pool) {
|
|
314
|
-
const map = ensureInstanceStats(userId);
|
|
315
|
-
const next = setStatValue(map, statId, pool);
|
|
316
|
-
map[statId] = next[statId];
|
|
317
|
-
}
|
|
318
|
-
const loadouts = notifyAfter(createLoadouts({
|
|
319
|
-
inventory: {
|
|
320
|
-
begin(userId) {
|
|
321
|
-
const inventory = inventoryFor(userId);
|
|
322
|
-
const staged = new Map();
|
|
323
|
-
return {
|
|
324
|
-
put(inventoryId, itemId, count, slot) {
|
|
325
|
-
const layout = layouts[inventoryId];
|
|
326
|
-
if (layout === undefined)
|
|
327
|
-
return { reason: `unknown inventory "${inventoryId}"` };
|
|
328
|
-
const state = staged.get(inventoryId) ?? inventory.state(inventoryId);
|
|
329
|
-
const result = putItem(state, layout, traits, itemId, count, slot === undefined ? undefined : { slot });
|
|
330
|
-
if (result.status === "rejected")
|
|
331
|
-
return { reason: result.reason };
|
|
332
|
-
staged.set(inventoryId, result.state);
|
|
333
|
-
return null;
|
|
334
|
-
},
|
|
335
|
-
commit() {
|
|
336
|
-
for (const [inventoryId, state] of staged)
|
|
337
|
-
inventory.replaceState(inventoryId, state);
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
stats: { seed: seedUserPool },
|
|
343
|
-
economy: { grant: economy.grant },
|
|
344
|
-
unlocks: { grant: (userId, unlockId) => featureValue("unlocks")?.grant(userId, unlockId) },
|
|
345
|
-
}), ["applyLoadout"], signal.notify);
|
|
346
|
-
function spawnEntity(name, spawnOptions) {
|
|
347
|
-
const entry = content.entityById?.(name);
|
|
348
|
-
const walkSpeed = spawnOptions?.movement?.walkSpeed ?? entry?.movement?.walkSpeed;
|
|
349
|
-
const options = walkSpeed === undefined
|
|
350
|
-
? spawnOptions
|
|
351
|
-
: { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
|
|
352
|
-
const instanceId = entities.spawn(name, options);
|
|
353
|
-
death.revive(instanceId);
|
|
354
|
-
statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedStatValues(entry.stats));
|
|
355
|
-
return instanceId;
|
|
356
|
-
}
|
|
357
|
-
function despawnEntity(instanceId) {
|
|
358
|
-
const existed = entities.despawn(instanceId);
|
|
359
|
-
statsByInstance.delete(instanceId);
|
|
360
|
-
targeting.clearAll(instanceId);
|
|
361
|
-
pose.clear(instanceId);
|
|
362
|
-
entityColliders.delete(instanceId);
|
|
363
|
-
return existed;
|
|
364
|
-
}
|
|
365
|
-
function moveEntityTowardCommit(instanceId, target, options) {
|
|
366
|
-
const next = spatial.moveToward(instanceId, target, options);
|
|
367
|
-
if (next === null)
|
|
368
|
-
return null;
|
|
369
|
-
const current = entities.get(instanceId);
|
|
370
|
-
if (current === null)
|
|
371
|
-
return null;
|
|
372
|
-
let rotationY = current.rotationY;
|
|
373
|
-
if (options.face === true) {
|
|
374
|
-
const dx = next[0] - current.position[0];
|
|
375
|
-
const dz = next[2] - current.position[2];
|
|
376
|
-
if (Math.hypot(dx, dz) > 1e-9)
|
|
377
|
-
rotationY = Math.atan2(dx, dz);
|
|
378
|
-
}
|
|
379
|
-
entities.setPose(instanceId, { position: next, rotationY });
|
|
380
|
-
return next;
|
|
381
|
-
}
|
|
382
|
-
function resetAllToSpawn(filter) {
|
|
383
|
-
let count = 0;
|
|
384
|
-
for (const entity of entities.list()) {
|
|
385
|
-
if (filter !== undefined && !filter(entity))
|
|
386
|
-
continue;
|
|
387
|
-
if (entities.resetToSpawn(entity.id))
|
|
388
|
-
count += 1;
|
|
389
|
-
}
|
|
390
|
-
return count;
|
|
391
|
-
}
|
|
392
|
-
const bodyBinds = new Map();
|
|
393
|
-
function bind(key) {
|
|
394
|
-
const existing = bodyBinds.get(key);
|
|
395
|
-
if (existing !== undefined)
|
|
396
|
-
return existing;
|
|
397
|
-
const created = createBodyBind({
|
|
398
|
-
has: (id) => entities.get(id) !== null,
|
|
399
|
-
spawn: spawnEntity,
|
|
400
|
-
despawn: despawnEntity,
|
|
401
|
-
setPose: entities.setPose,
|
|
402
|
-
update: entities.update,
|
|
403
|
-
});
|
|
404
|
-
bodyBinds.set(key, created);
|
|
405
|
-
return created;
|
|
406
|
-
}
|
|
407
132
|
const { worldItems, spawnWorldItem, pickupWorldItem } = createWorldItemContext({
|
|
408
133
|
entities,
|
|
409
134
|
events,
|
|
@@ -411,171 +136,43 @@ export function createGameContext(options) {
|
|
|
411
136
|
grantToPlayer: loot.grantToPlayer,
|
|
412
137
|
signalNotify: signal.notify,
|
|
413
138
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
catalogId: entity.name,
|
|
422
|
-
position: [entity.position[0], entity.position[1], entity.position[2]],
|
|
423
|
-
};
|
|
424
|
-
},
|
|
425
|
-
loot: { roll: (tableId) => (lootRegistry.has(tableId) ? lootRegistry.roll(tableId) : []) },
|
|
426
|
-
events,
|
|
427
|
-
runCommand(name, args) {
|
|
428
|
-
commandRegistry.run(ctx, name, args);
|
|
429
|
-
},
|
|
430
|
-
despawn(instanceId) {
|
|
431
|
-
despawnEntity(instanceId);
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
const effects = notifyAfter(createEffectSystem({
|
|
435
|
-
resolveReceive: (instanceId) => catalogEntry(instanceId)?.receive,
|
|
436
|
-
resolveStats: (instanceId) => statsByInstance.get(instanceId),
|
|
437
|
-
getStat: weapon.getStat,
|
|
438
|
-
spatial: combatSpatial,
|
|
439
|
-
onLethal(instanceId, lethalCtx) {
|
|
440
|
-
const dyingEntity = entities.get(instanceId);
|
|
441
|
-
const catalogId = dyingEntity?.name;
|
|
442
|
-
const position = dyingEntity?.position;
|
|
443
|
-
const normalizedOnDeath = normalizeOnDeath(catalogEntry(instanceId)?.onDeath);
|
|
444
|
-
const reason = deathReasonFromEffect({
|
|
445
|
-
...lethalCtx,
|
|
446
|
-
userIdOf: (id) => (id === player.userId ? player.userId : undefined),
|
|
447
|
-
});
|
|
448
|
-
const resolution = death.resolveDeath(instanceId, reason);
|
|
449
|
-
if (resolution.status === "resolved" &&
|
|
450
|
-
resolution.drops.length > 0 &&
|
|
451
|
-
reason.kind === "player_kill" &&
|
|
452
|
-
reason.killerUserId === player.userId) {
|
|
453
|
-
if (normalizedOnDeath.dropMode === "world" && position !== undefined) {
|
|
454
|
-
const resolved = resolveDeathDrops(resolution.drops, {
|
|
455
|
-
mode: "world",
|
|
456
|
-
origin: position,
|
|
457
|
-
resolveRarity: (itemId) => content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
|
|
458
|
-
resolveBaseType: (itemId) => content.itemById?.(itemId)?.baseType ?? itemId,
|
|
459
|
-
scatter: normalizedOnDeath.scatter,
|
|
460
|
-
...(catalogId !== undefined ? { source: catalogId } : {}),
|
|
461
|
-
});
|
|
462
|
-
for (const spawn of resolved.worldSpawns)
|
|
463
|
-
spawnWorldItem(spawn);
|
|
464
|
-
if (resolved.grants.length > 0)
|
|
465
|
-
loot.grantToPlayer(player.userId, resolved.grants, catalogId);
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
loot.grantToPlayer(player.userId, resolution.drops, catalogId);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
}), ["applyEffect"], signal.notify);
|
|
473
|
-
const { emitFloatText, emitVfx, fireTelegraph, applyHitReaction, applyEffectAndFloat } = createCombatFx({
|
|
474
|
-
entities,
|
|
139
|
+
// --- Combat installer (effects, projectiles, death, combat FX) ---
|
|
140
|
+
// Command runner closes over `ctx` once assembled (same late-bind pattern as before).
|
|
141
|
+
let ctxRef = null;
|
|
142
|
+
const combat = createCombatSubsystem({
|
|
143
|
+
content,
|
|
144
|
+
signalNotify: signal.notify,
|
|
145
|
+
now,
|
|
475
146
|
events,
|
|
476
147
|
time,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
now: () => time.now() * 1000,
|
|
482
|
-
});
|
|
483
|
-
const floatingEffects = {
|
|
484
|
-
canReceive: effects.canReceive,
|
|
485
|
-
preview: effects.preview,
|
|
486
|
-
applyEffect: applyEffectAndFloat,
|
|
487
|
-
};
|
|
488
|
-
const projectileObstacles = definition.physics?.projectileObstacles === true;
|
|
489
|
-
const projectileSceneRaycast = {
|
|
490
|
-
raycast(input) {
|
|
491
|
-
return sceneRaycast.raycast({
|
|
492
|
-
...input,
|
|
493
|
-
filter: {
|
|
494
|
-
entities: true,
|
|
495
|
-
objects: projectileObstacles,
|
|
496
|
-
terrain: false,
|
|
497
|
-
walls: projectileObstacles,
|
|
498
|
-
...input.filter,
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
},
|
|
502
|
-
raycastAll(input) {
|
|
503
|
-
return sceneRaycast.raycastAll({
|
|
504
|
-
...input,
|
|
505
|
-
filter: {
|
|
506
|
-
entities: true,
|
|
507
|
-
objects: projectileObstacles,
|
|
508
|
-
terrain: false,
|
|
509
|
-
walls: projectileObstacles,
|
|
510
|
-
...input.filter,
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
const projectiles = notifyAfter(createProjectileSystem({
|
|
516
|
-
effects: floatingEffects,
|
|
517
|
-
spatial: combatSpatial,
|
|
518
|
-
getStat: weapon.getStat,
|
|
519
|
-
sceneRaycast: projectileSceneRaycast,
|
|
520
|
-
objects: projectileObstacles
|
|
521
|
-
? {
|
|
522
|
-
list: () => objects.list(),
|
|
523
|
-
inBox: (min, max) => objects.inBox(min, max),
|
|
524
|
-
halfExtents: (catalogId) => {
|
|
525
|
-
const half = content.objectById?.(catalogId)?.halfExtents;
|
|
526
|
-
return half === undefined ? null : [half[0], half[1], half[2]];
|
|
527
|
-
},
|
|
528
|
-
collidersOf: objectCollidersOf,
|
|
529
|
-
}
|
|
530
|
-
: undefined,
|
|
148
|
+
entities,
|
|
149
|
+
objects,
|
|
150
|
+
combatSpatial,
|
|
151
|
+
sceneRaycast,
|
|
531
152
|
entityCollidersOf,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const objectSelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signal.notify);
|
|
545
|
-
const sceneObjects = {
|
|
546
|
-
...objects,
|
|
547
|
-
remove(instanceId) {
|
|
548
|
-
const existed = objects.remove(instanceId);
|
|
549
|
-
if (existed)
|
|
550
|
-
objectSelection.remove(instanceId);
|
|
551
|
-
return existed;
|
|
552
|
-
},
|
|
553
|
-
catalog: (instanceId) => catalogObject(instanceId) ?? null,
|
|
554
|
-
raycast: (input) => raycastObjects(objects, input),
|
|
555
|
-
raycastAll: (input) => raycastObjectsAll(objects, input),
|
|
556
|
-
setColliders(instanceId, colliders) {
|
|
557
|
-
if (colliders === null)
|
|
558
|
-
objectColliders.delete(instanceId);
|
|
559
|
-
else
|
|
560
|
-
objectColliders.set(instanceId, colliders);
|
|
561
|
-
signal.notify();
|
|
153
|
+
objectCollidersOf,
|
|
154
|
+
catalogEntry,
|
|
155
|
+
statsByInstance,
|
|
156
|
+
weapon,
|
|
157
|
+
loot,
|
|
158
|
+
lootRegistry,
|
|
159
|
+
spawnWorldItem,
|
|
160
|
+
despawnEntity,
|
|
161
|
+
runCommand(name, args) {
|
|
162
|
+
if (ctxRef === null)
|
|
163
|
+
return;
|
|
164
|
+
commandRegistry.run(ctxRef, name, args);
|
|
562
165
|
},
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
};
|
|
166
|
+
localUserId: player.userId,
|
|
167
|
+
...(definition.physics !== undefined ? { physics: definition.physics } : {}),
|
|
168
|
+
});
|
|
169
|
+
scene.setOnAfterSpawn((instanceId) => combat.death.revive(instanceId));
|
|
170
|
+
const { projectiles, combatFx, vfxInstances, effects } = combat;
|
|
566
171
|
const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
|
|
567
172
|
const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify);
|
|
568
173
|
const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
|
|
569
174
|
const input = createInputSnapshot();
|
|
570
|
-
|
|
571
|
-
function motionFor(userId) {
|
|
572
|
-
let queue = motionByUser.get(userId);
|
|
573
|
-
if (queue === undefined) {
|
|
574
|
-
queue = createMotionIntents();
|
|
575
|
-
motionByUser.set(userId, queue);
|
|
576
|
-
}
|
|
577
|
-
return queue;
|
|
578
|
-
}
|
|
175
|
+
// --- Descriptor install (single feature enable path) ---
|
|
579
176
|
const featureDeps = {
|
|
580
177
|
features,
|
|
581
178
|
signalNotify: signal.notify,
|
|
@@ -614,6 +211,12 @@ export function createGameContext(options) {
|
|
|
614
211
|
if (build.save !== undefined)
|
|
615
212
|
featureSaveModules.push(build.save);
|
|
616
213
|
}
|
|
214
|
+
const gameFeatureBlock = {};
|
|
215
|
+
for (const descriptor of featureDescriptors) {
|
|
216
|
+
if (descriptor.key === "cosmetics")
|
|
217
|
+
continue;
|
|
218
|
+
gameFeatureBlock[descriptor.key] = featureRegistry.get(descriptor.key);
|
|
219
|
+
}
|
|
617
220
|
const baselineDeps = {
|
|
618
221
|
signalNotify: signal.notify,
|
|
619
222
|
entities,
|
|
@@ -638,15 +241,16 @@ export function createGameContext(options) {
|
|
|
638
241
|
const replication = options.replication;
|
|
639
242
|
const projectsViewers = policyProjectsViewers(replication);
|
|
640
243
|
const aoiRadius = replication?.aoiRadius;
|
|
244
|
+
const typedProject = (project) => (data, viewer, world) => project(data, viewer, world);
|
|
641
245
|
const projectorFor = (key) => {
|
|
642
246
|
if (aoiRadius !== undefined && key === "entities") {
|
|
643
|
-
return (data, viewer) => projectEntitiesForViewer(data, viewer, aoiRadius);
|
|
247
|
+
return typedProject((data, viewer) => projectEntitiesForViewer(data, viewer, aoiRadius));
|
|
644
248
|
}
|
|
645
249
|
if (aoiRadius !== undefined && key === "stats") {
|
|
646
|
-
return (data, viewer, world) => projectByVisibleIds(data, visibleEntityIds((world["entities"] ?? []), viewer, aoiRadius));
|
|
250
|
+
return typedProject((data, viewer, world) => projectByVisibleIds(data, visibleEntityIds((world["entities"] ?? []), viewer, aoiRadius)));
|
|
647
251
|
}
|
|
648
252
|
if (replication?.privatePerUser === true && key === "inventory") {
|
|
649
|
-
return (data, viewer) => projectPerUserForViewer(data, viewer);
|
|
253
|
+
return typedProject((data, viewer) => projectPerUserForViewer(data, viewer));
|
|
650
254
|
}
|
|
651
255
|
return undefined;
|
|
652
256
|
};
|
|
@@ -659,17 +263,6 @@ export function createGameContext(options) {
|
|
|
659
263
|
};
|
|
660
264
|
});
|
|
661
265
|
const replicationVersion = () => replicationModules.reduce((sum, module) => sum + (module.version?.() ?? 0), 0);
|
|
662
|
-
/**
|
|
663
|
-
* The whole-world *save* set is a superset of the *replication* set (`snapshotModules`): it adds the
|
|
664
|
-
* always-on baseline subsystems (economy, time, pose, possession, motion) plus every opted-in
|
|
665
|
-
* feature's `save` module (unlocks, roster, quest, cosmetics, cards, turn) — the persistent state a
|
|
666
|
-
* single-player save must restore but a host does not replicate to clients. Coverage is owned per
|
|
667
|
-
* subsystem, not hand-listed against a drifting feature manifest: each feature descriptor emits its
|
|
668
|
-
* own `save`, so a new persistent feature can't silently fall out of the save. Keeping the two sets
|
|
669
|
-
* distinct leaves `ctx.snapshot()`/`ctx.hydrate()` — the host→client payload — byte-identical for
|
|
670
|
-
* every multiplayer game, while `ctx.game.save` still persists everything.
|
|
671
|
-
* Mutable so game systems can register save modules at install time via `ctx.game.registerSave`.
|
|
672
|
-
*/
|
|
673
266
|
const saveModules = [
|
|
674
267
|
...snapshotModules,
|
|
675
268
|
...baselineBuilds.flatMap((build) => (build.save === undefined ? [] : [build.save])),
|
|
@@ -687,6 +280,7 @@ export function createGameContext(options) {
|
|
|
687
280
|
});
|
|
688
281
|
saveModules.push(module);
|
|
689
282
|
}
|
|
283
|
+
// --- Assemble public GameContext ---
|
|
690
284
|
const ctx = {
|
|
691
285
|
scene: {
|
|
692
286
|
object: sceneObjects,
|
|
@@ -705,17 +299,17 @@ export function createGameContext(options) {
|
|
|
705
299
|
resetAllToSpawn,
|
|
706
300
|
blackboard: entities.blackboard,
|
|
707
301
|
stats: entityStats,
|
|
708
|
-
floatText: emitFloatText,
|
|
709
|
-
vfx: emitVfx,
|
|
302
|
+
floatText: combatFx.emitFloatText,
|
|
303
|
+
vfx: combatFx.emitVfx,
|
|
710
304
|
vfxInstance: vfxInstances,
|
|
711
|
-
telegraph: fireTelegraph,
|
|
712
|
-
hitReaction: applyHitReaction,
|
|
305
|
+
telegraph: combatFx.fireTelegraph,
|
|
306
|
+
hitReaction: combatFx.applyHitReaction,
|
|
713
307
|
setTarget: targeting.setTarget,
|
|
714
308
|
getTarget: targeting.getTarget,
|
|
715
309
|
cycleTarget: targeting.cycleTarget,
|
|
716
310
|
canReceive: effects.canReceive,
|
|
717
311
|
preview: effects.preview,
|
|
718
|
-
effect: applyEffectAndFloat,
|
|
312
|
+
effect: combatFx.applyEffectAndFloat,
|
|
719
313
|
willHitProjectile: projectiles.willHitProjectile,
|
|
720
314
|
fireProjectile: projectiles.fireProjectile,
|
|
721
315
|
settleProjectile: projectiles.settleProjectile,
|
|
@@ -784,6 +378,9 @@ export function createGameContext(options) {
|
|
|
784
378
|
play: (sound, at) => events.emit("audio.play", at === undefined ? { sound } : { sound, at }),
|
|
785
379
|
music: (theme, transpose) => events.emit("audio.music", transpose === undefined ? { theme } : { theme, transpose }),
|
|
786
380
|
resume: () => events.emit("audio.resume", {}),
|
|
381
|
+
loop: (id, sound, options) => events.emit("audio.loopStart", options?.at === undefined ? { id, sound } : { id, sound, at: options.at }),
|
|
382
|
+
setLoop: (id, params) => events.emit("audio.loopSet", { id, ...params }),
|
|
383
|
+
stopLoop: (id) => events.emit("audio.loopStop", { id }),
|
|
787
384
|
},
|
|
788
385
|
feed: {
|
|
789
386
|
bind: (action) => feed.bind(action, events),
|
|
@@ -800,20 +397,9 @@ export function createGameContext(options) {
|
|
|
800
397
|
},
|
|
801
398
|
},
|
|
802
399
|
loot,
|
|
803
|
-
trade: featureValue("trade"),
|
|
804
|
-
quest: featureValue("quest"),
|
|
805
|
-
social: featureValue("social"),
|
|
806
|
-
chat: featureValue("chat"),
|
|
807
|
-
unlocks: featureValue("unlocks"),
|
|
808
400
|
economy,
|
|
809
|
-
leaderboard: featureValue("leaderboard"),
|
|
810
|
-
roster: featureValue("roster"),
|
|
811
401
|
store,
|
|
812
|
-
|
|
813
|
-
cards: featureValue("cards"),
|
|
814
|
-
turn: featureValue("turn"),
|
|
815
|
-
race: featureValue("race"),
|
|
816
|
-
players: featureValue("players"),
|
|
402
|
+
...gameFeatureBlock,
|
|
817
403
|
registerSave,
|
|
818
404
|
registerReplicate,
|
|
819
405
|
},
|
|
@@ -867,6 +453,7 @@ export function createGameContext(options) {
|
|
|
867
453
|
replicationVersion,
|
|
868
454
|
replicatesPerViewer: () => projectsViewers,
|
|
869
455
|
};
|
|
456
|
+
ctxRef = ctx;
|
|
870
457
|
const saveOptions = resolveSaveOptions(definition, options);
|
|
871
458
|
if (saveOptions !== undefined) {
|
|
872
459
|
const saveTarget = {
|