@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,197 @@
|
|
|
1
|
+
# Apply damage to caller-owned resources
|
|
2
|
+
|
|
3
|
+
Use this recipe when an existing JavaScript/TypeScript game wants JGengine's
|
|
4
|
+
damage math, ordered interceptors, status resolution, shield spillover, and
|
|
5
|
+
lethal result without adopting `GameContext`, the entity store, renderer, React,
|
|
6
|
+
or JGengine's game loop.
|
|
7
|
+
|
|
8
|
+
## Install and focused imports
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add @jgengine/core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import {
|
|
16
|
+
createEffectSystem,
|
|
17
|
+
type ReceiveMap,
|
|
18
|
+
} from "@jgengine/core/combat/effects";
|
|
19
|
+
import {
|
|
20
|
+
createDamageClamp,
|
|
21
|
+
resolveDamage,
|
|
22
|
+
} from "@jgengine/core/combat/damageInterceptors";
|
|
23
|
+
import { resolveDamageHit } from "@jgengine/core/combat/damageResolution";
|
|
24
|
+
import {
|
|
25
|
+
createStatPool,
|
|
26
|
+
type StatPool,
|
|
27
|
+
type StatPoolAccess,
|
|
28
|
+
} from "@jgengine/core/stats/statPool";
|
|
29
|
+
import { seededRng } from "@jgengine/core/random/rng";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Caller-owned state and adapter
|
|
33
|
+
|
|
34
|
+
This example uses an immutable store-shaped object. A Redux, Zustand, ECS, or
|
|
35
|
+
custom store can implement the same two methods without copying its entities
|
|
36
|
+
into JGengine.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface UnitState {
|
|
40
|
+
pools: Record<string, StatPool>;
|
|
41
|
+
receive: ReceiveMap;
|
|
42
|
+
statuses: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ExistingSave {
|
|
46
|
+
units: Record<string, UnitState>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let save: ExistingSave = {
|
|
50
|
+
units: {
|
|
51
|
+
enemy_7: {
|
|
52
|
+
pools: {
|
|
53
|
+
shield: createStatPool({ current: 25, max: 25 }),
|
|
54
|
+
vitality: createStatPool({ current: 80, max: 80 }),
|
|
55
|
+
},
|
|
56
|
+
receive: { damage: { order: ["shield", "vitality"] } },
|
|
57
|
+
statuses: {},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const statPools: StatPoolAccess = {
|
|
63
|
+
get(ownerId, statId) {
|
|
64
|
+
return save.units[ownerId]?.pools[statId] ?? null;
|
|
65
|
+
},
|
|
66
|
+
set(ownerId, statId, next) {
|
|
67
|
+
const unit = save.units[ownerId];
|
|
68
|
+
if (unit === undefined) return false;
|
|
69
|
+
save = {
|
|
70
|
+
...save,
|
|
71
|
+
units: {
|
|
72
|
+
...save.units,
|
|
73
|
+
[ownerId]: { ...unit, pools: { ...unit.pools, [statId]: next } },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`set` receives a complete plain replacement pool. The SDK never asks for the
|
|
82
|
+
store itself and never mutates an entity object behind the adapter.
|
|
83
|
+
|
|
84
|
+
## Build the effect committer
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const existingEventQueue: unknown[] = [];
|
|
88
|
+
const existingEvents = {
|
|
89
|
+
emit(name: string, payload: unknown) {
|
|
90
|
+
existingEventQueue.push({ name, payload });
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const effects = createEffectSystem({
|
|
95
|
+
resolveReceive: (unitId) => save.units[unitId]?.receive,
|
|
96
|
+
statPools,
|
|
97
|
+
getStat: (_itemId, _statId) => null,
|
|
98
|
+
spatial: {
|
|
99
|
+
inRadius: (_center, _radius) => [],
|
|
100
|
+
hasLineOfSight: (_from, _to) => true,
|
|
101
|
+
positionOf: (_unitId) => undefined,
|
|
102
|
+
},
|
|
103
|
+
onLethal(unitId, context) {
|
|
104
|
+
existingEvents.emit("unit-defeated", { unitId, context });
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The spatial adapter is only used by area effects. Connect those three methods
|
|
110
|
+
to the existing project's spatial index and line-of-sight query when needed;
|
|
111
|
+
single-target effects do no world scan.
|
|
112
|
+
|
|
113
|
+
## Resolve and apply a hit
|
|
114
|
+
|
|
115
|
+
Keep resolution and commitment explicit. The pure stages return provenance and
|
|
116
|
+
status state; authority decides what to persist and then routes final
|
|
117
|
+
applications into the structural effect committer.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const seededRandom = seededRng("shot-2048");
|
|
121
|
+
const simulationNowMs = 4_000;
|
|
122
|
+
|
|
123
|
+
const hit = resolveDamageHit({
|
|
124
|
+
channel: "arc",
|
|
125
|
+
impact: 40,
|
|
126
|
+
source: "player",
|
|
127
|
+
target: "enemy_7",
|
|
128
|
+
targetTraits: ["shielded"],
|
|
129
|
+
status: { status: "charged", chance: 0.35, durationMs: 2_000 },
|
|
130
|
+
rng: seededRandom, // caller-owned deterministic stream
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const intercepted = resolveDamage(
|
|
134
|
+
[createDamageClamp({ id: "per-hit-cap", maxPerHit: 30 })],
|
|
135
|
+
{
|
|
136
|
+
source: "player",
|
|
137
|
+
target: "enemy_7",
|
|
138
|
+
amount: hit.impact,
|
|
139
|
+
tag: hit.channel,
|
|
140
|
+
},
|
|
141
|
+
{ nowMs: simulationNowMs }, // caller-owned deterministic clock
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
for (const application of intercepted.applications) {
|
|
145
|
+
effects.applyEffect({
|
|
146
|
+
from: application.source,
|
|
147
|
+
to: application.target,
|
|
148
|
+
effect: "damage",
|
|
149
|
+
via: { amount: application.amount },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (hit.status?.instance !== null && hit.status?.instance !== undefined) {
|
|
154
|
+
save.units.enemy_7!.statuses[hit.status.instance.status] = hit.status.instance;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`intercepted.provenance` records each ordered policy decision. `hit.matchup`,
|
|
159
|
+
`hit.received`, and `hit.status` retain the pure resolution evidence. The effect
|
|
160
|
+
result lists exact per-pool deltas in the receive order and reports lethality
|
|
161
|
+
when the final pool reaches its minimum.
|
|
162
|
+
|
|
163
|
+
## Save and restore
|
|
164
|
+
|
|
165
|
+
The authoritative pools and returned status instances are already plain data:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const encoded = JSON.stringify(save);
|
|
169
|
+
save = JSON.parse(encoded) as ExistingSave;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Recreate the small adapters over the restored store. Interceptor functions and
|
|
173
|
+
catalog rules are configuration, so re-supply them rather than serializing
|
|
174
|
+
closures. Persist any stateful interceptor records separately when their API
|
|
175
|
+
exposes serializable state.
|
|
176
|
+
|
|
177
|
+
## Ownership
|
|
178
|
+
|
|
179
|
+
The existing project retains entity identity, resource names, store updates,
|
|
180
|
+
save schema, status collection, authority, spatial queries, death/despawn,
|
|
181
|
+
events, clock, RNG, content, presentation, and its update loop. JGengine owns
|
|
182
|
+
the pure resolution stages, bounded ordered interception, pool spillover math,
|
|
183
|
+
and inspectable results.
|
|
184
|
+
|
|
185
|
+
## Common traps
|
|
186
|
+
|
|
187
|
+
- Use positive effect magnitudes to drain pools and negative magnitudes to
|
|
188
|
+
restore them; the receive rule determines which named pools participate.
|
|
189
|
+
- Put shields or other absorbers before the final lethal pool in `order`.
|
|
190
|
+
- Pass final interceptor applications to `applyEffect`; do not apply the
|
|
191
|
+
pre-interceptor impact as well.
|
|
192
|
+
- Persist the returned status `instance`; `resolveDamageHit` is intentionally
|
|
193
|
+
pure and does not hide a status store.
|
|
194
|
+
- Use a bounded spatial index for `inRadius`; do not add a full-world scan per
|
|
195
|
+
area effect.
|
|
196
|
+
- Keep audio, particles, hit flashes, animations, camera shake, and models in
|
|
197
|
+
the existing project. Drive them from the returned results and provenance.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Save and restore closure-backed combat runtimes
|
|
2
|
+
|
|
3
|
+
Use this recipe when an existing project wants JGengine's convenient magazine
|
|
4
|
+
or stat-modifier runtime but keeps its own save system, clock, entities, and
|
|
5
|
+
authority. Both runtimes expose detached plain snapshots; no JGengine store or
|
|
6
|
+
`GameContext` is required.
|
|
7
|
+
|
|
8
|
+
## Install and focused imports
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add @jgengine/core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import {
|
|
16
|
+
createMagazine,
|
|
17
|
+
type MagazineSnapshot,
|
|
18
|
+
} from "@jgengine/core/combat/magazine";
|
|
19
|
+
import {
|
|
20
|
+
createStats,
|
|
21
|
+
type StatsSnapshot,
|
|
22
|
+
} from "@jgengine/core/stats/statModifiers";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Caller-owned save data
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type VehicleStat = "speed" | "handling";
|
|
29
|
+
|
|
30
|
+
interface VehicleRuntimeSave {
|
|
31
|
+
magazine: MagazineSnapshot;
|
|
32
|
+
stats: StatsSnapshot<VehicleStat>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ExistingSave {
|
|
36
|
+
nowMs: number;
|
|
37
|
+
vehicle: VehicleRuntimeSave;
|
|
38
|
+
sharedAmmo: number;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The project owns this schema and decides when it is written, replicated,
|
|
43
|
+
rolled back, replayed, or transferred to a worker.
|
|
44
|
+
|
|
45
|
+
## Run and snapshot
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
let nowMs = 4_000;
|
|
49
|
+
let sharedAmmo = 18;
|
|
50
|
+
|
|
51
|
+
const magazine = createMagazine({
|
|
52
|
+
capacity: 6,
|
|
53
|
+
reloadMs: 900,
|
|
54
|
+
reserve: {
|
|
55
|
+
current: () => sharedAmmo,
|
|
56
|
+
spend(amount) {
|
|
57
|
+
if (amount > sharedAmmo) return false;
|
|
58
|
+
sharedAmmo -= amount;
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
gain(amount) {
|
|
62
|
+
sharedAmmo += amount;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const stats = createStats<VehicleStat>(
|
|
68
|
+
{ speed: 12, handling: 8 },
|
|
69
|
+
{ now: () => nowMs },
|
|
70
|
+
);
|
|
71
|
+
stats.addSource("boost", { speed: { multiply: 1.25 } }, { expiresAtMs: 7_000 });
|
|
72
|
+
|
|
73
|
+
magazine.fire(2);
|
|
74
|
+
magazine.startReload();
|
|
75
|
+
magazine.tick(0.3);
|
|
76
|
+
|
|
77
|
+
const save: ExistingSave = {
|
|
78
|
+
nowMs,
|
|
79
|
+
vehicle: {
|
|
80
|
+
magazine: magazine.snapshot(),
|
|
81
|
+
stats: stats.snapshot(),
|
|
82
|
+
},
|
|
83
|
+
sharedAmmo,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const serialized = JSON.stringify(save);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Snapshots are detached from the live closures. Mutating or continuing the
|
|
90
|
+
runtime after this point does not rewrite `save`.
|
|
91
|
+
|
|
92
|
+
## Restore and resume
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const decoded = JSON.parse(serialized) as ExistingSave;
|
|
96
|
+
nowMs = decoded.nowMs;
|
|
97
|
+
sharedAmmo = decoded.sharedAmmo; // restore caller-owned reserve first
|
|
98
|
+
|
|
99
|
+
const resumedMagazine = createMagazine({
|
|
100
|
+
capacity: 6,
|
|
101
|
+
reloadMs: 900,
|
|
102
|
+
loaded: 0,
|
|
103
|
+
reserve: {
|
|
104
|
+
current: () => sharedAmmo,
|
|
105
|
+
spend(amount) {
|
|
106
|
+
if (amount > sharedAmmo) return false;
|
|
107
|
+
sharedAmmo -= amount;
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
gain(amount) {
|
|
111
|
+
sharedAmmo += amount;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (!resumedMagazine.restore(decoded.vehicle.magazine)) {
|
|
116
|
+
throw new Error("magazine save does not match its reserve configuration");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resumedStats = createStats<VehicleStat>(
|
|
120
|
+
{ speed: 0, handling: 0 },
|
|
121
|
+
{ now: () => nowMs },
|
|
122
|
+
);
|
|
123
|
+
resumedStats.restore(decoded.vehicle.stats);
|
|
124
|
+
|
|
125
|
+
resumedMagazine.tick(0.6); // finishes the same in-progress 900ms reload
|
|
126
|
+
nowMs = 7_000;
|
|
127
|
+
resumedStats.get("speed"); // injected clock expires the boost deterministically
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For a self-managed numeric magazine reserve, the magazine snapshot contains
|
|
131
|
+
and restores that reserve directly. For a caller-owned `MagazineReserve`, save
|
|
132
|
+
and restore the caller's reserve first; `restore` returns `false` rather than
|
|
133
|
+
partially changing loaded/reload state when it cannot reconcile the reserve.
|
|
134
|
+
|
|
135
|
+
## Ownership
|
|
136
|
+
|
|
137
|
+
The existing project owns save versioning, entity identity, clock value,
|
|
138
|
+
reserve state, networking/rollback policy, content, and when ticks occur.
|
|
139
|
+
JGengine owns the closure convenience methods, snapshot encoding shape,
|
|
140
|
+
detached copies, and deterministic continuation once the caller supplies the
|
|
141
|
+
same config and clock.
|
|
142
|
+
|
|
143
|
+
## Common traps
|
|
144
|
+
|
|
145
|
+
- Re-supply immutable configuration such as magazine capacity/reload duration
|
|
146
|
+
and the stat id set when rebuilding a runtime; snapshots contain mutable
|
|
147
|
+
state, not content catalogs.
|
|
148
|
+
- Restore externally-owned reserve data before calling `Magazine.restore`.
|
|
149
|
+
- Persist the injected clock alongside expiring modifier snapshots or convert
|
|
150
|
+
expiry timestamps in the project's migration layer.
|
|
151
|
+
- Do not keep a live snapshot object as mutable state. Restore it into a runtime
|
|
152
|
+
and take a fresh snapshot for the next save.
|
|
153
|
+
- Check the boolean returned by `Magazine.restore`; reserve-kind mismatches and
|
|
154
|
+
unreconcilable external reserves are rejected without partial magazine state.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Use JGengine stat pools with an existing save model
|
|
2
|
+
|
|
3
|
+
Use this recipe when a project already owns its entities and persistence but
|
|
4
|
+
needs deterministic bounded resources such as shields, energy, stamina, XP, or
|
|
5
|
+
durability. Resource names and meaning remain caller data; the portable
|
|
6
|
+
contract has no privileged health field.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add @jgengine/core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Import the focused, renderer-free module:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
applyStatPoolDelta,
|
|
19
|
+
createStatPool,
|
|
20
|
+
type StatPool,
|
|
21
|
+
type StatPoolAccess,
|
|
22
|
+
} from "@jgengine/core/stats/statPool";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@jgengine/core` has no runtime dependencies and does not require a JGengine
|
|
26
|
+
entity store, game loop, renderer, React provider, or backend.
|
|
27
|
+
|
|
28
|
+
## Minimal adapter
|
|
29
|
+
|
|
30
|
+
Suppose the existing project saves resources inline on its own units:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
interface UnitSave {
|
|
34
|
+
id: string;
|
|
35
|
+
resources: Record<string, StatPool>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WorldSave {
|
|
39
|
+
units: Record<string, UnitSave>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const world: WorldSave = {
|
|
43
|
+
units: {
|
|
44
|
+
rover: {
|
|
45
|
+
id: "rover",
|
|
46
|
+
resources: {
|
|
47
|
+
energy: createStatPool({ current: 8, max: 10 }),
|
|
48
|
+
durability: createStatPool({ max: 40 }),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const resources: StatPoolAccess = {
|
|
55
|
+
get(ownerId, statId) {
|
|
56
|
+
return world.units[ownerId]?.resources[statId] ?? null;
|
|
57
|
+
},
|
|
58
|
+
set(ownerId, statId, next) {
|
|
59
|
+
const unit = world.units[ownerId];
|
|
60
|
+
if (unit !== undefined) unit.resources[statId] = next;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The adapter performs structural reads and complete replacement writes. It does
|
|
66
|
+
not expose the caller's store implementation or hand mutation authority to the
|
|
67
|
+
engine.
|
|
68
|
+
|
|
69
|
+
## Apply a resource change
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const spent = applyStatPoolDelta(resources, "rover", "energy", -3);
|
|
73
|
+
|
|
74
|
+
if (spent.status === "ok") {
|
|
75
|
+
console.log(spent.pool.current); // 5
|
|
76
|
+
console.log(spent.applied); // -3 (after clamping)
|
|
77
|
+
console.log(spent.hitMin); // false
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Positive amounts increase a pool and negative amounts decrease it. The result
|
|
82
|
+
records the previous and next values, the exact clamped amount, and whether a
|
|
83
|
+
bound was reached. Missing owners or stat ids return a rejected result without
|
|
84
|
+
writing.
|
|
85
|
+
|
|
86
|
+
For reducer-style stores, call the pure `changeStatPool(pool, amount)` or
|
|
87
|
+
`patchStatPool(pool, patch)` transition inside the project's existing action
|
|
88
|
+
and commit the returned `pool` through that store's normal update path.
|
|
89
|
+
|
|
90
|
+
## Save and restore
|
|
91
|
+
|
|
92
|
+
`StatPool` is plain JSON data, so keep it in the project's existing save:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const serialized = JSON.stringify(world);
|
|
96
|
+
const restored = JSON.parse(serialized) as WorldSave;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
After restore, point the same adapter at `restored`. There is no second JGengine
|
|
100
|
+
snapshot to reconcile, no registry to rebuild, and no required migration away
|
|
101
|
+
from the caller's schema. If the existing schema uses different field names,
|
|
102
|
+
translate only at `get`/`set`.
|
|
103
|
+
|
|
104
|
+
## Ownership
|
|
105
|
+
|
|
106
|
+
The existing project owns resource ids, entity ids, storage layout,
|
|
107
|
+
persistence, replication, authority, UI, scheduling, and the decision to apply
|
|
108
|
+
a change. JGengine owns normalization, clamping, pure transitions, and the
|
|
109
|
+
small structural access contract. Native JGengine entity stats implement the
|
|
110
|
+
same portable behavior through their existing API.
|
|
111
|
+
|
|
112
|
+
## Common traps
|
|
113
|
+
|
|
114
|
+
- Do not copy caller resources into a parallel `StatValueMap`; adapt the state
|
|
115
|
+
where it already lives.
|
|
116
|
+
- Do not mutate the object returned by `get`. Commit the complete returned
|
|
117
|
+
`pool` through `set` or the caller's reducer/action.
|
|
118
|
+
- Pool changes are signed: positive restores/fills, negative spends/drains.
|
|
119
|
+
- Keep formulas, regeneration schedules, damage meaning, and resource names in
|
|
120
|
+
caller code. A stat pool only enforces bounds.
|
|
121
|
+
- Persist `current`, `max`, and `min` together so a restored value is clamped
|
|
122
|
+
against the same bounds.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Fire a weapon with your own Three.js raycaster
|
|
2
|
+
|
|
3
|
+
Use this recipe when an existing Three.js/JavaScript game wants JGengine's fire
|
|
4
|
+
cadence, magazine/reload, and deterministic damage resolution but keeps its own
|
|
5
|
+
models, animations, muzzle flash, audio, input, camera, aiming, and — crucially —
|
|
6
|
+
its own raycaster and entity list. There is no default gun: you compose generic
|
|
7
|
+
primitives. No `GameContext`, entity store, renderer, or React is required.
|
|
8
|
+
|
|
9
|
+
## Install and focused imports
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add @jgengine/core three
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
createWeaponRuntime,
|
|
18
|
+
type WeaponRuntime,
|
|
19
|
+
} from "@jgengine/core/combat";
|
|
20
|
+
import type { DamageHitInput } from "@jgengine/core/combat";
|
|
21
|
+
import * as THREE from "three";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Caller-owned aim, entities, and raycaster
|
|
25
|
+
|
|
26
|
+
The engine never touches your scene. You own the raycast; it returns whatever hit
|
|
27
|
+
shape you like.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
interface Enemy {
|
|
31
|
+
id: string;
|
|
32
|
+
mesh: THREE.Object3D;
|
|
33
|
+
armored: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const enemies: Enemy[] = loadEnemies();
|
|
37
|
+
const raycaster = new THREE.Raycaster();
|
|
38
|
+
|
|
39
|
+
function raycastFromCamera(camera: THREE.Camera): Enemy[] {
|
|
40
|
+
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); // center-screen shot
|
|
41
|
+
const meshes = enemies.map((e) => e.mesh);
|
|
42
|
+
const intersects = raycaster.intersectObjects(meshes, true);
|
|
43
|
+
const first = intersects[0];
|
|
44
|
+
if (first === undefined) return [];
|
|
45
|
+
const enemy = enemies.find((e) => e.mesh === first.object || e.mesh === first.object.parent);
|
|
46
|
+
return enemy === undefined ? [] : [enemy];
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Compose the weapon runtime
|
|
51
|
+
|
|
52
|
+
`cadenceMs`, `magazine`, and `ready` are plain data; `resolveHits` runs your
|
|
53
|
+
raycaster and `damageFor` maps a hit to a portable damage input (return `null` to
|
|
54
|
+
skip damage, e.g. a wall). Derive `cadenceMs` from a rounds-per-second fire rate
|
|
55
|
+
with `1000 / roundsPerSecond`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const rifle: WeaponRuntime<THREE.Camera, Enemy> = createWeaponRuntime({
|
|
59
|
+
cadenceMs: 1000 / 8, // 8 rounds/sec
|
|
60
|
+
magazine: { capacity: 30, reloadMs: 1800, reserve: 120 },
|
|
61
|
+
resolveHits: (camera) => raycastFromCamera(camera),
|
|
62
|
+
damageFor: (enemy): DamageHitInput => ({
|
|
63
|
+
channel: "kinetic",
|
|
64
|
+
impact: 24,
|
|
65
|
+
target: enemy.id,
|
|
66
|
+
targetTraits: enemy.armored ? ["armored"] : [],
|
|
67
|
+
matchup: { entries: { kinetic: { armored: { impact: 0.5 } } } },
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Fire, tick, and apply results
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Your input handler:
|
|
76
|
+
function onTriggerDown(camera: THREE.Camera): void {
|
|
77
|
+
const result = rifle.fire(camera);
|
|
78
|
+
if (result.status === "fired") {
|
|
79
|
+
playMuzzleFlash();
|
|
80
|
+
for (const hit of result.resolutions) {
|
|
81
|
+
// Apply the resolved impact to YOUR health store, spawn YOUR hit spark, etc.
|
|
82
|
+
applyDamageToMyEntities(hit); // hit.impact, hit.channel, hit.status, hit.received
|
|
83
|
+
}
|
|
84
|
+
} else if (result.status === "empty") {
|
|
85
|
+
rifle.startReload();
|
|
86
|
+
playReloadAnim();
|
|
87
|
+
} // "cooling" and "reloading" are silent no-ops you can ignore or feed to the HUD
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Your frame loop:
|
|
91
|
+
function update(dtSeconds: number): void {
|
|
92
|
+
rifle.tick(dtSeconds); // advances cadence + reload timers
|
|
93
|
+
hud.setAmmo(rifle.magazine?.loaded() ?? Infinity, rifle.magazine?.reserve() ?? null);
|
|
94
|
+
hud.setReload(rifle.magazine?.reloadFraction() ?? 0);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`resolveHits` and `damageFor` are only called on a `"fired"` result, so a blocked
|
|
99
|
+
trigger pull never touches your raycaster or ammo.
|
|
100
|
+
|
|
101
|
+
## Bring your own ammo economy
|
|
102
|
+
|
|
103
|
+
Point the magazine's `reserve` at a caller-owned pool with a `MagazineReserve`
|
|
104
|
+
adapter instead of a plain number, so reloads draw from your shared inventory:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import type { MagazineReserve } from "@jgengine/core/combat";
|
|
108
|
+
|
|
109
|
+
const backpack = { rifleAmmo: 240 };
|
|
110
|
+
const reserve: MagazineReserve = {
|
|
111
|
+
current: () => backpack.rifleAmmo,
|
|
112
|
+
spend: (n) => (n <= backpack.rifleAmmo ? ((backpack.rifleAmmo -= n), true) : false),
|
|
113
|
+
gain: (n) => { backpack.rifleAmmo += n; },
|
|
114
|
+
};
|
|
115
|
+
// magazine: { capacity: 30, reloadMs: 1800, reserve }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Save and restore
|
|
119
|
+
|
|
120
|
+
The magazine exposes `loaded()`/`reserve()`/`reloadFraction()` and the cadence
|
|
121
|
+
exposes `elapsedMs()`/`restore()` for HUD and snapshots:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const snapshot = {
|
|
125
|
+
cadenceMs: rifle.cadence.elapsedMs(),
|
|
126
|
+
loaded: rifle.magazine?.loaded() ?? null,
|
|
127
|
+
};
|
|
128
|
+
// After load: recreate the runtime from the same config, then:
|
|
129
|
+
rifle.cadence.restore(snapshot.cadenceMs);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Recreate the magazine at its saved `loaded` via `MagazineConfig.loaded`. (Full
|
|
133
|
+
magazine snapshot/restore lands with the serializable-runtime-state slice.)
|
|
134
|
+
|
|
135
|
+
## Ownership
|
|
136
|
+
|
|
137
|
+
The existing project keeps entities, meshes, the raycaster, aim, input, camera,
|
|
138
|
+
muzzle flash, audio, animation, the health/damage sink, ammo inventory, weapon
|
|
139
|
+
content and stats, and when the trigger fires. JGengine owns fire-rate gating,
|
|
140
|
+
magazine/reload bookkeeping, and deterministic damage resolution (matchup,
|
|
141
|
+
receiver modifiers, status rolls) with full provenance.
|
|
142
|
+
|
|
143
|
+
## Common traps
|
|
144
|
+
|
|
145
|
+
- `fire()` gates in order **reloading → cooling → empty**; a `"cooling"` result
|
|
146
|
+
means the cadence, not the magazine, blocked the shot. Tick before expecting
|
|
147
|
+
`"empty"`.
|
|
148
|
+
- Only a `"fired"` result spends a round and resets cadence; blocked attempts are
|
|
149
|
+
free, so you can call `fire()` every input frame.
|
|
150
|
+
- Firing does not auto-reload or fire-cancel a reload; call `startReload()` /
|
|
151
|
+
`cancelReload()` from your own input policy.
|
|
152
|
+
- `damageFor` returning `null` keeps the hit in `result.hits` but produces no
|
|
153
|
+
`DamageHitResolution` — use it for terrain/props your raycast also returns.
|
|
154
|
+
- Pass a seeded `rng` in the config for deterministic status rolls in multiplayer
|
|
155
|
+
authority, replays, or tests; per-hit `DamageHitInput.rng` overrides it.
|
|
156
|
+
- Convert fire rate to `cadenceMs` with `1000 / roundsPerSecond`, not the reverse.
|