@jgengine/core 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +5 -2
- package/dist/combat.js +4 -1
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +60 -0
- package/dist/runtime/context/scene.js +329 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +7 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +79 -0
- package/dist/scene/colliders.js +170 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +19 -15
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CollisionObstacle } from "../movement/movementModel.js";
|
|
2
|
+
/**
|
|
3
|
+
* A single blocked-move event a car can react to (#1051): crash damage, screen shake, a metal-crunch
|
|
4
|
+
* cue. Reported once, when a move is first vetoed, then consumed by {@link VehicleObstacleClamp.takeImpact}.
|
|
5
|
+
*/
|
|
6
|
+
export interface VehicleImpact {
|
|
7
|
+
/** Speed (world units/s) the car was carrying *into* the obstacle at the moment the move was blocked. */
|
|
8
|
+
closingSpeed: number;
|
|
9
|
+
/** Unit push-back direction — points from the struck surface back toward the car. */
|
|
10
|
+
normal: readonly [number, number];
|
|
11
|
+
/** Index into the obstacle set passed for the blocking tick. */
|
|
12
|
+
obstacleIndex: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A planar move clamp for a kinematic car: feed {@link clampMove} to a vehicle's `clampMove` hook so
|
|
16
|
+
* an attempted XZ displacement slides along world solids instead of driving through them, and read
|
|
17
|
+
* {@link takeImpact} once per tick for the crash it produced.
|
|
18
|
+
*/
|
|
19
|
+
export interface VehicleObstacleClamp {
|
|
20
|
+
/** Resolve an attempted `from`→`to` XZ move to the destination actually allowed (slide-along). */
|
|
21
|
+
clampMove(from: readonly [number, number], to: readonly [number, number]): readonly [number, number];
|
|
22
|
+
/** Impact recorded since last take, consumed on read (null when none). Closing speed is the velocity component into the obstacle at block time, units/s. */
|
|
23
|
+
takeImpact(): VehicleImpact | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a slide-along move clamp for a kinematic car (#1051). `obstacles` is sampled fresh each tick —
|
|
27
|
+
* the caller hands back the already-filtered set of solids near the car — and `dt` supplies the current
|
|
28
|
+
* tick length so a blocked move's lost displacement converts to a closing speed. `radius` inflates each
|
|
29
|
+
* obstacle footprint by the car's body radius (default {@link DEFAULT_VEHICLE_RADIUS}).
|
|
30
|
+
*
|
|
31
|
+
* The slide itself is delegated to {@link resolveObstacleStep} so cars and pedestrians share one proven
|
|
32
|
+
* axis-separated resolver: a shallow graze keeps its tangential run and only the into-wall component is
|
|
33
|
+
* shed, a head-on zeroes forward progress. When any component is shed the largest such block this tick is
|
|
34
|
+
* kept as a {@link VehicleImpact}; {@link VehicleObstacleClamp.takeImpact} returns and clears it so a hit
|
|
35
|
+
* fires exactly once. The happy path (no solids near) is a zero-allocation passthrough returning `to`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createVehicleObstacleClamp(options: {
|
|
38
|
+
/** Solids near the car this tick (already filtered to the relevant, solid set). */
|
|
39
|
+
obstacles: () => readonly CollisionObstacle[];
|
|
40
|
+
/** Vehicle body radius (units). Default {@link DEFAULT_VEHICLE_RADIUS}. */
|
|
41
|
+
radius?: number;
|
|
42
|
+
/** Current tick dt (seconds) — the divisor turning shed displacement into closing speed. */
|
|
43
|
+
dt: () => number;
|
|
44
|
+
}): VehicleObstacleClamp;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { resolveObstacleStep } from "../movement/movementModel.js";
|
|
2
|
+
/** Matches the unit-box default {@link resolveObstacleStep} applies to obstacles without explicit extents. */
|
|
3
|
+
const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
4
|
+
/** Default vehicle body radius (units) inflating obstacle footprints; a compact hatchback-ish disc. */
|
|
5
|
+
const DEFAULT_VEHICLE_RADIUS = 1.4;
|
|
6
|
+
/**
|
|
7
|
+
* Y the car body is pinned to when handing solids to {@link resolveObstacleStep}. That stepper gates
|
|
8
|
+
* each obstacle by a 1.8-tall pedestrian span around `current[1]`; centring every pre-filtered solid
|
|
9
|
+
* on this value makes the gate always admit them, so collision is purely horizontal and the car hits
|
|
10
|
+
* world solids at any ground elevation (bridges, hills) even though the clamp only knows XZ.
|
|
11
|
+
*/
|
|
12
|
+
const CAR_BODY_MID_Y = 0.9;
|
|
13
|
+
/** Below this the returned step is treated as unblocked (float-noise slack), so no phantom impacts fire. */
|
|
14
|
+
const BLOCK_EPSILON = 1e-6;
|
|
15
|
+
/**
|
|
16
|
+
* Build a slide-along move clamp for a kinematic car (#1051). `obstacles` is sampled fresh each tick —
|
|
17
|
+
* the caller hands back the already-filtered set of solids near the car — and `dt` supplies the current
|
|
18
|
+
* tick length so a blocked move's lost displacement converts to a closing speed. `radius` inflates each
|
|
19
|
+
* obstacle footprint by the car's body radius (default {@link DEFAULT_VEHICLE_RADIUS}).
|
|
20
|
+
*
|
|
21
|
+
* The slide itself is delegated to {@link resolveObstacleStep} so cars and pedestrians share one proven
|
|
22
|
+
* axis-separated resolver: a shallow graze keeps its tangential run and only the into-wall component is
|
|
23
|
+
* shed, a head-on zeroes forward progress. When any component is shed the largest such block this tick is
|
|
24
|
+
* kept as a {@link VehicleImpact}; {@link VehicleObstacleClamp.takeImpact} returns and clears it so a hit
|
|
25
|
+
* fires exactly once. The happy path (no solids near) is a zero-allocation passthrough returning `to`.
|
|
26
|
+
*/
|
|
27
|
+
export function createVehicleObstacleClamp(options) {
|
|
28
|
+
const radius = options.radius ?? DEFAULT_VEHICLE_RADIUS;
|
|
29
|
+
// Grow-only scratch of Y-normalised solids, reused across ticks so a sustained crash allocates nothing
|
|
30
|
+
// after warm-up. Per-clamp (closure-local) so concurrent cars never share it.
|
|
31
|
+
const scratch = [];
|
|
32
|
+
let pending = null;
|
|
33
|
+
function vehicleView(source) {
|
|
34
|
+
const n = source.length;
|
|
35
|
+
for (let i = 0; i < n; i += 1) {
|
|
36
|
+
let entry = scratch[i];
|
|
37
|
+
if (entry === undefined) {
|
|
38
|
+
entry = { position: [0, 0, 0] };
|
|
39
|
+
scratch[i] = entry;
|
|
40
|
+
}
|
|
41
|
+
const from = source[i];
|
|
42
|
+
entry.position[0] = from.position[0];
|
|
43
|
+
entry.position[1] = CAR_BODY_MID_Y;
|
|
44
|
+
entry.position[2] = from.position[2];
|
|
45
|
+
entry.halfExtents = from.halfExtents;
|
|
46
|
+
}
|
|
47
|
+
if (scratch.length > n)
|
|
48
|
+
scratch.length = n;
|
|
49
|
+
return scratch;
|
|
50
|
+
}
|
|
51
|
+
/** Obstacle the attempted endpoint penetrates deepest (its inflated footprint), else nearest centre. */
|
|
52
|
+
function blockingIndex(source, tx, tz) {
|
|
53
|
+
let bestIndex = -1;
|
|
54
|
+
let bestDepth = -Number.POSITIVE_INFINITY;
|
|
55
|
+
let fallbackIndex = 0;
|
|
56
|
+
let fallbackDistSq = Number.POSITIVE_INFINITY;
|
|
57
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
58
|
+
const o = source[i];
|
|
59
|
+
const half = o.halfExtents ?? DEFAULT_OBSTACLE_HALF_EXTENTS;
|
|
60
|
+
const ex = half[0] + radius;
|
|
61
|
+
const ez = half[2] + radius;
|
|
62
|
+
const minX = o.position[0] - ex;
|
|
63
|
+
const maxX = o.position[0] + ex;
|
|
64
|
+
const minZ = o.position[2] - ez;
|
|
65
|
+
const maxZ = o.position[2] + ez;
|
|
66
|
+
if (tx > minX && tx < maxX && tz > minZ && tz < maxZ) {
|
|
67
|
+
const depth = Math.min(tx - minX, maxX - tx, tz - minZ, maxZ - tz);
|
|
68
|
+
if (depth > bestDepth) {
|
|
69
|
+
bestDepth = depth;
|
|
70
|
+
bestIndex = i;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const dx = o.position[0] - tx;
|
|
74
|
+
const dz = o.position[2] - tz;
|
|
75
|
+
const distSq = dx * dx + dz * dz;
|
|
76
|
+
if (distSq < fallbackDistSq) {
|
|
77
|
+
fallbackDistSq = distSq;
|
|
78
|
+
fallbackIndex = i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return bestIndex >= 0 ? bestIndex : fallbackIndex;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
clampMove(from, to) {
|
|
85
|
+
const obstacles = options.obstacles();
|
|
86
|
+
if (obstacles.length === 0)
|
|
87
|
+
return to;
|
|
88
|
+
const attemptedX = to[0] - from[0];
|
|
89
|
+
const attemptedZ = to[1] - from[1];
|
|
90
|
+
const step = resolveObstacleStep([from[0], CAR_BODY_MID_Y, from[1]], attemptedX, attemptedZ, vehicleView(obstacles), radius);
|
|
91
|
+
const blockedX = attemptedX - step.stepX;
|
|
92
|
+
const blockedZ = attemptedZ - step.stepZ;
|
|
93
|
+
if (blockedX === 0 && blockedZ === 0)
|
|
94
|
+
return to;
|
|
95
|
+
const blockedMag = Math.hypot(blockedX, blockedZ);
|
|
96
|
+
if (blockedMag > BLOCK_EPSILON) {
|
|
97
|
+
const dt = options.dt();
|
|
98
|
+
const closingSpeed = dt > 0 ? blockedMag / dt : 0;
|
|
99
|
+
if (pending === null || closingSpeed > pending.closingSpeed) {
|
|
100
|
+
pending = {
|
|
101
|
+
closingSpeed,
|
|
102
|
+
normal: [-blockedX / blockedMag, -blockedZ / blockedMag],
|
|
103
|
+
obstacleIndex: blockingIndex(obstacles, to[0], to[1]),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [from[0] + step.stepX, from[1] + step.stepZ];
|
|
108
|
+
},
|
|
109
|
+
takeImpact() {
|
|
110
|
+
const impact = pending;
|
|
111
|
+
pending = null;
|
|
112
|
+
return impact;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/random/rng.d.ts
CHANGED
|
@@ -20,7 +20,10 @@ export declare function randomSeedFrom(seed: number): RandomSeed;
|
|
|
20
20
|
* instead of closing over a generator.
|
|
21
21
|
*/
|
|
22
22
|
export declare function stepRandomSeed(seed: RandomSeed): readonly [value: number, next: RandomSeed];
|
|
23
|
-
/** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
23
|
+
/** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
24
|
+
*
|
|
25
|
+
* @capability seeded-random injected deterministic randomness for game logic — never `Math.random` in a simulation
|
|
26
|
+
*/
|
|
24
27
|
export declare function seededRng(seed: string | number): () => number;
|
|
25
28
|
/** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
|
|
26
29
|
export declare function seededStreams(seed: string | number): (stream: string) => () => number;
|
package/dist/random/rng.js
CHANGED
|
@@ -29,7 +29,10 @@ export function stepRandomSeed(seed) {
|
|
|
29
29
|
const value = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
30
30
|
return [value, next];
|
|
31
31
|
}
|
|
32
|
-
/** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
32
|
+
/** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
33
|
+
*
|
|
34
|
+
* @capability seeded-random injected deterministic randomness for game logic — never `Math.random` in a simulation
|
|
35
|
+
*/
|
|
33
36
|
export function seededRng(seed) {
|
|
34
37
|
let cursor = randomSeedFrom(hashSeed(seed));
|
|
35
38
|
return () => {
|
|
@@ -56,6 +56,8 @@ export declare function convexPresence(config?: {
|
|
|
56
56
|
* WebSocket transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer
|
|
57
57
|
* `wsPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared,
|
|
58
58
|
* host-authoritative world — see `examples/HOSTED.md`.
|
|
59
|
+
*
|
|
60
|
+
* @capability multiplayer-ws server-hosted shared world over WebSocket — `multiplayer: ws({ authority: "server" })`
|
|
59
61
|
*/
|
|
60
62
|
export declare function ws(config?: {
|
|
61
63
|
topology?: MultiplayerTopology;
|
|
@@ -78,6 +80,11 @@ export declare function socketIo(config?: {
|
|
|
78
80
|
url?: string;
|
|
79
81
|
authority?: MultiplayerAuthority;
|
|
80
82
|
}): MultiplayerAdapterConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Serverless peer-to-peer (WebRTC) session — one peer hosts, friends join by room code.
|
|
85
|
+
*
|
|
86
|
+
* @capability multiplayer-p2p serverless co-op over WebRTC — `multiplayer: p2p({ room })`
|
|
87
|
+
*/
|
|
81
88
|
export declare function p2p(config?: {
|
|
82
89
|
topology?: MultiplayerTopology;
|
|
83
90
|
room?: string;
|
|
@@ -105,6 +112,12 @@ export declare function resolveAuthority(multiplayer: unknown): MultiplayerAutho
|
|
|
105
112
|
export declare function isPresenceOnly(multiplayer: unknown): boolean;
|
|
106
113
|
/** True for a single-player world — no adapter, or an explicit `offline()` one. Gates offline-only wiring like local whole-world save. */
|
|
107
114
|
export declare function isOffline(multiplayer: unknown): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell
|
|
117
|
+
* `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
|
|
118
|
+
*
|
|
119
|
+
* @capability multiplayer-offline explicit solo adapter — omit `multiplayer` instead; offline is the shell default
|
|
120
|
+
*/
|
|
108
121
|
export declare function offline(): MultiplayerAdapterConfig;
|
|
109
122
|
export declare function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null;
|
|
110
123
|
export declare function multiplayerAdapterKind(multiplayer: unknown): string | null;
|
package/dist/runtime/adapter.js
CHANGED
|
@@ -14,6 +14,8 @@ export function convexPresence(config) {
|
|
|
14
14
|
* WebSocket transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer
|
|
15
15
|
* `wsPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared,
|
|
16
16
|
* host-authoritative world — see `examples/HOSTED.md`.
|
|
17
|
+
*
|
|
18
|
+
* @capability multiplayer-ws server-hosted shared world over WebSocket — `multiplayer: ws({ authority: "server" })`
|
|
17
19
|
*/
|
|
18
20
|
export function ws(config) {
|
|
19
21
|
return { kind: "ws", topology: config?.topology ?? "shared", url: config?.url, authority: config?.authority };
|
|
@@ -33,6 +35,11 @@ export function fly(config) {
|
|
|
33
35
|
export function socketIo(config) {
|
|
34
36
|
return { kind: "socketio", topology: config?.topology ?? "shared", url: config?.url, authority: config?.authority };
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Serverless peer-to-peer (WebRTC) session — one peer hosts, friends join by room code.
|
|
40
|
+
*
|
|
41
|
+
* @capability multiplayer-p2p serverless co-op over WebRTC — `multiplayer: p2p({ room })`
|
|
42
|
+
*/
|
|
36
43
|
export function p2p(config) {
|
|
37
44
|
return { kind: "p2p", topology: config?.topology ?? "private", room: config?.room, authority: config?.authority };
|
|
38
45
|
}
|
|
@@ -69,6 +76,12 @@ export function isOffline(multiplayer) {
|
|
|
69
76
|
const adapter = adapterOf(multiplayer);
|
|
70
77
|
return adapter === null || adapter.kind === "offline";
|
|
71
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell
|
|
81
|
+
* `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
|
|
82
|
+
*
|
|
83
|
+
* @capability multiplayer-offline explicit solo adapter — omit `multiplayer` instead; offline is the shell default
|
|
84
|
+
*/
|
|
72
85
|
export function offline() {
|
|
73
86
|
return { kind: "offline" };
|
|
74
87
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type DeathSystem } from "../../combat/death.js";
|
|
2
|
+
import { type CombatSpatialDeps, type EffectSystem } from "../../combat/effects.js";
|
|
3
|
+
import { type ProjectileSystem } from "../../combat/projectiles.js";
|
|
4
|
+
import type { PhysicsConfig } from "../../game/defineGame.js";
|
|
5
|
+
import type { GameEvents } from "../../game/events.js";
|
|
6
|
+
import { type VfxInstanceStore } from "../../game/vfxInstance.js";
|
|
7
|
+
import type { LootRegistry } from "../../game/lootTable.js";
|
|
8
|
+
import type { EntityColliderSet } from "../../scene/colliders.js";
|
|
9
|
+
import type { EntityStore } from "../../scene/entityStore.js";
|
|
10
|
+
import type { ObjectStore } from "../../scene/objectStore.js";
|
|
11
|
+
import type { SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
12
|
+
import type { WeaponStats } from "../../item/weapon.js";
|
|
13
|
+
import type { SimClock } from "../../time/simClock.js";
|
|
14
|
+
import type { WorldItemRecord, WorldItemSpawnInput } from "../../game/worldItem.js";
|
|
15
|
+
import type { GameContextContent, GameContextEntityEntry, GameContextLoot } from "../gameContext.js";
|
|
16
|
+
import { type CombatFx } from "./combatFx.js";
|
|
17
|
+
/** @internal Wiring combat needs from the live scene, loot, and command seams. */
|
|
18
|
+
export interface CombatSubsystemDeps {
|
|
19
|
+
content: GameContextContent;
|
|
20
|
+
signalNotify: () => void;
|
|
21
|
+
now: () => number;
|
|
22
|
+
events: GameEvents;
|
|
23
|
+
time: SimClock;
|
|
24
|
+
entities: EntityStore;
|
|
25
|
+
objects: ObjectStore;
|
|
26
|
+
combatSpatial: CombatSpatialDeps;
|
|
27
|
+
sceneRaycast: SceneRaycastApi;
|
|
28
|
+
entityCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
29
|
+
objectCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
30
|
+
catalogEntry: (instanceId: string) => GameContextEntityEntry | null | undefined;
|
|
31
|
+
statsByInstance: Map<string, import("../../scene/entityStats.js").StatValueMap>;
|
|
32
|
+
weapon: WeaponStats;
|
|
33
|
+
loot: GameContextLoot;
|
|
34
|
+
lootRegistry: LootRegistry;
|
|
35
|
+
spawnWorldItem: (input: WorldItemSpawnInput) => WorldItemRecord;
|
|
36
|
+
despawnEntity: (instanceId: string) => boolean;
|
|
37
|
+
runCommand: (name: string, args: unknown) => void;
|
|
38
|
+
localUserId: string;
|
|
39
|
+
physics?: PhysicsConfig;
|
|
40
|
+
}
|
|
41
|
+
/** @internal Effects, projectiles, death, and combat presentation surface. */
|
|
42
|
+
export interface CombatSubsystem {
|
|
43
|
+
death: DeathSystem;
|
|
44
|
+
effects: EffectSystem;
|
|
45
|
+
floatingEffects: EffectSystem;
|
|
46
|
+
projectiles: ProjectileSystem;
|
|
47
|
+
combatFx: CombatFx;
|
|
48
|
+
vfxInstances: VfxInstanceStore;
|
|
49
|
+
}
|
|
50
|
+
/** @internal */
|
|
51
|
+
export declare function createCombatSubsystem(d: CombatSubsystemDeps): CombatSubsystem;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createDeathSystem, deathReasonFromEffect } from "../../combat/death.js";
|
|
2
|
+
import { createEffectSystem, } from "../../combat/effects.js";
|
|
3
|
+
import { createProjectileSystem } from "../../combat/projectiles.js";
|
|
4
|
+
import { createVfxInstanceStore } from "../../game/vfxInstance.js";
|
|
5
|
+
import { createEntityStatsApi } from "../../scene/entityStats.js";
|
|
6
|
+
import { notifyAfter } from "../../store/changeSignal.js";
|
|
7
|
+
import { createCombatFx } from "./combatFx.js";
|
|
8
|
+
import { applyLethalLoot, isLocalPlayerKill } from "./deathLoot.js";
|
|
9
|
+
/** @internal */
|
|
10
|
+
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, localUserId, } = d;
|
|
12
|
+
const death = createDeathSystem({
|
|
13
|
+
resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
|
|
14
|
+
resolveIdentity(instanceId) {
|
|
15
|
+
const entity = entities.get(instanceId);
|
|
16
|
+
if (entity === null)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
catalogId: entity.name,
|
|
20
|
+
position: [entity.position[0], entity.position[1], entity.position[2]],
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
loot: { roll: (tableId) => (lootRegistry.has(tableId) ? lootRegistry.roll(tableId) : []) },
|
|
24
|
+
events,
|
|
25
|
+
runCommand(name, args) {
|
|
26
|
+
runCommand(name, args);
|
|
27
|
+
},
|
|
28
|
+
despawn(instanceId) {
|
|
29
|
+
despawnEntity(instanceId);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const effects = notifyAfter(createEffectSystem({
|
|
33
|
+
resolveReceive: (instanceId) => catalogEntry(instanceId)?.receive,
|
|
34
|
+
statPools: createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)),
|
|
35
|
+
getStat: weapon.getStat,
|
|
36
|
+
spatial: combatSpatial,
|
|
37
|
+
onLethal(instanceId, lethalCtx) {
|
|
38
|
+
// Capture identity + onDeath *before* resolveDeath despawns the entity.
|
|
39
|
+
const dyingEntity = entities.get(instanceId);
|
|
40
|
+
const catalogId = dyingEntity?.name;
|
|
41
|
+
const position = dyingEntity?.position;
|
|
42
|
+
const onDeath = catalogEntry(instanceId)?.onDeath;
|
|
43
|
+
const reason = deathReasonFromEffect({
|
|
44
|
+
...lethalCtx,
|
|
45
|
+
userIdOf: (id) => (id === localUserId ? localUserId : undefined),
|
|
46
|
+
});
|
|
47
|
+
const resolution = death.resolveDeath(instanceId, reason);
|
|
48
|
+
if (resolution.status !== "resolved")
|
|
49
|
+
return;
|
|
50
|
+
applyLethalLoot({
|
|
51
|
+
drops: resolution.drops,
|
|
52
|
+
grantToLocalPlayer: isLocalPlayerKill(lethalCtx, localUserId),
|
|
53
|
+
onDeath,
|
|
54
|
+
position,
|
|
55
|
+
catalogId,
|
|
56
|
+
content,
|
|
57
|
+
spawnWorldItem,
|
|
58
|
+
grantToPlayer: loot.grantToPlayer,
|
|
59
|
+
localUserId,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
}), ["applyEffect"], signalNotify);
|
|
63
|
+
const combatFx = createCombatFx({
|
|
64
|
+
entities,
|
|
65
|
+
events,
|
|
66
|
+
time,
|
|
67
|
+
applyEffect: (input) => effects.applyEffect(input),
|
|
68
|
+
});
|
|
69
|
+
const vfxInstances = createVfxInstanceStore({
|
|
70
|
+
onOp: (op) => events.emit("combat.vfxInstance", op),
|
|
71
|
+
now: () => time.now() * 1000,
|
|
72
|
+
});
|
|
73
|
+
const floatingEffects = {
|
|
74
|
+
canReceive: effects.canReceive,
|
|
75
|
+
preview: effects.preview,
|
|
76
|
+
applyEffect: combatFx.applyEffectAndFloat,
|
|
77
|
+
};
|
|
78
|
+
const projectileObstacles = d.physics?.projectileObstacles === true;
|
|
79
|
+
const projectileFilterFor = (filter) => ({
|
|
80
|
+
entities: true,
|
|
81
|
+
objects: projectileObstacles,
|
|
82
|
+
terrain: false,
|
|
83
|
+
walls: projectileObstacles,
|
|
84
|
+
...filter,
|
|
85
|
+
});
|
|
86
|
+
const projectileSceneRaycast = {
|
|
87
|
+
raycast(input) {
|
|
88
|
+
return sceneRaycast.raycast({ ...input, filter: projectileFilterFor(input.filter) });
|
|
89
|
+
},
|
|
90
|
+
raycastAll(input) {
|
|
91
|
+
return sceneRaycast.raycastAll({ ...input, filter: projectileFilterFor(input.filter) });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const projectiles = notifyAfter(createProjectileSystem({
|
|
95
|
+
effects: floatingEffects,
|
|
96
|
+
spatial: combatSpatial,
|
|
97
|
+
getStat: weapon.getStat,
|
|
98
|
+
sceneRaycast: projectileSceneRaycast,
|
|
99
|
+
objects: projectileObstacles
|
|
100
|
+
? {
|
|
101
|
+
list: () => objects.list(),
|
|
102
|
+
inBox: (min, max) => objects.inBox(min, max),
|
|
103
|
+
halfExtents: (catalogId) => {
|
|
104
|
+
const half = content.objectById?.(catalogId)?.halfExtents;
|
|
105
|
+
return half === undefined ? null : [half[0], half[1], half[2]];
|
|
106
|
+
},
|
|
107
|
+
collidersOf: objectCollidersOf,
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
entityCollidersOf,
|
|
111
|
+
rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
|
|
112
|
+
now,
|
|
113
|
+
onSettle(report) {
|
|
114
|
+
events.emit("projectile.settled", {
|
|
115
|
+
from: report.from,
|
|
116
|
+
origin: [report.origin[0], report.origin[1], report.origin[2]],
|
|
117
|
+
at: [report.at[0], report.at[1], report.at[2]],
|
|
118
|
+
effect: report.effect,
|
|
119
|
+
hit: report.hit,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
}), ["fireProjectile", "settleProjectile"], signalNotify);
|
|
123
|
+
return {
|
|
124
|
+
death,
|
|
125
|
+
effects,
|
|
126
|
+
floatingEffects,
|
|
127
|
+
projectiles,
|
|
128
|
+
combatFx,
|
|
129
|
+
vfxInstances,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveHitReaction } from "../../combat/hitReaction.js";
|
|
2
2
|
import { pointInTelegraph } from "../../combat/telegraph.js";
|
|
3
|
+
import { resolveVfxPreset } from "../../combat/vfxPresets.js";
|
|
3
4
|
/** @internal */
|
|
4
5
|
export function createCombatFx(d) {
|
|
5
6
|
const { entities, events, time } = d;
|
|
@@ -52,17 +53,26 @@ export function createCombatFx(d) {
|
|
|
52
53
|
const from = resolveVfxPoint(input.from) ?? to;
|
|
53
54
|
if (from === undefined)
|
|
54
55
|
return;
|
|
56
|
+
// A named preset fills in kind/color/defaults; any field on the call still wins. With no
|
|
57
|
+
// preset and no explicit kind/color the resolver falls back to a visible spark rather than
|
|
58
|
+
// emitting an invalid (blank) burst — so a bare `vfx({ from, to })` still shows something.
|
|
59
|
+
const resolved = resolveVfxPreset(input.preset, {
|
|
60
|
+
...(input.kind === undefined ? {} : { kind: input.kind }),
|
|
61
|
+
...(input.color === undefined ? {} : { color: input.color }),
|
|
62
|
+
...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
|
|
63
|
+
...(input.radius === undefined ? {} : { radius: input.radius }),
|
|
64
|
+
});
|
|
55
65
|
const event = {
|
|
56
66
|
id: vfxSeq++,
|
|
57
|
-
kind:
|
|
58
|
-
color:
|
|
67
|
+
kind: resolved.kind,
|
|
68
|
+
color: resolved.color,
|
|
59
69
|
from,
|
|
60
|
-
durationMs:
|
|
70
|
+
durationMs: resolved.durationMs ?? vfxDefaultDurationMs[resolved.kind],
|
|
61
71
|
};
|
|
62
72
|
if (to !== undefined)
|
|
63
73
|
event.to = to;
|
|
64
|
-
if (
|
|
65
|
-
event.radius =
|
|
74
|
+
if (resolved.radius !== undefined)
|
|
75
|
+
event.radius = resolved.radius;
|
|
66
76
|
events.emit("combat.vfx", event);
|
|
67
77
|
}
|
|
68
78
|
let telegraphSeq = 0;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type EffectDeathContext, type OnDeathSpec } from "../../combat/death.js";
|
|
2
|
+
import type { Drop } from "../../game/lootTable.js";
|
|
3
|
+
import { type WorldItemSpawnInput } from "../../game/worldItem.js";
|
|
4
|
+
import type { GameContextContent } from "../gameContext.js";
|
|
5
|
+
/** @internal Inputs for routing a resolved lethal kill into bag grants and/or ground-item spawns. */
|
|
6
|
+
export interface LethalLootInput {
|
|
7
|
+
/** Drops produced by the death system's resolution (already rolled). */
|
|
8
|
+
drops: readonly Drop[];
|
|
9
|
+
/** True when the killer is the local/host player this context is resolving loot for. */
|
|
10
|
+
grantToLocalPlayer: boolean;
|
|
11
|
+
/** Catalog `onDeath` for the dying entity (drop mode + scatter). */
|
|
12
|
+
onDeath: OnDeathSpec | undefined;
|
|
13
|
+
/** World position of the dying entity — required for `dropMode: "world"`. */
|
|
14
|
+
position: readonly [number, number, number] | undefined;
|
|
15
|
+
/** Catalog id of the dying entity — used as loot source tag when present. */
|
|
16
|
+
catalogId: string | undefined;
|
|
17
|
+
content: GameContextContent;
|
|
18
|
+
spawnWorldItem: (input: WorldItemSpawnInput) => void;
|
|
19
|
+
grantToPlayer: (userId: string, drops: Drop[], source?: string) => void;
|
|
20
|
+
localUserId: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
|
|
24
|
+
* world items (`dropMode: "world"`) or grant straight into bags. Extracted from `createGameContext`
|
|
25
|
+
* so combat install stays free of nested loot branching.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyLethalLoot(input: LethalLootInput): void;
|
|
29
|
+
/** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
|
|
30
|
+
export declare function isLocalPlayerKill(lethalCtx: EffectDeathContext, localUserId: string): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { deathReasonFromEffect, normalizeOnDeath, } from "../../combat/death.js";
|
|
2
|
+
import { DEFAULT_RARITY, resolveDeathDrops, } from "../../game/worldItem.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
|
|
5
|
+
* world items (`dropMode: "world"`) or grant straight into bags. Extracted from `createGameContext`
|
|
6
|
+
* so combat install stays free of nested loot branching.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function applyLethalLoot(input) {
|
|
10
|
+
if (input.drops.length === 0 || !input.grantToLocalPlayer)
|
|
11
|
+
return;
|
|
12
|
+
const normalizedOnDeath = normalizeOnDeath(input.onDeath);
|
|
13
|
+
if (normalizedOnDeath.dropMode === "world" && input.position !== undefined) {
|
|
14
|
+
const resolved = resolveDeathDrops([...input.drops], {
|
|
15
|
+
mode: "world",
|
|
16
|
+
origin: input.position,
|
|
17
|
+
resolveRarity: (itemId) => input.content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
|
|
18
|
+
resolveBaseType: (itemId) => input.content.itemById?.(itemId)?.baseType ?? itemId,
|
|
19
|
+
scatter: normalizedOnDeath.scatter,
|
|
20
|
+
...(input.catalogId !== undefined ? { source: input.catalogId } : {}),
|
|
21
|
+
});
|
|
22
|
+
for (const spawn of resolved.worldSpawns)
|
|
23
|
+
input.spawnWorldItem(spawn);
|
|
24
|
+
if (resolved.grants.length > 0) {
|
|
25
|
+
input.grantToPlayer(input.localUserId, resolved.grants, input.catalogId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
input.grantToPlayer(input.localUserId, [...input.drops], input.catalogId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** @internal Build the grant-to-local-player flag from a lethal effect context + local user id. */
|
|
33
|
+
export function isLocalPlayerKill(lethalCtx, localUserId) {
|
|
34
|
+
const reason = deathReasonFromEffect({
|
|
35
|
+
...lethalCtx,
|
|
36
|
+
userIdOf: (id) => (id === localUserId ? localUserId : undefined),
|
|
37
|
+
});
|
|
38
|
+
return reason.kind === "player_kill" && reason.killerUserId === localUserId;
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type WalletState } from "../../economy/wallet.js";
|
|
2
|
+
import type { GameFeatures } from "../../game/defineGame.js";
|
|
3
|
+
import type { InventoryDeclaration } from "../../game/defineGame.js";
|
|
4
|
+
import type { GameEvents } from "../../game/events.js";
|
|
5
|
+
import { type Loadouts } from "../../game/loadout.js";
|
|
6
|
+
import { type InventoryLayout, type InventorySet } from "../../inventory/inventoryModel.js";
|
|
7
|
+
import { type PoseState } from "../../movement/poseState.js";
|
|
8
|
+
import { type Possession } from "../../scene/possession.js";
|
|
9
|
+
import type { EntityStore } from "../../scene/entityStore.js";
|
|
10
|
+
import { type StatValueMap } from "../../scene/entityStats.js";
|
|
11
|
+
import { type Stats } from "../../stats/statModifiers.js";
|
|
12
|
+
import { type MotionIntents } from "../motionIntents.js";
|
|
13
|
+
import type { GameContextEconomy, GameContextEntityEntry } from "../gameContext.js";
|
|
14
|
+
/** @internal Wiring player-owned state needs from the live context. */
|
|
15
|
+
export interface PlayerSubsystemDeps {
|
|
16
|
+
signalNotify: () => void;
|
|
17
|
+
inventoryDeclarations: Record<string, InventoryDeclaration>;
|
|
18
|
+
entities: EntityStore;
|
|
19
|
+
events: GameEvents;
|
|
20
|
+
catalogEntry: (instanceId: string) => GameContextEntityEntry | null | undefined;
|
|
21
|
+
ensureInstanceStats: (instanceId: string) => StatValueMap;
|
|
22
|
+
/** Reads another feature after descriptor install (loadout → unlocks). */
|
|
23
|
+
feature: <T>(key: keyof GameFeatures) => T | undefined;
|
|
24
|
+
}
|
|
25
|
+
/** @internal Inventory, wallets, pose, motion, loadout — per-player state surfaces. */
|
|
26
|
+
export interface PlayerSubsystem {
|
|
27
|
+
inventoryIds: readonly string[];
|
|
28
|
+
inventoryByUser: ReadonlyMap<string, InventorySet<string>>;
|
|
29
|
+
inventoryFor: (userId: string) => InventorySet<string>;
|
|
30
|
+
layouts: Record<string, InventoryLayout>;
|
|
31
|
+
wallets: Map<string, WalletState>;
|
|
32
|
+
walletOf: (userId: string) => WalletState;
|
|
33
|
+
economy: GameContextEconomy;
|
|
34
|
+
putIntoAnyInventory: (userId: string, itemId: string, count: number) => void;
|
|
35
|
+
playerStatsFor: (userId: string) => Stats<string>;
|
|
36
|
+
pose: PoseState;
|
|
37
|
+
possession: Possession;
|
|
38
|
+
motionByUser: ReadonlyMap<string, MotionIntents>;
|
|
39
|
+
motionFor: (userId: string) => MotionIntents;
|
|
40
|
+
loadouts: Loadouts;
|
|
41
|
+
seedUserPool: (userId: string, statId: string, pool: {
|
|
42
|
+
current: number;
|
|
43
|
+
max?: number;
|
|
44
|
+
min?: number;
|
|
45
|
+
}) => void;
|
|
46
|
+
}
|
|
47
|
+
/** @internal */
|
|
48
|
+
export declare function createPlayerSubsystem(d: PlayerSubsystemDeps): PlayerSubsystem;
|