@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
|
@@ -1,47 +1,16 @@
|
|
|
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 "../
|
|
25
|
-
import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
|
|
26
|
-
import { createItemUse, } from "../item/use.js";
|
|
9
|
+
import { splitStack } from "../inventory/inventoryModel.js";
|
|
10
|
+
import { createItemUse } from "../item/use.js";
|
|
27
11
|
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";
|
|
12
|
+
import {} from "../scene/entityStats.js";
|
|
13
|
+
import {} from "../scene/entityStore.js";
|
|
45
14
|
import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
|
|
46
15
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
47
16
|
import { applyWorldSnapshot, composeWorldSnapshot, } from "./worldSnapshot.js";
|
|
@@ -50,15 +19,14 @@ import { createRuntimeSave } from "./runtimeSave.js";
|
|
|
50
19
|
import { isOffline } from "./adapter.js";
|
|
51
20
|
import { localSaveBackend, memorySaveBackend } from "../game/saveStore.js";
|
|
52
21
|
import { createSimClock } from "../time/simClock.js";
|
|
53
|
-
import {} from "../turn/turnLoop.js";
|
|
54
|
-
import { RaceState } from "../game/race.js";
|
|
55
22
|
import { createCameraDirector } from "./cameraDirector.js";
|
|
56
23
|
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
57
|
-
import { createMotionIntents } from "./motionIntents.js";
|
|
58
24
|
import { baselineDescriptors } from "./descriptors/baseline.js";
|
|
59
25
|
import { featureDescriptors } from "./descriptors/features.js";
|
|
60
|
-
import {
|
|
26
|
+
import { createCombatSubsystem } from "./context/combat.js";
|
|
61
27
|
import { createContextRegistries } from "./context/registries.js";
|
|
28
|
+
import { createPlayerSubsystem } from "./context/player.js";
|
|
29
|
+
import { createSceneSubsystem } from "./context/scene.js";
|
|
62
30
|
import { createWorldItemContext } from "./context/worldItems.js";
|
|
63
31
|
export function createGameContext(options) {
|
|
64
32
|
const { definition, content, player } = options;
|
|
@@ -69,131 +37,6 @@ export function createGameContext(options) {
|
|
|
69
37
|
const activeUserId = () => actingUserId ?? player.userId;
|
|
70
38
|
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
71
39
|
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
40
|
const rawEvents = createGameEvents();
|
|
198
41
|
const events = {
|
|
199
42
|
on: rawEvents.on,
|
|
@@ -203,8 +46,27 @@ export function createGameContext(options) {
|
|
|
203
46
|
signal.notify();
|
|
204
47
|
},
|
|
205
48
|
};
|
|
49
|
+
// --- Scene installer (entities, objects, colliders, spatial, raycast, targeting) ---
|
|
50
|
+
const scene = createSceneSubsystem({
|
|
51
|
+
content,
|
|
52
|
+
signalNotify: signal.notify,
|
|
53
|
+
ground,
|
|
54
|
+
events,
|
|
55
|
+
time,
|
|
56
|
+
...(occluder !== undefined ? { occluder } : {}),
|
|
57
|
+
...(options.models !== undefined ? { models: options.models } : {}),
|
|
58
|
+
});
|
|
59
|
+
const { entities, objects, statsByInstance, entityStats, ensureInstanceStats, catalogEntry, spatial, combatSpatial, sceneRaycast, targeting, entityColliders, entityCollidersOf, objectCollidersOf, reportEntityBounds, entityVisualScaleOf, forms, paintLayer, spawnEntity, despawnEntity, moveEntityTowardCommit, resetAllToSpawn, bind, sceneObjects, } = scene;
|
|
60
|
+
// --- Shared game services (weapon, feed, features bag, commands, item use) ---
|
|
61
|
+
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
206
62
|
const feed = createGameFeed(definition.feed);
|
|
207
63
|
const lootRegistry = createLootRegistry();
|
|
64
|
+
/**
|
|
65
|
+
* Single feature-activation path (G9): `definition.features` is already the final map after
|
|
66
|
+
* `defineGame` OR-merged explicit boolean sugar (`features: { quest: true }`) with system-implied
|
|
67
|
+
* flags via `mergeSystemFeatures`. Descriptor install below is the only place that decides
|
|
68
|
+
* "feature on" — never a second enable check against systems.
|
|
69
|
+
*/
|
|
208
70
|
const features = definition.features ?? {};
|
|
209
71
|
const featureRegistry = new Map();
|
|
210
72
|
const featureValue = (key) => featureRegistry.get(key);
|
|
@@ -222,16 +84,6 @@ export function createGameContext(options) {
|
|
|
222
84
|
}
|
|
223
85
|
return rawSocial;
|
|
224
86
|
};
|
|
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
87
|
const commandRegistry = createCommandRegistry();
|
|
236
88
|
if (definition.lifecycle !== undefined) {
|
|
237
89
|
const lifecycle = definition.lifecycle;
|
|
@@ -251,50 +103,57 @@ export function createGameContext(options) {
|
|
|
251
103
|
});
|
|
252
104
|
}
|
|
253
105
|
const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const inventoryByUser =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
signal.notify();
|
|
106
|
+
// --- Player installer (inventory, wallets, pose, motion, loadout) ---
|
|
107
|
+
const playerSys = createPlayerSubsystem({
|
|
108
|
+
signalNotify: signal.notify,
|
|
109
|
+
inventoryDeclarations: definition.inventories ?? {},
|
|
110
|
+
entities,
|
|
111
|
+
events,
|
|
112
|
+
catalogEntry,
|
|
113
|
+
ensureInstanceStats,
|
|
114
|
+
feature: featureValue,
|
|
115
|
+
});
|
|
116
|
+
const { inventoryIds, inventoryByUser, inventoryFor, layouts, wallets, walletOf, economy, putIntoAnyInventory, playerStatsFor, pose, possession, motionByUser, motionFor, loadouts, seedUserPool, } = playerSys;
|
|
117
|
+
scene.setOnAfterDespawn((instanceId) => pose.clear(instanceId));
|
|
118
|
+
// Built-in inventory mutation commands so a HUD grid drives moves/splits through the notifying
|
|
119
|
+
// command path (a raw InventorySet mutation would not re-render React). Each resolves the acting
|
|
120
|
+
// user's own inventory, so it respects `privatePerUser` replication.
|
|
121
|
+
const knownInventory = (inventoryId) => layouts[inventoryId] !== undefined;
|
|
122
|
+
const slotInRange = (inventoryId, slot) => typeof slot === "number" && Number.isInteger(slot) && slot >= 0 && slot < layouts[inventoryId].slots;
|
|
123
|
+
commandRegistry.define("inventory.move", {
|
|
124
|
+
validate(_state, input) {
|
|
125
|
+
if (!knownInventory(input.inventoryId))
|
|
126
|
+
return { reason: `unknown inventory "${input.inventoryId}"` };
|
|
127
|
+
if (!slotInRange(input.inventoryId, input.from) || !slotInRange(input.inventoryId, input.to)) {
|
|
128
|
+
return { reason: "invalid-slot" };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
280
131
|
},
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
132
|
+
apply(_state, input) {
|
|
133
|
+
const inventory = inventoryFor(activeUserId());
|
|
134
|
+
// A rejected move (empty source, wrong-kind, no-space) is a harmless no-op: the item bounces back.
|
|
135
|
+
inventory.move(input.inventoryId, input.from, input.inventoryId, input.to);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
commandRegistry.define("inventory.split", {
|
|
139
|
+
validate(_state, input) {
|
|
140
|
+
if (!knownInventory(input.inventoryId))
|
|
141
|
+
return { reason: `unknown inventory "${input.inventoryId}"` };
|
|
142
|
+
if (!slotInRange(input.inventoryId, input.slot))
|
|
143
|
+
return { reason: "invalid-slot" };
|
|
144
|
+
if (input.toSlot !== undefined && !slotInRange(input.inventoryId, input.toSlot)) {
|
|
145
|
+
return { reason: "invalid-slot" };
|
|
146
|
+
}
|
|
287
147
|
return null;
|
|
288
148
|
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
for (const inventoryId of inventoryIds) {
|
|
294
|
-
if (inventory.put(inventoryId, itemId, count).status === "ok")
|
|
149
|
+
apply(_state, input) {
|
|
150
|
+
const inventory = inventoryFor(activeUserId());
|
|
151
|
+
const result = splitStack(inventory.state(input.inventoryId), input.slot, input.amount, input.toSlot);
|
|
152
|
+
if (result.status === "rejected")
|
|
295
153
|
return;
|
|
296
|
-
|
|
297
|
-
|
|
154
|
+
inventory.replaceState(input.inventoryId, result.state);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
298
157
|
const loot = {
|
|
299
158
|
register: lootRegistry.register,
|
|
300
159
|
has: lootRegistry.has,
|
|
@@ -310,100 +169,6 @@ export function createGameContext(options) {
|
|
|
310
169
|
events.emit("loot.granted", event);
|
|
311
170
|
},
|
|
312
171
|
};
|
|
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
172
|
const { worldItems, spawnWorldItem, pickupWorldItem } = createWorldItemContext({
|
|
408
173
|
entities,
|
|
409
174
|
events,
|
|
@@ -411,171 +176,43 @@ export function createGameContext(options) {
|
|
|
411
176
|
grantToPlayer: loot.grantToPlayer,
|
|
412
177
|
signalNotify: signal.notify,
|
|
413
178
|
});
|
|
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,
|
|
179
|
+
// --- Combat installer (effects, projectiles, death, combat FX) ---
|
|
180
|
+
// Command runner closes over `ctx` once assembled (same late-bind pattern as before).
|
|
181
|
+
let ctxRef = null;
|
|
182
|
+
const combat = createCombatSubsystem({
|
|
183
|
+
content,
|
|
184
|
+
signalNotify: signal.notify,
|
|
185
|
+
now,
|
|
475
186
|
events,
|
|
476
187
|
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,
|
|
188
|
+
entities,
|
|
189
|
+
objects,
|
|
190
|
+
combatSpatial,
|
|
191
|
+
sceneRaycast,
|
|
531
192
|
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();
|
|
193
|
+
objectCollidersOf,
|
|
194
|
+
catalogEntry,
|
|
195
|
+
statsByInstance,
|
|
196
|
+
weapon,
|
|
197
|
+
loot,
|
|
198
|
+
lootRegistry,
|
|
199
|
+
spawnWorldItem,
|
|
200
|
+
despawnEntity,
|
|
201
|
+
runCommand(name, args) {
|
|
202
|
+
if (ctxRef === null)
|
|
203
|
+
return;
|
|
204
|
+
commandRegistry.run(ctxRef, name, args);
|
|
562
205
|
},
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
};
|
|
206
|
+
localUserId: player.userId,
|
|
207
|
+
...(definition.physics !== undefined ? { physics: definition.physics } : {}),
|
|
208
|
+
});
|
|
209
|
+
scene.setOnAfterSpawn((instanceId) => combat.death.revive(instanceId));
|
|
210
|
+
const { projectiles, combatFx, vfxInstances, effects } = combat;
|
|
566
211
|
const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
|
|
567
212
|
const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify);
|
|
568
213
|
const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
|
|
569
214
|
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
|
-
}
|
|
215
|
+
// --- Descriptor install (single feature enable path) ---
|
|
579
216
|
const featureDeps = {
|
|
580
217
|
features,
|
|
581
218
|
signalNotify: signal.notify,
|
|
@@ -614,6 +251,12 @@ export function createGameContext(options) {
|
|
|
614
251
|
if (build.save !== undefined)
|
|
615
252
|
featureSaveModules.push(build.save);
|
|
616
253
|
}
|
|
254
|
+
const gameFeatureBlock = {};
|
|
255
|
+
for (const descriptor of featureDescriptors) {
|
|
256
|
+
if (descriptor.key === "cosmetics")
|
|
257
|
+
continue;
|
|
258
|
+
gameFeatureBlock[descriptor.key] = featureRegistry.get(descriptor.key);
|
|
259
|
+
}
|
|
617
260
|
const baselineDeps = {
|
|
618
261
|
signalNotify: signal.notify,
|
|
619
262
|
entities,
|
|
@@ -638,15 +281,16 @@ export function createGameContext(options) {
|
|
|
638
281
|
const replication = options.replication;
|
|
639
282
|
const projectsViewers = policyProjectsViewers(replication);
|
|
640
283
|
const aoiRadius = replication?.aoiRadius;
|
|
284
|
+
const typedProject = (project) => (data, viewer, world) => project(data, viewer, world);
|
|
641
285
|
const projectorFor = (key) => {
|
|
642
286
|
if (aoiRadius !== undefined && key === "entities") {
|
|
643
|
-
return (data, viewer) => projectEntitiesForViewer(data, viewer, aoiRadius);
|
|
287
|
+
return typedProject((data, viewer) => projectEntitiesForViewer(data, viewer, aoiRadius));
|
|
644
288
|
}
|
|
645
289
|
if (aoiRadius !== undefined && key === "stats") {
|
|
646
|
-
return (data, viewer, world) => projectByVisibleIds(data, visibleEntityIds((world["entities"] ?? []), viewer, aoiRadius));
|
|
290
|
+
return typedProject((data, viewer, world) => projectByVisibleIds(data, visibleEntityIds((world["entities"] ?? []), viewer, aoiRadius)));
|
|
647
291
|
}
|
|
648
292
|
if (replication?.privatePerUser === true && key === "inventory") {
|
|
649
|
-
return (data, viewer) => projectPerUserForViewer(data, viewer);
|
|
293
|
+
return typedProject((data, viewer) => projectPerUserForViewer(data, viewer));
|
|
650
294
|
}
|
|
651
295
|
return undefined;
|
|
652
296
|
};
|
|
@@ -659,17 +303,6 @@ export function createGameContext(options) {
|
|
|
659
303
|
};
|
|
660
304
|
});
|
|
661
305
|
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
306
|
const saveModules = [
|
|
674
307
|
...snapshotModules,
|
|
675
308
|
...baselineBuilds.flatMap((build) => (build.save === undefined ? [] : [build.save])),
|
|
@@ -687,6 +320,7 @@ export function createGameContext(options) {
|
|
|
687
320
|
});
|
|
688
321
|
saveModules.push(module);
|
|
689
322
|
}
|
|
323
|
+
// --- Assemble public GameContext ---
|
|
690
324
|
const ctx = {
|
|
691
325
|
scene: {
|
|
692
326
|
object: sceneObjects,
|
|
@@ -705,17 +339,17 @@ export function createGameContext(options) {
|
|
|
705
339
|
resetAllToSpawn,
|
|
706
340
|
blackboard: entities.blackboard,
|
|
707
341
|
stats: entityStats,
|
|
708
|
-
floatText: emitFloatText,
|
|
709
|
-
vfx: emitVfx,
|
|
342
|
+
floatText: combatFx.emitFloatText,
|
|
343
|
+
vfx: combatFx.emitVfx,
|
|
710
344
|
vfxInstance: vfxInstances,
|
|
711
|
-
telegraph: fireTelegraph,
|
|
712
|
-
hitReaction: applyHitReaction,
|
|
345
|
+
telegraph: combatFx.fireTelegraph,
|
|
346
|
+
hitReaction: combatFx.applyHitReaction,
|
|
713
347
|
setTarget: targeting.setTarget,
|
|
714
348
|
getTarget: targeting.getTarget,
|
|
715
349
|
cycleTarget: targeting.cycleTarget,
|
|
716
350
|
canReceive: effects.canReceive,
|
|
717
351
|
preview: effects.preview,
|
|
718
|
-
effect: applyEffectAndFloat,
|
|
352
|
+
effect: combatFx.applyEffectAndFloat,
|
|
719
353
|
willHitProjectile: projectiles.willHitProjectile,
|
|
720
354
|
fireProjectile: projectiles.fireProjectile,
|
|
721
355
|
settleProjectile: projectiles.settleProjectile,
|
|
@@ -734,6 +368,7 @@ export function createGameContext(options) {
|
|
|
734
368
|
signal.notify();
|
|
735
369
|
},
|
|
736
370
|
collidersOf: entityCollidersOf,
|
|
371
|
+
reportBounds: reportEntityBounds,
|
|
737
372
|
visualScaleOf: entityVisualScaleOf,
|
|
738
373
|
form: forms,
|
|
739
374
|
paint: paintLayer,
|
|
@@ -784,6 +419,9 @@ export function createGameContext(options) {
|
|
|
784
419
|
play: (sound, at) => events.emit("audio.play", at === undefined ? { sound } : { sound, at }),
|
|
785
420
|
music: (theme, transpose) => events.emit("audio.music", transpose === undefined ? { theme } : { theme, transpose }),
|
|
786
421
|
resume: () => events.emit("audio.resume", {}),
|
|
422
|
+
loop: (id, sound, options) => events.emit("audio.loopStart", options?.at === undefined ? { id, sound } : { id, sound, at: options.at }),
|
|
423
|
+
setLoop: (id, params) => events.emit("audio.loopSet", { id, ...params }),
|
|
424
|
+
stopLoop: (id) => events.emit("audio.loopStop", { id }),
|
|
787
425
|
},
|
|
788
426
|
feed: {
|
|
789
427
|
bind: (action) => feed.bind(action, events),
|
|
@@ -800,20 +438,9 @@ export function createGameContext(options) {
|
|
|
800
438
|
},
|
|
801
439
|
},
|
|
802
440
|
loot,
|
|
803
|
-
trade: featureValue("trade"),
|
|
804
|
-
quest: featureValue("quest"),
|
|
805
|
-
social: featureValue("social"),
|
|
806
|
-
chat: featureValue("chat"),
|
|
807
|
-
unlocks: featureValue("unlocks"),
|
|
808
441
|
economy,
|
|
809
|
-
leaderboard: featureValue("leaderboard"),
|
|
810
|
-
roster: featureValue("roster"),
|
|
811
442
|
store,
|
|
812
|
-
|
|
813
|
-
cards: featureValue("cards"),
|
|
814
|
-
turn: featureValue("turn"),
|
|
815
|
-
race: featureValue("race"),
|
|
816
|
-
players: featureValue("players"),
|
|
443
|
+
...gameFeatureBlock,
|
|
817
444
|
registerSave,
|
|
818
445
|
registerReplicate,
|
|
819
446
|
},
|
|
@@ -867,6 +494,7 @@ export function createGameContext(options) {
|
|
|
867
494
|
replicationVersion,
|
|
868
495
|
replicatesPerViewer: () => projectsViewers,
|
|
869
496
|
};
|
|
497
|
+
ctxRef = ctx;
|
|
870
498
|
const saveOptions = resolveSaveOptions(definition, options);
|
|
871
499
|
if (saveOptions !== undefined) {
|
|
872
500
|
const saveTarget = {
|
|
@@ -885,16 +513,21 @@ function saveKey(name) {
|
|
|
885
513
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
886
514
|
return `jgengine:save:${slug === "" ? "game" : slug}`;
|
|
887
515
|
}
|
|
888
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* Explicit `options.save` wins (the shell can inject any backend, e.g. cloud); otherwise an offline /
|
|
518
|
+
* single-player world auto-wires a `localStorage` whole-world save **by default** — a game gets `ctx.game.save`
|
|
519
|
+
* and autosave without touching `persist`. Only an explicit `persist: false` opts out; a host-authoritative
|
|
520
|
+
* multiplayer world never persists to `localStorage` (its host owns persistence) regardless of `persist`.
|
|
521
|
+
*/
|
|
889
522
|
function resolveSaveOptions(definition, options) {
|
|
890
523
|
if (options.save !== undefined)
|
|
891
524
|
return options.save;
|
|
892
525
|
const persist = definition.persist;
|
|
893
|
-
if (persist ===
|
|
526
|
+
if (persist === false)
|
|
894
527
|
return undefined;
|
|
895
528
|
if (!isOffline(definition.multiplayer))
|
|
896
529
|
return undefined;
|
|
897
|
-
const config = persist === true ? {} : persist;
|
|
530
|
+
const config = persist === undefined || persist === true ? {} : persist;
|
|
898
531
|
return {
|
|
899
532
|
backend: config.storage === "memory" ? memorySaveBackend() : localSaveBackend(),
|
|
900
533
|
mode: config.mode,
|