@jgengine/core 0.19.0 → 0.22.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 +364 -0
- package/dist/ai/decisionGraph.d.ts +2 -0
- package/dist/ai/decisionGraph.js +6 -0
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/anim/animGraph.d.ts +33 -2
- package/dist/anim/animGraph.js +200 -12
- package/dist/anim/locomotionGraph.d.ts +11 -3
- package/dist/anim/locomotionGraph.js +25 -15
- package/dist/audio/audioFalloff.d.ts +4 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +19 -0
- package/dist/combat/abilityKit.js +40 -4
- package/dist/combat/death.d.ts +4 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- package/dist/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +116 -17
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.js +1 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/terrainMaterials.d.ts +10 -0
- package/dist/editor/terrainMaterials.js +13 -0
- package/dist/editor/types.d.ts +11 -0
- package/dist/game/cameraConfig.d.ts +3 -1
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +5 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +52 -20
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- package/dist/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +348 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- package/dist/movement/movementModel.d.ts +24 -1
- package/dist/movement/movementModel.js +74 -7
- package/dist/movement/playerMovement.d.ts +28 -1
- package/dist/movement/playerMovement.js +425 -102
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +3 -1
- package/dist/movement/solidObstacles.js +8 -5
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +3 -3
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/kinematicVehicle.d.ts +8 -3
- package/dist/physics/kinematicVehicle.js +2 -2
- package/dist/physics/physicsWorldBackend.js +19 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleDynamics.d.ts +3 -2
- package/dist/physics/vehicleDynamics.js +2 -2
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/runtime/cameraDirector.d.ts +39 -2
- package/dist/runtime/cameraDirector.js +162 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/context/combat.d.ts +11 -4
- package/dist/runtime/context/combat.js +178 -11
- package/dist/runtime/context/deathLoot.d.ts +9 -8
- package/dist/runtime/context/deathLoot.js +16 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +79 -13
- package/dist/runtime/gameContextTypes.d.ts +14 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +16 -4
- package/dist/runtime/inputSnapshot.js +31 -3
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +26 -5
- package/dist/scene/behaviorRuntime.js +263 -16
- package/dist/scene/behaviors.d.ts +30 -1
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +36 -2
- package/dist/scene/vehicleSeat.js +89 -2
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +9 -2
- package/dist/stats/eventMeter.js +5 -2
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -0
- package/dist/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingKit.d.ts +8 -3
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +3 -3
- package/dist/world/footprintGrid.d.ts +55 -0
- package/dist/world/footprintGrid.js +201 -0
- package/dist/world/grassKind.d.ts +10 -0
- package/dist/world/grassKind.js +22 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +2 -2
- package/dist/world/terraform.js +46 -18
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +11 -0
- package/dist/world/worldSolids.js +112 -1
- package/dist/world.d.ts +5 -5
- package/dist/world.js +4 -4
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-combat/SKILL.md +8 -2
- package/skills/jgengine-combat/api.md +22 -10
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +131 -37
- package/skills/jgengine-gameplay/capabilities.md +68 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +119 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const routes = new WeakMap();
|
|
2
|
+
/** Bind UI command dispatch to the host for this context. A missing join rejects; it never mutates the local replica. */
|
|
3
|
+
export function bindCommandTransport(ctx, route) {
|
|
4
|
+
routes.set(ctx, route);
|
|
5
|
+
return () => { if (routes.get(ctx) === route)
|
|
6
|
+
routes.delete(ctx); };
|
|
7
|
+
}
|
|
8
|
+
/** Dispatch a player's command through the bound host, or locally for an unbound offline context. Await the result before showing success. */
|
|
9
|
+
export function dispatchCommand(ctx, name, input) {
|
|
10
|
+
const route = routes.get(ctx);
|
|
11
|
+
if (route === undefined)
|
|
12
|
+
return ctx.game.commands.run(name, input);
|
|
13
|
+
const serverId = route.serverId();
|
|
14
|
+
if (serverId === null)
|
|
15
|
+
return { status: "rejected", reason: "not-joined" };
|
|
16
|
+
return route.transport.runCommand({ serverId, command: name, input }).then((result) => result.ok
|
|
17
|
+
? { status: "applied", state: ctx }
|
|
18
|
+
: { status: "rejected", reason: result.reason }, (error) => ({ status: "rejected", reason: error instanceof Error ? error.message : "Command transport failed" }));
|
|
19
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type DeathSystem } from "../../combat/death.js";
|
|
2
2
|
import { type CombatSpatialDeps, type EffectSystem } from "../../combat/effects.js";
|
|
3
|
-
import { type ProjectileSystem } from "../../combat/projectiles.js";
|
|
3
|
+
import { type ProjectileSystem, type ProjectileTravelDeps } from "../../combat/projectiles.js";
|
|
4
4
|
import type { PhysicsConfig } from "../../game/defineGame.js";
|
|
5
5
|
import type { GameEvents } from "../../game/events.js";
|
|
6
6
|
import { type VfxInstanceStore } from "../../game/vfxInstance.js";
|
|
7
7
|
import type { LootRegistry } from "../../game/lootTable.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type EntityColliderSet } from "../../scene/colliders.js";
|
|
9
9
|
import type { EntityStore } from "../../scene/entityStore.js";
|
|
10
10
|
import type { ObjectStore } from "../../scene/objectStore.js";
|
|
11
11
|
import type { SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
@@ -34,10 +34,16 @@ export interface CombatSubsystemDeps {
|
|
|
34
34
|
lootRegistry: LootRegistry;
|
|
35
35
|
spawnWorldItem: (input: WorldItemSpawnInput) => WorldItemRecord;
|
|
36
36
|
despawnEntity: (instanceId: string) => boolean;
|
|
37
|
-
runCommand: (name: string, args: unknown) => void;
|
|
38
|
-
|
|
37
|
+
runCommand: (name: string, args: unknown, actorUserId?: string) => void;
|
|
38
|
+
userIdOf: (instanceId: string) => string | undefined;
|
|
39
39
|
rng: () => number;
|
|
40
40
|
physics?: PhysicsConfig;
|
|
41
|
+
projectileTravel?: Pick<ProjectileTravelDeps, "acceleration" | "sweep" | "maxActive" | "maxRetained"> & {
|
|
42
|
+
/** Bounded collision source; omit for the context's entities. */
|
|
43
|
+
targets?(): readonly string[];
|
|
44
|
+
/** Reject an oversized collision source rather than silently missing targets. Default 2048. */
|
|
45
|
+
maxTargets?: number;
|
|
46
|
+
};
|
|
41
47
|
}
|
|
42
48
|
/** @internal Effects, projectiles, death, and combat presentation surface. */
|
|
43
49
|
export interface CombatSubsystem {
|
|
@@ -47,6 +53,7 @@ export interface CombatSubsystem {
|
|
|
47
53
|
projectiles: ProjectileSystem;
|
|
48
54
|
combatFx: CombatFx;
|
|
49
55
|
vfxInstances: VfxInstanceStore;
|
|
56
|
+
captureProjectileTargets(gameDt: number): void;
|
|
50
57
|
}
|
|
51
58
|
/** @internal */
|
|
52
59
|
export declare function createCombatSubsystem(d: CombatSubsystemDeps): CombatSubsystem;
|
|
@@ -2,28 +2,45 @@ import { createDeathSystem, deathReasonFromEffect } from "../../combat/death.js"
|
|
|
2
2
|
import { createEffectSystem, } from "../../combat/effects.js";
|
|
3
3
|
import { createProjectileSystem } from "../../combat/projectiles.js";
|
|
4
4
|
import { createVfxInstanceStore } from "../../game/vfxInstance.js";
|
|
5
|
+
import { colliderBounds, colliderWorldCenter, defaultEntityColliders, resolveColliders } from "../../scene/colliders.js";
|
|
6
|
+
import { raycastCollisionMesh } from "../../scene/collisionMesh.js";
|
|
7
|
+
import { sweepMovingBounds, sweepMovingSphere } from "../../physics/ballisticSweep.js";
|
|
8
|
+
import { createSpatialIndex } from "../../visibility/spatialIndex.js";
|
|
5
9
|
import { createEntityStatsApi } from "../../scene/entityStats.js";
|
|
6
10
|
import { notifyAfter } from "../../store/changeSignal.js";
|
|
7
11
|
import { createCombatFx } from "./combatFx.js";
|
|
8
|
-
import {
|
|
12
|
+
import { allowsUnownedWorldDrops, applyLethalLoot } from "./deathLoot.js";
|
|
9
13
|
/** @internal */
|
|
10
14
|
export function createCombatSubsystem(d) {
|
|
11
|
-
const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand,
|
|
15
|
+
const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, userIdOf, rng, } = d;
|
|
12
16
|
const death = createDeathSystem({
|
|
13
|
-
resolveOnDeath
|
|
17
|
+
resolveOnDeath(instanceId, reason) {
|
|
18
|
+
const spec = catalogEntry(instanceId)?.onDeath;
|
|
19
|
+
if (reason !== undefined && reason.kind !== "player_kill" && allowsUnownedWorldDrops(spec)) {
|
|
20
|
+
// Explicit opt-in must not enable neighboring legacy/unfiltered tables.
|
|
21
|
+
return {
|
|
22
|
+
...spec,
|
|
23
|
+
drops: Array.isArray(spec?.drops)
|
|
24
|
+
? spec.drops.filter((rule) => rule.when?.reason === "any") : [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return spec;
|
|
28
|
+
},
|
|
14
29
|
resolveIdentity(instanceId) {
|
|
15
30
|
const entity = entities.get(instanceId);
|
|
16
31
|
if (entity === null)
|
|
17
32
|
return null;
|
|
33
|
+
const userId = userIdOf(instanceId);
|
|
18
34
|
return {
|
|
19
35
|
catalogId: entity.name,
|
|
20
36
|
position: [entity.position[0], entity.position[1], entity.position[2]],
|
|
37
|
+
...(userId === undefined ? {} : { userId }),
|
|
21
38
|
};
|
|
22
39
|
},
|
|
23
40
|
loot: { roll: (tableId) => (lootRegistry.has(tableId) ? lootRegistry.roll(tableId) : []) },
|
|
24
41
|
events,
|
|
25
|
-
runCommand(name, args) {
|
|
26
|
-
runCommand(name, args);
|
|
42
|
+
runCommand(name, args, reason) {
|
|
43
|
+
runCommand(name, args, reason?.kind === "player_kill" ? reason.killerUserId : undefined);
|
|
27
44
|
},
|
|
28
45
|
despawn(instanceId) {
|
|
29
46
|
despawnEntity(instanceId);
|
|
@@ -39,7 +56,8 @@ export function createCombatSubsystem(d) {
|
|
|
39
56
|
if (entity === null)
|
|
40
57
|
return null;
|
|
41
58
|
// `entity.name` is the spawn kind/catalog id, matching the `entity.died` event.
|
|
42
|
-
|
|
59
|
+
const userId = userIdOf(instanceId);
|
|
60
|
+
return { catalogId: entity.name, name: entity.name, ...(userId === undefined ? {} : { userId }) };
|
|
43
61
|
},
|
|
44
62
|
onLethal(instanceId, lethalCtx) {
|
|
45
63
|
// Capture identity + onDeath *before* resolveDeath despawns the entity.
|
|
@@ -49,21 +67,21 @@ export function createCombatSubsystem(d) {
|
|
|
49
67
|
const onDeath = catalogEntry(instanceId)?.onDeath;
|
|
50
68
|
const reason = deathReasonFromEffect({
|
|
51
69
|
...lethalCtx,
|
|
52
|
-
userIdOf
|
|
70
|
+
userIdOf,
|
|
53
71
|
});
|
|
54
72
|
const resolution = death.resolveDeath(instanceId, reason);
|
|
55
73
|
if (resolution.status !== "resolved")
|
|
56
74
|
return;
|
|
57
75
|
applyLethalLoot({
|
|
58
76
|
drops: resolution.drops,
|
|
59
|
-
|
|
77
|
+
recipientUserId: reason.kind === "player_kill" ? reason.killerUserId : undefined,
|
|
78
|
+
allowUnownedWorldDrops: reason.kind !== "player_kill" && allowsUnownedWorldDrops(onDeath),
|
|
60
79
|
onDeath,
|
|
61
80
|
position,
|
|
62
81
|
catalogId,
|
|
63
82
|
content,
|
|
64
83
|
spawnWorldItem,
|
|
65
84
|
grantToPlayer: loot.grantToPlayer,
|
|
66
|
-
localUserId,
|
|
67
85
|
rng,
|
|
68
86
|
});
|
|
69
87
|
},
|
|
@@ -99,7 +117,8 @@ export function createCombatSubsystem(d) {
|
|
|
99
117
|
return sceneRaycast.raycastAll({ ...input, filter: projectileFilterFor(input.filter) });
|
|
100
118
|
},
|
|
101
119
|
};
|
|
102
|
-
const
|
|
120
|
+
const travelQueries = createProjectileTravelQueries(d);
|
|
121
|
+
const projectileOwner = createProjectileSystem({
|
|
103
122
|
effects: floatingEffects,
|
|
104
123
|
spatial: combatSpatial,
|
|
105
124
|
getStat: weapon.getStat,
|
|
@@ -118,8 +137,12 @@ export function createCombatSubsystem(d) {
|
|
|
118
137
|
entityCollidersOf,
|
|
119
138
|
rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
|
|
120
139
|
now,
|
|
140
|
+
rng,
|
|
141
|
+
travel: { ...d.projectileTravel, now: time.now, sweep: d.projectileTravel?.sweep ?? travelQueries.sweep },
|
|
121
142
|
onSettle(report) {
|
|
122
143
|
events.emit("projectile.settled", {
|
|
144
|
+
shotId: report.shotId,
|
|
145
|
+
hits: structuredClone(report.hits),
|
|
123
146
|
from: report.from,
|
|
124
147
|
origin: [report.origin[0], report.origin[1], report.origin[2]],
|
|
125
148
|
at: [report.at[0], report.at[1], report.at[2]],
|
|
@@ -128,7 +151,15 @@ export function createCombatSubsystem(d) {
|
|
|
128
151
|
ballistic: report.ballistic,
|
|
129
152
|
});
|
|
130
153
|
},
|
|
131
|
-
})
|
|
154
|
+
});
|
|
155
|
+
const resettingProjectileOwner = {
|
|
156
|
+
...projectileOwner,
|
|
157
|
+
restore(state) {
|
|
158
|
+
projectileOwner.restore(state);
|
|
159
|
+
travelQueries.reset();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const projectiles = notifyAfter(resettingProjectileOwner, ["fireProjectile", "settleProjectile", "restore"], signalNotify);
|
|
132
163
|
return {
|
|
133
164
|
death,
|
|
134
165
|
effects,
|
|
@@ -136,5 +167,141 @@ export function createCombatSubsystem(d) {
|
|
|
136
167
|
projectiles,
|
|
137
168
|
combatFx,
|
|
138
169
|
vfxInstances,
|
|
170
|
+
captureProjectileTargets(gameDt) { travelQueries.capture(gameDt, projectiles.activeProjectiles().length > 0); },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Derived collision scratch data is rebuilt from the scene each authoritative step. */
|
|
174
|
+
function createProjectileTravelQueries(d) {
|
|
175
|
+
const { entities, time, sceneRaycast, entityCollidersOf } = d;
|
|
176
|
+
const projectileObstacles = d.physics?.projectileObstacles === true;
|
|
177
|
+
const previousTargets = new Map();
|
|
178
|
+
const maxTargets = d.projectileTravel?.maxTargets ?? 2048;
|
|
179
|
+
if (!Number.isInteger(maxTargets) || maxTargets <= 0)
|
|
180
|
+
throw new RangeError("Projectile collision budget must be a positive integer");
|
|
181
|
+
const targetIndex = createSpatialIndex();
|
|
182
|
+
const targetColliders = new Map();
|
|
183
|
+
const targetCandidates = [];
|
|
184
|
+
let targetIndexReady = false;
|
|
185
|
+
let targetStepStart = 0;
|
|
186
|
+
let targetStepDuration = 0;
|
|
187
|
+
function captureProjectileTargets(gameDt, active) {
|
|
188
|
+
previousTargets.clear();
|
|
189
|
+
targetIndexReady = false;
|
|
190
|
+
targetStepDuration = gameDt;
|
|
191
|
+
targetStepStart = time.now() - gameDt;
|
|
192
|
+
if (!active)
|
|
193
|
+
return;
|
|
194
|
+
const ids = d.projectileTravel?.targets?.() ?? entities.ids();
|
|
195
|
+
if (ids.length > maxTargets)
|
|
196
|
+
throw new RangeError("Projectile collision target budget exhausted; provide a bounded target source");
|
|
197
|
+
for (const id of ids) {
|
|
198
|
+
const entity = entities.get(id);
|
|
199
|
+
if (entity !== null)
|
|
200
|
+
previousTargets.set(id, { position: [...entity.position], rotationY: entity.rotationY });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function indexProjectileTargets() {
|
|
204
|
+
if (targetIndexReady)
|
|
205
|
+
return;
|
|
206
|
+
targetIndex.clear();
|
|
207
|
+
targetColliders.clear();
|
|
208
|
+
const ids = d.projectileTravel?.targets?.() ?? entities.ids();
|
|
209
|
+
if (ids.length > maxTargets)
|
|
210
|
+
throw new RangeError("Projectile collision target budget exhausted; provide a bounded target source");
|
|
211
|
+
for (const id of ids) {
|
|
212
|
+
const entity = entities.get(id);
|
|
213
|
+
if (entity === null)
|
|
214
|
+
continue;
|
|
215
|
+
const previous = previousTargets.get(id);
|
|
216
|
+
const colliders = resolveColliders(entityCollidersOf(id) ?? defaultEntityColliders());
|
|
217
|
+
targetColliders.set(id, colliders);
|
|
218
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
219
|
+
for (const collider of colliders) {
|
|
220
|
+
const start = colliderBounds(collider, previous?.position ?? entity.position, previous?.rotationY ?? entity.rotationY);
|
|
221
|
+
const end = colliderBounds(collider, entity.position, entity.rotationY);
|
|
222
|
+
minX = Math.min(minX, start.min[0], end.min[0]);
|
|
223
|
+
minY = Math.min(minY, start.min[1], end.min[1]);
|
|
224
|
+
minZ = Math.min(minZ, start.min[2], end.min[2]);
|
|
225
|
+
maxX = Math.max(maxX, start.max[0], end.max[0]);
|
|
226
|
+
maxY = Math.max(maxY, start.max[1], end.max[1]);
|
|
227
|
+
maxZ = Math.max(maxZ, start.max[2], end.max[2]);
|
|
228
|
+
}
|
|
229
|
+
if (colliders.length > 0)
|
|
230
|
+
targetIndex.insert(id, { minX, minY, minZ, maxX, maxY, maxZ, centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, centerZ: (minZ + maxZ) / 2, radius: Math.hypot(maxX - minX, maxY - minY, maxZ - minZ) / 2 });
|
|
231
|
+
}
|
|
232
|
+
targetIndexReady = true;
|
|
233
|
+
}
|
|
234
|
+
function sweepProjectile(from, to, step) {
|
|
235
|
+
if (projectileObstacles && step.radius > 0)
|
|
236
|
+
throw new RangeError("Authored cover requires centerline projectiles (radius 0); inject a radius-aware travel sweep for larger projectiles");
|
|
237
|
+
const distance = Math.hypot(to[0] - from[0], to[1] - from[1], to[2] - from[2]);
|
|
238
|
+
const staticHits = distance > 0 ? sceneRaycast.raycastAll({ origin: from, direction: [to[0] - from[0], to[1] - from[1], to[2] - from[2]], maxDistance: distance, excludeInstanceIds: [step.input.from], filter: { entities: false, objects: projectileObstacles, walls: projectileObstacles, terrain: projectileObstacles } }) : [];
|
|
239
|
+
const blocker = staticHits.find(hit => hit.blocks);
|
|
240
|
+
let nearest = blocker === undefined ? null : { fraction: blocker.distance / distance, at: blocker.point };
|
|
241
|
+
let nearestKey = blocker === undefined ? "" : `${blocker.instanceId}:${blocker.colliderName}`;
|
|
242
|
+
indexProjectileTargets();
|
|
243
|
+
const radius = step.radius;
|
|
244
|
+
const ids = targetIndex.queryBox(Math.min(from[0], to[0]) - radius, Math.min(from[1], to[1]) - radius, Math.min(from[2], to[2]) - radius, Math.max(from[0], to[0]) + radius, Math.max(from[1], to[1]) + radius, Math.max(from[2], to[2]) + radius, targetCandidates);
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
if (id === step.input.from)
|
|
247
|
+
continue;
|
|
248
|
+
const entity = entities.get(id);
|
|
249
|
+
if (entity === null)
|
|
250
|
+
continue;
|
|
251
|
+
const previous = previousTargets.get(id);
|
|
252
|
+
const fractionAt = (at) => targetStepDuration > 0 ? Math.max(0, Math.min(1, (at - targetStepStart) / targetStepDuration)) : 1;
|
|
253
|
+
const positionAt = (at) => {
|
|
254
|
+
if (previous === undefined)
|
|
255
|
+
return entity.position;
|
|
256
|
+
const alpha = fractionAt(at);
|
|
257
|
+
return [previous.position[0] + (entity.position[0] - previous.position[0]) * alpha, previous.position[1] + (entity.position[1] - previous.position[1]) * alpha, previous.position[2] + (entity.position[2] - previous.position[2]) * alpha];
|
|
258
|
+
};
|
|
259
|
+
const colliders = targetColliders.get(id);
|
|
260
|
+
for (const collider of colliders) {
|
|
261
|
+
if (!collider.blocks && !collider.damageEligible)
|
|
262
|
+
continue;
|
|
263
|
+
const targetFrom = colliderWorldCenter(collider, positionAt(step.fromTime), previous?.rotationY ?? entity.rotationY);
|
|
264
|
+
const targetTo = colliderWorldCenter(collider, positionAt(step.toTime), entity.rotationY);
|
|
265
|
+
let fraction;
|
|
266
|
+
if (collider.shape.kind === "sphere") {
|
|
267
|
+
fraction = sweepMovingSphere(from, to, targetFrom, targetTo, collider.shape.radius + step.radius);
|
|
268
|
+
}
|
|
269
|
+
else if (collider.shape.kind === "mesh" && step.radius === 0) {
|
|
270
|
+
const start = positionAt(step.fromTime);
|
|
271
|
+
const end = positionAt(step.toTime);
|
|
272
|
+
const origin = [from[0] + end[0] - start[0], from[1] + end[1] - start[1], from[2] + end[2] - start[2]];
|
|
273
|
+
const motion = [to[0] - origin[0], to[1] - origin[1], to[2] - origin[2]];
|
|
274
|
+
const length = Math.hypot(...motion);
|
|
275
|
+
const hit = length > 0 ? raycastCollisionMesh(collider.shape.mesh, origin, [motion[0] / length, motion[1] / length, motion[2] / length], length, end, entity.rotationY, collider.shape.meshScale, collider.shape.meshTranslate) : null;
|
|
276
|
+
fraction = hit === null ? null : hit.distance / length;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const half = collider.shape.halfExtents;
|
|
280
|
+
fraction = sweepMovingBounds(from, to, targetFrom, targetTo, [half[0] + step.radius, half[1] + step.radius, half[2] + step.radius]);
|
|
281
|
+
}
|
|
282
|
+
const key = `${id}:${collider.name}`;
|
|
283
|
+
if (fraction === null || (nearest !== null && (fraction > nearest.fraction || (fraction === nearest.fraction && key >= nearestKey))))
|
|
284
|
+
continue;
|
|
285
|
+
nearestKey = key;
|
|
286
|
+
nearest = {
|
|
287
|
+
fraction, at: [from[0] + (to[0] - from[0]) * fraction, from[1] + (to[1] - from[1]) * fraction, from[2] + (to[2] - from[2]) * fraction],
|
|
288
|
+
target: { kind: "entity", instanceId: id, distance: distance * fraction, colliderName: collider.name, damageEligible: collider.damageEligible },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return nearest;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
capture: captureProjectileTargets,
|
|
296
|
+
sweep: sweepProjectile,
|
|
297
|
+
reset() {
|
|
298
|
+
previousTargets.clear();
|
|
299
|
+
targetIndex.clear();
|
|
300
|
+
targetColliders.clear();
|
|
301
|
+
targetCandidates.length = 0;
|
|
302
|
+
targetIndexReady = false;
|
|
303
|
+
targetStepStart = 0;
|
|
304
|
+
targetStepDuration = 0;
|
|
305
|
+
},
|
|
139
306
|
};
|
|
140
307
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type OnDeathSpec } from "../../combat/death.js";
|
|
2
2
|
import type { Drop } from "../../game/lootTable.js";
|
|
3
3
|
import { type WorldItemSpawnInput } from "../../game/worldItem.js";
|
|
4
4
|
import type { GameContextContent } from "../gameContext.js";
|
|
@@ -6,8 +6,10 @@ import type { GameContextContent } from "../gameContext.js";
|
|
|
6
6
|
export interface LethalLootInput {
|
|
7
7
|
/** Drops produced by the death system's resolution (already rolled). */
|
|
8
8
|
drops: readonly Drop[];
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/** Resolved killer identity; environmental kills have no player recipient. */
|
|
10
|
+
recipientUserId: string | undefined;
|
|
11
|
+
/** Explicit any-death world rules may spawn items without a player recipient. */
|
|
12
|
+
allowUnownedWorldDrops?: boolean;
|
|
11
13
|
/** Catalog `onDeath` for the dying entity (drop mode + scatter). */
|
|
12
14
|
onDeath: OnDeathSpec | undefined;
|
|
13
15
|
/** World position of the dying entity — required for `dropMode: "world"`. */
|
|
@@ -17,16 +19,15 @@ export interface LethalLootInput {
|
|
|
17
19
|
content: GameContextContent;
|
|
18
20
|
spawnWorldItem: (input: WorldItemSpawnInput) => void;
|
|
19
21
|
grantToPlayer: (userId: string, drops: Drop[], source?: string) => void;
|
|
20
|
-
localUserId: string;
|
|
21
22
|
/** The world's seeded stream — scatter positions must replay with the rest of the simulation. */
|
|
22
23
|
rng: () => number;
|
|
23
24
|
}
|
|
25
|
+
/** @internal Only an explicit rule opts a catalog into unowned world drops. */
|
|
26
|
+
export declare function allowsUnownedWorldDrops(onDeath: OnDeathSpec | undefined): boolean;
|
|
24
27
|
/**
|
|
25
|
-
* Pure death→loot policy:
|
|
26
|
-
*
|
|
28
|
+
* Pure death→loot policy: scatter world items or grant straight into player bags.
|
|
29
|
+
* Unowned deaths require explicit opt-in and never grant currency. Extracted from `createGameContext`
|
|
27
30
|
* so combat install stays free of nested loot branching.
|
|
28
31
|
* @internal
|
|
29
32
|
*/
|
|
30
33
|
export declare function applyLethalLoot(input: LethalLootInput): void;
|
|
31
|
-
/** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
|
|
32
|
-
export declare function isLocalPlayerKill(lethalCtx: EffectDeathContext, localUserId: string): boolean;
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeOnDeath } from "../../combat/death.js";
|
|
2
2
|
import { DEFAULT_RARITY, resolveDeathDrops, } from "../../game/worldItem.js";
|
|
3
|
+
/** @internal Only an explicit rule opts a catalog into unowned world drops. */
|
|
4
|
+
export function allowsUnownedWorldDrops(onDeath) {
|
|
5
|
+
const normalized = normalizeOnDeath(onDeath);
|
|
6
|
+
return normalized.dropMode === "world" && normalized.drops.some((rule) => rule.when?.reason === "any");
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
|
-
* Pure death→loot policy:
|
|
5
|
-
*
|
|
9
|
+
* Pure death→loot policy: scatter world items or grant straight into player bags.
|
|
10
|
+
* Unowned deaths require explicit opt-in and never grant currency. Extracted from `createGameContext`
|
|
6
11
|
* so combat install stays free of nested loot branching.
|
|
7
12
|
* @internal
|
|
8
13
|
*/
|
|
9
14
|
export function applyLethalLoot(input) {
|
|
10
|
-
if (input.drops.length === 0
|
|
15
|
+
if (input.drops.length === 0)
|
|
11
16
|
return;
|
|
12
17
|
const normalizedOnDeath = normalizeOnDeath(input.onDeath);
|
|
18
|
+
if (input.recipientUserId === undefined &&
|
|
19
|
+
!(input.allowUnownedWorldDrops && normalizedOnDeath.dropMode === "world" && input.position !== undefined))
|
|
20
|
+
return;
|
|
13
21
|
if (normalizedOnDeath.dropMode === "world" && input.position !== undefined) {
|
|
14
22
|
const resolved = resolveDeathDrops([...input.drops], {
|
|
15
23
|
mode: "world",
|
|
@@ -22,19 +30,11 @@ export function applyLethalLoot(input) {
|
|
|
22
30
|
});
|
|
23
31
|
for (const spawn of resolved.worldSpawns)
|
|
24
32
|
input.spawnWorldItem(spawn);
|
|
25
|
-
if (resolved.grants.length > 0) {
|
|
26
|
-
input.grantToPlayer(input.
|
|
33
|
+
if (resolved.grants.length > 0 && input.recipientUserId !== undefined) {
|
|
34
|
+
input.grantToPlayer(input.recipientUserId, resolved.grants, input.catalogId);
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
|
-
else {
|
|
30
|
-
input.grantToPlayer(input.
|
|
37
|
+
else if (input.recipientUserId !== undefined) {
|
|
38
|
+
input.grantToPlayer(input.recipientUserId, [...input.drops], input.catalogId);
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
|
-
/** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
|
|
34
|
-
export function isLocalPlayerKill(lethalCtx, localUserId) {
|
|
35
|
-
const reason = deathReasonFromEffect({
|
|
36
|
-
...lethalCtx,
|
|
37
|
-
userIdOf: (id) => (id === localUserId ? localUserId : undefined),
|
|
38
|
-
});
|
|
39
|
-
return reason.kind === "player_kill" && reason.killerUserId === localUserId;
|
|
40
|
-
}
|
|
@@ -13,6 +13,7 @@ import { type SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
|
13
13
|
import { type SpatialApi } from "../../scene/spatial.js";
|
|
14
14
|
import { type Targeting } from "../../scene/targeting.js";
|
|
15
15
|
import type { TerrainField } from "../../world/terrain.js";
|
|
16
|
+
import type { WorldSolids } from "../../world/worldSolids.js";
|
|
16
17
|
import type { SimClock } from "../../time/simClock.js";
|
|
17
18
|
import type { GameContextContent, GameContextEntityEntry, GameContextModels, GameContextObjectEntry, MoveTowardCommitOptions, SceneObjectContext } from "../gameContext.js";
|
|
18
19
|
/** @internal Live scene stores + spatial/collider/raycast wiring handed in by `createGameContext`. */
|
|
@@ -20,6 +21,7 @@ export interface SceneSubsystemDeps {
|
|
|
20
21
|
content: GameContextContent;
|
|
21
22
|
signalNotify: () => void;
|
|
22
23
|
ground: TerrainField;
|
|
24
|
+
solids?: Pick<WorldSolids, "count" | "inBox">;
|
|
23
25
|
events: GameEvents;
|
|
24
26
|
/** Full sim clock — forms need `after` for timed shapeshift reversion. */
|
|
25
27
|
time: SimClock;
|
|
@@ -18,7 +18,14 @@ import { notifyAfter } from "../../store/changeSignal.js";
|
|
|
18
18
|
/** @internal */
|
|
19
19
|
export function createSceneSubsystem(d) {
|
|
20
20
|
const { content, signalNotify, ground, events, time, occluder } = d;
|
|
21
|
-
const entities = createEntityStore(
|
|
21
|
+
const entities = createEntityStore({
|
|
22
|
+
onChange: (instanceId) => {
|
|
23
|
+
if (instanceId === undefined)
|
|
24
|
+
spatial.invalidate();
|
|
25
|
+
else
|
|
26
|
+
spatial.updateEntity(instanceId);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
22
29
|
const objects = createObjectStore();
|
|
23
30
|
entities.subscribe(signalNotify);
|
|
24
31
|
objects.subscribe(signalNotify);
|
|
@@ -40,39 +47,23 @@ export function createSceneSubsystem(d) {
|
|
|
40
47
|
const object = objects.get(instanceId);
|
|
41
48
|
return object === null ? undefined : content.objectById?.(object.catalogId);
|
|
42
49
|
}
|
|
43
|
-
let spatialGeneration = 0;
|
|
44
|
-
const candidateIds = [];
|
|
45
|
-
let candidateIdsDirty = true;
|
|
46
|
-
function refreshCandidateIds() {
|
|
47
|
-
if (!candidateIdsDirty)
|
|
48
|
-
return candidateIds;
|
|
49
|
-
candidateIds.length = 0;
|
|
50
|
-
for (const entity of entities.list())
|
|
51
|
-
candidateIds.push(entity.id);
|
|
52
|
-
candidateIdsDirty = false;
|
|
53
|
-
return candidateIds;
|
|
54
|
-
}
|
|
55
50
|
// `moveToward` reads the same blocking colliders the player resolver does, so a chaser stops at the
|
|
56
51
|
// wall that stops the player rather than lerping through it.
|
|
57
52
|
const walkerReach = createObstacleReachCache();
|
|
58
53
|
const solidSource = {
|
|
54
|
+
solids: d.solids,
|
|
59
55
|
list: () => objects.list(),
|
|
60
56
|
inBox: (min, max) => objects.inBox(min, max),
|
|
61
57
|
collidersOf: (instanceId) => objectCollidersOf(instanceId),
|
|
62
58
|
};
|
|
63
59
|
const spatial = createSpatialApi({
|
|
64
60
|
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
65
|
-
candidates:
|
|
61
|
+
candidates: entities.ids,
|
|
66
62
|
grid: { cellSize: 8 },
|
|
67
|
-
|
|
63
|
+
incremental: true,
|
|
68
64
|
resolveStep: (position, stepX, stepZ) => slideStep(position, stepX, stepZ, sourceObstaclesNear(solidSource, sourceObstacleReach(solidSource, walkerReach), position, Math.abs(stepX) + DEFAULT_OBSTACLE_PLAYER_RADIUS, Math.abs(stepZ) + DEFAULT_OBSTACLE_PLAYER_RADIUS)),
|
|
69
65
|
...(occluder !== undefined ? { occluder } : {}),
|
|
70
66
|
});
|
|
71
|
-
entities.subscribe(() => {
|
|
72
|
-
candidateIdsDirty = true;
|
|
73
|
-
spatialGeneration += 1;
|
|
74
|
-
spatial.invalidate();
|
|
75
|
-
});
|
|
76
67
|
const entityColliders = new Map();
|
|
77
68
|
const objectColliders = new Map();
|
|
78
69
|
const fittedEntityByKind = new Map();
|
|
@@ -216,7 +207,7 @@ export function createSceneSubsystem(d) {
|
|
|
216
207
|
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
217
208
|
}
|
|
218
209
|
const targeting = notifyAfter(createTargeting({
|
|
219
|
-
candidates: () => [...
|
|
210
|
+
candidates: () => [...entities.ids()],
|
|
220
211
|
classify(_fromId, toId) {
|
|
221
212
|
const role = catalogEntry(toId)?.role;
|
|
222
213
|
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
@@ -238,6 +229,7 @@ export function createSceneSubsystem(d) {
|
|
|
238
229
|
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
239
230
|
};
|
|
240
231
|
const sceneRaycast = createSceneRaycast({
|
|
232
|
+
solids: d.solids,
|
|
241
233
|
entities: {
|
|
242
234
|
list: () => entities.list().map((entity) => ({
|
|
243
235
|
id: entity.id,
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { hydrateEntityStats, snapshotEntityStats, } from "../../scene/entityStats.js";
|
|
2
2
|
import { decodeArray, decodeEntries, decodeRecord } from "../context/snapshotCodecs.js";
|
|
3
|
+
// A store slot whose value carries functions (a closure-based run store, an input handle) is that
|
|
4
|
+
// world's runtime object, not world state: it cannot be cloned, persisted or sent, so it stays out of
|
|
5
|
+
// snapshots and survives hydration in place.
|
|
6
|
+
function holdsLiveCode(value, seen = new Set()) {
|
|
7
|
+
if (typeof value === "function")
|
|
8
|
+
return true;
|
|
9
|
+
if (value === null || typeof value !== "object" || seen.has(value) || ArrayBuffer.isView(value))
|
|
10
|
+
return false;
|
|
11
|
+
seen.add(value);
|
|
12
|
+
if (value instanceof Map) {
|
|
13
|
+
for (const [key, entry] of value)
|
|
14
|
+
if (holdsLiveCode(key, seen) || holdsLiveCode(entry, seen))
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (value instanceof Set) {
|
|
19
|
+
for (const entry of value)
|
|
20
|
+
if (holdsLiveCode(entry, seen))
|
|
21
|
+
return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
for (const key in value)
|
|
25
|
+
if (holdsLiveCode(value[key], seen))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
3
29
|
/** @internal */
|
|
4
30
|
export const baselineDescriptors = [
|
|
5
31
|
{
|
|
@@ -29,9 +55,14 @@ export const baselineDescriptors = [
|
|
|
29
55
|
create: (d) => ({
|
|
30
56
|
replicate: {
|
|
31
57
|
key: "store",
|
|
32
|
-
snapshot: () => d.store.snapshot(),
|
|
58
|
+
snapshot: () => d.store.snapshot().filter(([, value]) => !holdsLiveCode(value)),
|
|
33
59
|
decode: (raw) => decodeEntries(raw),
|
|
34
|
-
hydrate: (data) =>
|
|
60
|
+
hydrate: (data) => {
|
|
61
|
+
const incoming = data;
|
|
62
|
+
const incomingKeys = new Set(incoming.map(([key]) => key));
|
|
63
|
+
const localLive = d.store.snapshot().filter(([key, value]) => !incomingKeys.has(key) && holdsLiveCode(value));
|
|
64
|
+
d.store.hydrate(localLive.length === 0 ? incoming : [...incoming, ...localLive]);
|
|
65
|
+
},
|
|
35
66
|
},
|
|
36
67
|
}),
|
|
37
68
|
},
|
|
@@ -94,7 +125,7 @@ export const baselineDescriptors = [
|
|
|
94
125
|
{
|
|
95
126
|
key: "time",
|
|
96
127
|
create: (d) => ({
|
|
97
|
-
|
|
128
|
+
replicate: {
|
|
98
129
|
key: "time",
|
|
99
130
|
snapshot: () => d.time.snapshot(),
|
|
100
131
|
decode: (raw) => {
|
|
@@ -4,7 +4,7 @@ import { type WalletState } from "../../economy/wallet.js";
|
|
|
4
4
|
import type { GameFeatures } from "../../game/defineGame.js";
|
|
5
5
|
import type { GameEvents } from "../../game/events.js";
|
|
6
6
|
import { type Social } from "../../game/social.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type InventoryLayout, type InventorySet, type ItemTraits } from "../../inventory/inventoryModel.js";
|
|
8
8
|
import type { StatValueMap } from "../../scene/entityStats.js";
|
|
9
9
|
import type { EntityStore } from "../../scene/entityStore.js";
|
|
10
10
|
import type { SpatialApi } from "../../scene/spatial.js";
|
|
@@ -35,6 +35,7 @@ export interface FeatureDeps {
|
|
|
35
35
|
walletOf: (userId: string) => WalletState;
|
|
36
36
|
setWallet: (userId: string, state: WalletState) => void;
|
|
37
37
|
layouts: Record<string, InventoryLayout>;
|
|
38
|
+
itemTraits: ItemTraits;
|
|
38
39
|
inventoryFor: (userId: string) => InventorySet<string>;
|
|
39
40
|
ensureInstanceStats: (instanceId: string) => StatValueMap;
|
|
40
41
|
seedUserPool: (userId: string, statId: string, pool: {
|
|
@@ -7,6 +7,7 @@ import { createChat } from "../../game/chat.js";
|
|
|
7
7
|
import {} from "../../game/social.js";
|
|
8
8
|
import { createTradeSystem } from "../../game/trade.js";
|
|
9
9
|
import { createUnlocks } from "../../game/unlocks.js";
|
|
10
|
+
import { putItem, } from "../../inventory/inventoryModel.js";
|
|
10
11
|
import { createRoster } from "../../scene/roster.js";
|
|
11
12
|
import { createConnectedPlayers } from "../../game/connectedPlayers.js";
|
|
12
13
|
import { notifyAfter } from "../../store/changeSignal.js";
|
|
@@ -197,6 +198,23 @@ export const featureDescriptors = [
|
|
|
197
198
|
const result = d.inventoryFor(userId).put(inventoryId, itemId, count);
|
|
198
199
|
return result.status === "ok" ? null : { reason: result.reason };
|
|
199
200
|
},
|
|
201
|
+
grantItems(userId, items) {
|
|
202
|
+
const inventories = d.inventoryFor(userId);
|
|
203
|
+
const staged = new Map();
|
|
204
|
+
for (const entry of items) {
|
|
205
|
+
const layout = d.layouts[entry.inventory];
|
|
206
|
+
if (layout === undefined)
|
|
207
|
+
return { reason: `unknown inventory "${entry.inventory}"` };
|
|
208
|
+
const current = staged.get(entry.inventory) ?? inventories.state(entry.inventory);
|
|
209
|
+
const result = putItem(current, layout, d.itemTraits, entry.item, entry.count);
|
|
210
|
+
if (result.status !== "ok")
|
|
211
|
+
return { reason: result.reason };
|
|
212
|
+
staged.set(entry.inventory, result.state);
|
|
213
|
+
}
|
|
214
|
+
for (const [inventoryId, state] of staged)
|
|
215
|
+
inventories.replaceState(inventoryId, state);
|
|
216
|
+
return null;
|
|
217
|
+
},
|
|
200
218
|
grantUnlock: (userId, unlockId) => d.feature("unlocks")?.grant(userId, unlockId),
|
|
201
219
|
},
|
|
202
220
|
hasUnlock: (userId, id) => d.feature("unlocks")?.has(userId, id) ?? false,
|