@jgengine/core 0.27.0 → 0.29.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 +140 -0
- package/dist/ai/policeResponse.d.ts +253 -0
- package/dist/ai/policeResponse.js +421 -0
- package/dist/anim/actionLayer.d.ts +47 -0
- package/dist/anim/actionLayer.js +63 -0
- package/dist/anim/ikSolver.d.ts +14 -0
- package/dist/anim/ikSolver.js +37 -0
- package/dist/anim/locomotionGraph.d.ts +58 -3
- package/dist/anim/locomotionGraph.js +142 -15
- package/dist/anim/ragdollRig.d.ts +53 -0
- package/dist/anim/ragdollRig.js +132 -0
- package/dist/anim/skeletonStandard.d.ts +140 -0
- package/dist/anim/skeletonStandard.js +326 -0
- package/dist/combat/damageRuntime.d.ts +14 -1
- package/dist/combat/damageRuntime.js +62 -4
- package/dist/combat/death.js +7 -3
- package/dist/combat/deathReason.d.ts +7 -1
- package/dist/combat/effects.d.ts +25 -0
- package/dist/combat/effects.js +16 -0
- package/dist/combat/statusRuntime.d.ts +2 -0
- package/dist/combat/statusRuntime.js +1 -0
- package/dist/economy/loanBook.d.ts +182 -0
- package/dist/economy/loanBook.js +303 -0
- package/dist/economy/market.d.ts +161 -0
- package/dist/economy/market.js +248 -0
- package/dist/economy/transactionLedger.d.ts +162 -0
- package/dist/economy/transactionLedger.js +301 -0
- package/dist/editor/creatorStorage.js +3 -1
- package/dist/editor/simulation.d.ts +5 -2
- package/dist/editor/simulation.js +29 -19
- package/dist/game/cameraConfig.d.ts +14 -0
- package/dist/game/clipRoles.d.ts +5 -4
- package/dist/game/clipRoles.js +34 -4
- package/dist/game/events.d.ts +27 -0
- package/dist/game/gameSave.d.ts +142 -0
- package/dist/game/gameSave.js +174 -0
- package/dist/game/playableGame.d.ts +55 -4
- package/dist/game/saveSlotIndex.d.ts +54 -0
- package/dist/game/saveSlotIndex.js +71 -0
- package/dist/game/saveSlots.d.ts +2 -0
- package/dist/game/saveSlots.js +1 -1
- package/dist/game/saveStore.d.ts +8 -0
- package/dist/game/saveStore.js +16 -0
- package/dist/gameplay.d.ts +12 -2
- package/dist/gameplay.js +10 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +130 -1
- package/dist/mission/compositions.d.ts +85 -0
- package/dist/mission/compositions.js +130 -0
- package/dist/mission/mission.d.ts +435 -0
- package/dist/mission/mission.js +773 -0
- package/dist/mission/triggers.d.ts +61 -0
- package/dist/mission/triggers.js +108 -0
- package/dist/nav/traffic.d.ts +0 -1
- package/dist/nav/traffic.js +1 -2
- package/dist/runtime/context/combat.js +11 -0
- package/dist/settings/accessibilitySettings.d.ts +81 -0
- package/dist/settings/accessibilitySettings.js +152 -0
- package/dist/settings/graphicsProfile.d.ts +3 -0
- package/dist/settings/graphicsProfile.js +4 -0
- package/dist/settings/settingsModel.d.ts +1 -1
- package/dist/settings/settingsModel.js +1 -0
- package/dist/sim/nearAgents.d.ts +8 -1
- package/dist/sim/nearAgents.js +34 -17
- package/dist/sim/offScreen.d.ts +2 -1
- package/dist/sim/offScreen.js +14 -3
- package/dist/sim/population.d.ts +147 -0
- package/dist/sim/population.js +182 -0
- package/dist/sim/queue.d.ts +90 -0
- package/dist/sim/queue.js +171 -0
- package/dist/sim/rides.d.ts +287 -0
- package/dist/sim/rides.js +471 -0
- package/dist/sim/routine.js +19 -11
- package/dist/ui/loadingTips.d.ts +41 -0
- package/dist/ui/loadingTips.js +64 -0
- package/dist/ui/menuNav.d.ts +79 -0
- package/dist/ui/menuNav.js +147 -0
- package/dist/ui/menuTransition.d.ts +50 -0
- package/dist/ui/menuTransition.js +67 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +2 -0
- package/dist/vfx/effectLibrary.d.ts +98 -0
- package/dist/vfx/effectLibrary.js +181 -0
- package/dist/vfx/particleBudget.d.ts +40 -0
- package/dist/vfx/particleBudget.js +39 -0
- package/dist/vfx/particleDirector.d.ts +16 -0
- package/dist/vfx/particleDirector.js +18 -0
- package/dist/world/authoredSimulation.js +7 -1
- package/dist/world/captureCamera.d.ts +30 -0
- package/dist/world/captureCamera.js +19 -0
- package/dist/world/features.d.ts +15 -4
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world.d.ts +2 -1
- package/dist/world.js +1 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-combat/SKILL.md +6 -0
- package/skills/jgengine-combat/api.md +5 -4
- package/skills/jgengine-combat/reference.md +4 -0
- package/skills/jgengine-gameplay/SKILL.md +55 -0
- package/skills/jgengine-gameplay/api.md +335 -10
- package/skills/jgengine-gameplay/capabilities.md +83 -1
- package/skills/jgengine-world/SKILL.md +4 -0
- package/skills/jgengine-world/api.md +128 -6
- package/skills/jgengine-world/capabilities.md +41 -1
- package/skills/jgengine-world/recipes/character-animation.md +25 -2
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graphics-tier particle budget. A tier caps the live particle count across every effect on
|
|
3
|
+
* screen; when the requested effects do not fit, whole effects are dropped in a defined order
|
|
4
|
+
* (lowest {@link ParticleBudgetEntry.priority} first) rather than every effect thinning out.
|
|
5
|
+
*/
|
|
6
|
+
/** One effect asking for particle capacity this frame. */
|
|
7
|
+
export interface ParticleBudgetEntry {
|
|
8
|
+
/** Emitter or burst id; unique within the request. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Drop order: higher survives, lower is dropped first. */
|
|
11
|
+
priority: number;
|
|
12
|
+
/** Particles this effect wants (its pool maximum, or a burst's count). */
|
|
13
|
+
requested: number;
|
|
14
|
+
}
|
|
15
|
+
/** An admitted effect and the particles it may hold. */
|
|
16
|
+
export interface ParticleBudgetGrant {
|
|
17
|
+
id: string;
|
|
18
|
+
particles: number;
|
|
19
|
+
}
|
|
20
|
+
/** What the tier cap admits, and what it drops. */
|
|
21
|
+
export interface ParticleBudgetPlan {
|
|
22
|
+
/** Admitted effects in priority order, each with its granted particle count. */
|
|
23
|
+
grants: ParticleBudgetGrant[];
|
|
24
|
+
/** Dropped effect ids, lowest priority first — the defined drop order. */
|
|
25
|
+
dropped: string[];
|
|
26
|
+
/** Sum of the granted particle counts, never above the cap. */
|
|
27
|
+
particles: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Allocate a tier's particle cap across the effects that want it.
|
|
31
|
+
*
|
|
32
|
+
* Effects are visited highest priority first (ties keep authoring order) and each takes
|
|
33
|
+
* `min(requested, remaining)`. An effect that receives nothing is dropped, so the drop list comes
|
|
34
|
+
* out lowest priority first: ambient dressing (dust, weather) yields before gameplay-loud effects
|
|
35
|
+
* (explosions, muzzle flashes). The granted total never exceeds the cap, so the live particle
|
|
36
|
+
* count cannot either.
|
|
37
|
+
*
|
|
38
|
+
* @capability plan-particle-budget cap live particles to a graphics tier and drop whole effects lowest-priority-first when over budget
|
|
39
|
+
*/
|
|
40
|
+
export declare function planParticleBudget(entries: readonly ParticleBudgetEntry[], cap: number): ParticleBudgetPlan;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graphics-tier particle budget. A tier caps the live particle count across every effect on
|
|
3
|
+
* screen; when the requested effects do not fit, whole effects are dropped in a defined order
|
|
4
|
+
* (lowest {@link ParticleBudgetEntry.priority} first) rather than every effect thinning out.
|
|
5
|
+
*/
|
|
6
|
+
function count(value, fallback = 0) {
|
|
7
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Allocate a tier's particle cap across the effects that want it.
|
|
11
|
+
*
|
|
12
|
+
* Effects are visited highest priority first (ties keep authoring order) and each takes
|
|
13
|
+
* `min(requested, remaining)`. An effect that receives nothing is dropped, so the drop list comes
|
|
14
|
+
* out lowest priority first: ambient dressing (dust, weather) yields before gameplay-loud effects
|
|
15
|
+
* (explosions, muzzle flashes). The granted total never exceeds the cap, so the live particle
|
|
16
|
+
* count cannot either.
|
|
17
|
+
*
|
|
18
|
+
* @capability plan-particle-budget cap live particles to a graphics tier and drop whole effects lowest-priority-first when over budget
|
|
19
|
+
*/
|
|
20
|
+
export function planParticleBudget(entries, cap) {
|
|
21
|
+
const limit = count(cap);
|
|
22
|
+
const ordered = entries
|
|
23
|
+
.map((entry, index) => ({ entry, index, priority: Number.isFinite(entry.priority) ? entry.priority : 0, wanted: count(entry.requested) }))
|
|
24
|
+
.sort((a, b) => (b.priority - a.priority) || (a.index - b.index));
|
|
25
|
+
const grants = [];
|
|
26
|
+
const dropped = [];
|
|
27
|
+
let remaining = limit;
|
|
28
|
+
for (const { entry, wanted } of ordered) {
|
|
29
|
+
const particles = Math.min(wanted, remaining);
|
|
30
|
+
if (particles <= 0) {
|
|
31
|
+
dropped.push(entry.id);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
grants.push({ id: entry.id, particles });
|
|
35
|
+
remaining -= particles;
|
|
36
|
+
}
|
|
37
|
+
dropped.reverse();
|
|
38
|
+
return { grants, dropped, particles: limit - remaining };
|
|
39
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EffectOverride } from "./effectLibrary.js";
|
|
1
2
|
import { type EmitterConfig, type ParticleEvent, type Vec3 } from "./particles.js";
|
|
2
3
|
/** How a particle effect composites on screen: `additive` for sparks/fire/glow, `normal` for smoke/dust. */
|
|
3
4
|
export type ParticleBlendHint = "additive" | "normal";
|
|
@@ -17,6 +18,8 @@ export interface ParticleBurst {
|
|
|
17
18
|
blending?: ParticleBlendHint;
|
|
18
19
|
emitterId?: string;
|
|
19
20
|
options?: ParticleAttachOptions;
|
|
21
|
+
/** Graphics-tier drop order; higher survives. Default 0. */
|
|
22
|
+
priority?: number;
|
|
20
23
|
}
|
|
21
24
|
/** A bounded collision child effect; children cannot spawn further collision children. */
|
|
22
25
|
export interface ParticleCollisionEffect {
|
|
@@ -43,6 +46,8 @@ export interface ParticleAttachOptions {
|
|
|
43
46
|
maxAcceleration?: number;
|
|
44
47
|
mask?: number;
|
|
45
48
|
};
|
|
49
|
+
/** Graphics-tier drop order: higher survives, lower is dropped first. Default 0. */
|
|
50
|
+
priority?: number;
|
|
46
51
|
}
|
|
47
52
|
/** A keyed continuous emitter owned by the director until detached. */
|
|
48
53
|
export interface ParticleEmitterSpec extends ParticleAttachOptions {
|
|
@@ -52,6 +57,11 @@ export interface ParticleEmitterSpec extends ParticleAttachOptions {
|
|
|
52
57
|
offset?: Vec3;
|
|
53
58
|
blending?: ParticleBlendHint;
|
|
54
59
|
}
|
|
60
|
+
/** A library effect played by name: where it plays, plus per-call overrides. */
|
|
61
|
+
export interface EffectRequest extends EffectOverride {
|
|
62
|
+
/** Entity id a standing emitter tracks each frame (plus `offset`). */
|
|
63
|
+
follow?: string;
|
|
64
|
+
}
|
|
55
65
|
/** A cosmetic simulation event, labelled with its emitting effect. */
|
|
56
66
|
export interface ParticleDirectorEvent extends ParticleEvent {
|
|
57
67
|
emitterId: string;
|
|
@@ -87,6 +97,12 @@ export interface ParticleDirector {
|
|
|
87
97
|
stop(id: string): void;
|
|
88
98
|
/** Emit once through a named emitter's existing pool, including when stopped. */
|
|
89
99
|
burstNamed(id: string, count: number): void;
|
|
100
|
+
/** Play a library effect by name once, as a one-shot burst at the request's position. */
|
|
101
|
+
play(name: string, request?: EffectRequest): void;
|
|
102
|
+
/** Create or replace a standing emitter from a library effect name under `id`. */
|
|
103
|
+
attachEffect(id: string, name: string, request?: EffectRequest): void;
|
|
104
|
+
/** The named effects a caller can play, from the shared library. */
|
|
105
|
+
effectNames(): readonly string[];
|
|
90
106
|
/** Renderer-to-game cosmetic event exchange, bounded by the director's event limit. */
|
|
91
107
|
reportEvents(id: string, events: readonly ParticleEvent[]): void;
|
|
92
108
|
drainEvents(): ParticleDirectorEvent[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectNames, resolveEffect } from "./effectLibrary.js";
|
|
1
2
|
import { validateEmitterConfig } from "./particles.js";
|
|
2
3
|
/**
|
|
3
4
|
* Create the particle intent registry a `GameContext` exposes as `ctx.particles`.
|
|
@@ -24,6 +25,7 @@ export function createParticleDirector(options = {}) {
|
|
|
24
25
|
bursts.push({ seq: nextSeq++, config: structuredClone(config), count: wanted,
|
|
25
26
|
...(blending === undefined ? {} : { blending }),
|
|
26
27
|
...(emitterId === undefined ? {} : { emitterId }),
|
|
28
|
+
priority: Number.isFinite(attachOptions?.priority) ? attachOptions.priority : 0,
|
|
27
29
|
...(attachOptions === undefined ? {} : { options: structuredClone(attachOptions) }) });
|
|
28
30
|
notify();
|
|
29
31
|
}
|
|
@@ -64,6 +66,20 @@ export function createParticleDirector(options = {}) {
|
|
|
64
66
|
const { id: _, config, ...options } = spec;
|
|
65
67
|
queue(config, count, spec.blending, id, options);
|
|
66
68
|
},
|
|
69
|
+
play(name, request) {
|
|
70
|
+
const effect = resolveEffect(name, request);
|
|
71
|
+
const follow = request?.follow ?? effect.options.follow;
|
|
72
|
+
queue(effect.config, effect.burst, effect.blending, undefined, { ...effect.options, ...(follow === undefined ? {} : { follow }) });
|
|
73
|
+
},
|
|
74
|
+
attachEffect(id, name, request) {
|
|
75
|
+
const effect = resolveEffect(name, request);
|
|
76
|
+
if (!emitters.has(id) && emitters.size >= maxEmitters)
|
|
77
|
+
throw new RangeError(`Particle emitter capacity ${maxEmitters} exceeded by ${id}`);
|
|
78
|
+
const follow = request?.follow ?? effect.options.follow;
|
|
79
|
+
emitters.set(id, { id, config: effect.config, ...effect.options, ...(follow === undefined ? {} : { follow }) });
|
|
80
|
+
notify();
|
|
81
|
+
},
|
|
82
|
+
effectNames,
|
|
67
83
|
reportEvents(id, next) {
|
|
68
84
|
for (let i = 0, n = Math.min(next.length, maxEvents - events.length); i < n; i++) {
|
|
69
85
|
events.push({ ...structuredClone(next[i]), emitterId: id });
|
|
@@ -129,6 +145,8 @@ export function validateParticleAttachOptions(value) {
|
|
|
129
145
|
errors.push("space must be world or local");
|
|
130
146
|
if (options.active !== undefined && typeof options.active !== "boolean")
|
|
131
147
|
errors.push("active must be boolean");
|
|
148
|
+
if (options.priority !== undefined && (typeof options.priority !== "number" || !Number.isFinite(options.priority)))
|
|
149
|
+
errors.push("priority must be a finite number");
|
|
132
150
|
const output = (render, blending, prefix) => {
|
|
133
151
|
if (blending !== undefined && blending !== "normal" && blending !== "additive")
|
|
134
152
|
errors.push(`${prefix}blending must be normal or additive`);
|
|
@@ -89,7 +89,13 @@ export function createAuthoredSimulation(options) {
|
|
|
89
89
|
const previous = config.emitters?.find((entry) => entry.id === emitter.id);
|
|
90
90
|
if (emitterIds.includes(id) && JSON.stringify(previous) === JSON.stringify(emitter))
|
|
91
91
|
continue;
|
|
92
|
-
|
|
92
|
+
const position = [emitter.position.x, emitter.position.y, emitter.position.z];
|
|
93
|
+
const fire = emitter.fireAreaId === undefined ? undefined : { active: emitter.options?.active !== false && nextFires.get(emitter.fireAreaId).burning > 0 };
|
|
94
|
+
const request = { position, options: { ...emitter.options, ...(fire ?? {}) } };
|
|
95
|
+
if (emitter.effect !== undefined)
|
|
96
|
+
options.particles?.attachEffect(id, emitter.effect, request);
|
|
97
|
+
else
|
|
98
|
+
options.particles?.attach(id, { ...emitter.config, position }, request.options);
|
|
93
99
|
}
|
|
94
100
|
emitterIds = nextEmitterIds;
|
|
95
101
|
document = nextDocument;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CameraRigKind } from "../game/playableGame.js";
|
|
2
|
+
import type { CameraRigConfig } from "../runtime/cameraDirector.js";
|
|
3
|
+
/**
|
|
4
|
+
* Per-session capture-camera override — the harness counterpart of `?cam=`/`?look=`, and the same
|
|
5
|
+
* install-style seam as `world/spawnOverride`: the runner installs one, the shell reads it, nothing
|
|
6
|
+
* changes until one is installed.
|
|
7
|
+
*
|
|
8
|
+
* It exists because a game that owns its camera wins by other routes: a static `camera` config
|
|
9
|
+
* swapped by the harness only reaches the rig until the game selects its own rig at runtime
|
|
10
|
+
* (`ctx.camera.setRig`, a persisted settings camera, a director cinematic), and the capture then
|
|
11
|
+
* silently photographs the game's camera. This override is read above every one of those, so a
|
|
12
|
+
* framed capture stays framed; a game's own camera is untouched the moment the page loads without
|
|
13
|
+
* the param.
|
|
14
|
+
*/
|
|
15
|
+
export interface CaptureCameraOverride {
|
|
16
|
+
kind: CameraRigKind;
|
|
17
|
+
config: CameraRigConfig;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Install (or, with `null`, clear) the capture camera. Idempotent; last write wins. It outranks the
|
|
21
|
+
* game's static camera config and any runtime `ctx.camera` rig selection, and it is never saved —
|
|
22
|
+
* it is a property of this capture session, not of the game.
|
|
23
|
+
*
|
|
24
|
+
* @capability capture force a framed capture camera over a game that owns its camera
|
|
25
|
+
*/
|
|
26
|
+
export declare function installCaptureCamera(override: CaptureCameraOverride | null): void;
|
|
27
|
+
/** Clear any installed capture camera. */
|
|
28
|
+
export declare function clearCaptureCamera(): void;
|
|
29
|
+
/** The installed capture camera, or `null` when the game's own camera should render. */
|
|
30
|
+
export declare function readCaptureCamera(): CaptureCameraOverride | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
let installed = null;
|
|
2
|
+
/**
|
|
3
|
+
* Install (or, with `null`, clear) the capture camera. Idempotent; last write wins. It outranks the
|
|
4
|
+
* game's static camera config and any runtime `ctx.camera` rig selection, and it is never saved —
|
|
5
|
+
* it is a property of this capture session, not of the game.
|
|
6
|
+
*
|
|
7
|
+
* @capability capture force a framed capture camera over a game that owns its camera
|
|
8
|
+
*/
|
|
9
|
+
export function installCaptureCamera(override) {
|
|
10
|
+
installed = override;
|
|
11
|
+
}
|
|
12
|
+
/** Clear any installed capture camera. */
|
|
13
|
+
export function clearCaptureCamera() {
|
|
14
|
+
installed = null;
|
|
15
|
+
}
|
|
16
|
+
/** The installed capture camera, or `null` when the game's own camera should render. */
|
|
17
|
+
export function readCaptureCamera() {
|
|
18
|
+
return installed;
|
|
19
|
+
}
|
package/dist/world/features.d.ts
CHANGED
|
@@ -70,16 +70,20 @@ export interface TerrainDetailConfig {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* PBR map URLs for a real ground texture — the same shape
|
|
73
|
-
* `
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* `libraryTerrainMaterial(name).maps` from `@jgengine/assets` returns. Kept dependency-free here so
|
|
74
|
+
* `core` never imports the assets package; any URLs (pulled maps, a CDN, a data URI) satisfy it.
|
|
75
|
+
* Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
|
|
76
76
|
*/
|
|
77
77
|
export interface TerrainMaterialMaps {
|
|
78
78
|
color: string;
|
|
79
79
|
normal: string;
|
|
80
80
|
roughness?: string;
|
|
81
81
|
ao?: string;
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Height map for callers that displace geometry themselves. The terrain detail shader never
|
|
84
|
+
* samples it, so pass the URL only when the pulled set actually ships one — never invent it.
|
|
85
|
+
*/
|
|
86
|
+
displacement?: string;
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
89
|
* Real PBR texture applied over the ground surface — the seam that lets a game put a
|
|
@@ -102,6 +106,13 @@ export interface TerrainDetailMaterialConfig {
|
|
|
102
106
|
* hand-tinted world wants from a stock PBR set.
|
|
103
107
|
*/
|
|
104
108
|
tint?: number;
|
|
109
|
+
/**
|
|
110
|
+
* What a failed map load does. Scanned maps are pulled by a game's asset step, not committed, so a
|
|
111
|
+
* checkout without them looks exactly like a broken one. `"procedural"` (default) keeps the
|
|
112
|
+
* authored procedural ground and logs one warning naming the map; `"fail"` hands the error to the
|
|
113
|
+
* presentation retry screen, which is what a view that cannot render without the scan asks for.
|
|
114
|
+
*/
|
|
115
|
+
missing?: "procedural" | "fail";
|
|
105
116
|
}
|
|
106
117
|
export interface TerrainFlattenMask {
|
|
107
118
|
center: EnvironmentVec2;
|
package/dist/world/terrain.d.ts
CHANGED
|
@@ -201,6 +201,7 @@ export interface ResolvedTerrainDetailMaterial {
|
|
|
201
201
|
repeat: number;
|
|
202
202
|
strength: number;
|
|
203
203
|
tint: number;
|
|
204
|
+
missing: "procedural" | "fail";
|
|
204
205
|
}
|
|
205
206
|
/** A {@link TerrainDetailConfig} with every field resolved to a concrete value — the shape the shell's detail material consumes. */
|
|
206
207
|
export type ResolvedTerrainDetail = Required<Omit<TerrainDetailConfig, "waterLevel" | "material" | "sweeps">> & {
|
package/dist/world/terrain.js
CHANGED
|
@@ -487,6 +487,7 @@ const IMPLICIT_TERRAIN_DETAIL = {
|
|
|
487
487
|
const DEFAULT_TERRAIN_MATERIAL_REPEAT = 4;
|
|
488
488
|
const DEFAULT_TERRAIN_MATERIAL_STRENGTH = 1;
|
|
489
489
|
const DEFAULT_TERRAIN_MATERIAL_TINT = 1;
|
|
490
|
+
const DEFAULT_TERRAIN_MATERIAL_MISSING = "procedural";
|
|
490
491
|
/** Sand-band level for a terrain with no water: far below any ground, so no beach is painted. */
|
|
491
492
|
const NO_WATER_SAND_LEVEL = -1e6;
|
|
492
493
|
/** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level,
|
|
@@ -520,6 +521,7 @@ export function resolveTerrainDetail(config, terrainWaterLevel) {
|
|
|
520
521
|
repeat: config.material.repeat ?? DEFAULT_TERRAIN_MATERIAL_REPEAT,
|
|
521
522
|
tint: clamp01(config.material.tint ?? DEFAULT_TERRAIN_MATERIAL_TINT),
|
|
522
523
|
strength: clamp01(config.material.strength ?? DEFAULT_TERRAIN_MATERIAL_STRENGTH),
|
|
524
|
+
missing: config.material.missing ?? DEFAULT_TERRAIN_MATERIAL_MISSING,
|
|
523
525
|
},
|
|
524
526
|
}),
|
|
525
527
|
};
|
package/dist/world.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { type HeatConfig, type HeatGain, type HeatLevelDef, type HeatState } from "./ai/heatSystem.js";
|
|
1
|
+
export { type HeatConfig, type HeatGain, type HeatLevelDef, type HeatPoint, type HeatState } from "./ai/heatSystem.js";
|
|
2
2
|
export { advanceInterestGate, createInterestCensus, createInterestGateState, interestPhase, type InterestCensus, type InterestCensusAccumulator, type InterestGateInput, type InterestGateState, type InterestGateStep, type InterestSchedulerConfig, type InterestState, type InterestTier, } from "./ai/interestScheduler.js";
|
|
3
3
|
export { type Job, type JobDef, type JobReport } from "./ai/jobBoard.js";
|
|
4
|
+
export { DEFAULT_POLICE_TIERS, DEFAULT_POLICE_TUNING, createPoliceResponse, policeRoadsFromLaneGraph, type PoliceLaneHit, type PoliceResponse, type PoliceResponseConfig, type PoliceResponseKind, type PoliceResponseState, type PoliceRoadblockSite, type PoliceRoads, type PoliceRoadsOptions, type PoliceSearchArea, type PoliceSighting, type PoliceStats, type PoliceStep, type PoliceTickContext, type PoliceTier, type PoliceTuning, type PoliceWitness, } from "./ai/policeResponse.js";
|
|
4
5
|
export { createRvoCrowd, type RvoAgentSpec, type RvoAgentView, type RvoCrowd, type RvoCrowdOptions, type RvoSnapshot, type RvoTunables, } from "./ai/rvo.js";
|
|
5
6
|
export { advanceSpawnDirector, advanceWave, createSpawnDirectorState, pickSpawnPoint, raiseAlert, type SpawnDirectorConfig, type SpawnDirectorState, type SpawnEntry, type SpawnRequest, type WaveManifest, } from "./ai/spawnDirector.js";
|
|
6
7
|
export { acquireTarget, createTargetAcquirer, type AcquisitionEnvelope, type AcquisitionPolicy, type AcquisitionResult, type AcquisitionRetention, type TargetAcquirer, } from "./ai/targetAcquisition.js";
|
package/dist/world.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export {} from "./ai/heatSystem.js";
|
|
2
2
|
export { advanceInterestGate, createInterestCensus, createInterestGateState, interestPhase, } from "./ai/interestScheduler.js";
|
|
3
3
|
export {} from "./ai/jobBoard.js";
|
|
4
|
+
export { DEFAULT_POLICE_TIERS, DEFAULT_POLICE_TUNING, createPoliceResponse, policeRoadsFromLaneGraph, } from "./ai/policeResponse.js";
|
|
4
5
|
export { createRvoCrowd, } from "./ai/rvo.js";
|
|
5
6
|
export { advanceSpawnDirector, advanceWave, createSpawnDirectorState, pickSpawnPoint, raiseAlert, } from "./ai/spawnDirector.js";
|
|
6
7
|
export { acquireTarget, createTargetAcquirer, } from "./ai/targetAcquisition.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jgengine",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.29.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.29.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -44,6 +44,12 @@ compose through `createDamageRuntime` (`combat/damageRuntime`): a readable table
|
|
|
44
44
|
time-to-kill quotes, and status ticks that follow the declared stack rule. It
|
|
45
45
|
snapshots/restores as plain data; see
|
|
46
46
|
[reference.md](reference.md#damage-runtime-armor-crits-and-statuses).
|
|
47
|
+
Both damage paths raise `combat.damage` on `ctx.game.events`, one event per target
|
|
48
|
+
application: the runtime with the table `type`, and `ctx.scene.entity.effect` with
|
|
49
|
+
the effect id as `type` and `remaining` read from the last pool in the receive
|
|
50
|
+
order. Use the runtime for new damage that needs types, zones or armor; keep the
|
|
51
|
+
effect path for games that already drain `receive` pools. Neither delegates to
|
|
52
|
+
the other, so a hit never raises the event twice.
|
|
47
53
|
|
|
48
54
|
Closure-backed magazines expose snapshot/restore and `retune` for capacity/reload
|
|
49
55
|
changes without reconstructing ammo state; capacity overflow is explicit.
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
- `DamageTableDef` (interface): interface DamageTableDef — A serializable damage table: damage types scored against resistance classes, plus the armor curve those rows are read with. It is data a generated gun (B3.1) and a weapon runtime (B2.3) pass in; the engine holds no built-in elemental chart.
|
|
70
70
|
- `DamageTableRow` (interface): interface DamageTableRow — One readable row: a type against a resistance class at a given armor value.
|
|
71
71
|
- `DamageTypeDef` (interface): interface DamageTypeDef — One damage type's row in a damage table: a caller-owned id (`"kinetic"`, `"fire"`) and how it fares against each resistance class a target can present (`"flesh"`, `"shield"`, `"armor"`). Values below `1` are resisted, `1` is neutral, above `1` is a weakness, and `0` is immune.
|
|
72
|
-
- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event.
|
|
72
|
+
- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; source?: string; via?: { item?: string } } | { kind: "environment"; source: string; attacker?: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. `source` names what dealt it: a player kill's killer instance id, or the hazard/item of an environment death; `attacker` carries the non-player entity's instance id when one dealt it.
|
|
73
73
|
- `DefenseResolution` (interface): interface DefenseResolution — Result of resolving an incoming hit against a defensive window: `outcome` is the graded defense (`"parry"`/`"block"`/`"iframe"`/`"hit"`) and `perfect` is true when the input landed inside the tight just-frame at the window's start rather than its wider active span.
|
|
74
74
|
- `DuplicateDamageTypeError` (class): class DuplicateDamageTypeError extends Error — Raised when a table lists the same damage type twice, which would make the winner depend on order.
|
|
75
75
|
- `EncounterConfig` (interface): interface EncounterConfig<TData = unknown> — Non-serializable configuration for an encounter: the authored phase tree plus predicate and spawn-provider registries. Re-supplied alongside a loaded {@link EncounterState}, the way `advanceSpawnDirector` pairs config with state, so the state itself stays pure data.
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
- `createCoverField` (function): function createCoverField(options: CoverFieldOptions): CoverField — Cover over authored boxes: derive the stand points once, re-derive them after an edit, and answer bounded nearest-cover queries. `walkable` is injected, so the seam checks the game's own ground test (`nav.findPolygon(point) !== null`) instead of assuming a flat floor.
|
|
244
244
|
- `createDamageClamp` (function): function createDamageClamp(config: DamageClampConfig): DamageInterceptor — A stateless interceptor that caps any single application at `maxPerHit`.
|
|
245
245
|
- `createDamagePipeline` (function): function createDamagePipeline(initial?: readonly DamageInterceptor[]): DamagePipeline — Create a mutable damage interception chain that state transitions install into and remove from — e.g. add an invulnerability interceptor on phase enter and drop it on phase exit. Evaluation order is install order.
|
|
246
|
-
- `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet.
|
|
246
|
+
- `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet. Every application — a hit or a status tick — is announced as `combat.damage` carrying the target, source, type, amount, absorbed, remaining health and a lethal flag, so a HUD names the blow instead of guessing.
|
|
247
247
|
- `createDeathSystem` (function): function createDeathSystem(deps: DeathSystemDeps): DeathSystem — Resolve entity death and the on-death consequences — drops, respawn eligibility, kill credit.
|
|
248
248
|
- `createDefensiveWindow` (function): function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow — Open a timed defensive window — block, parry, or i-frames — and test incoming hits against it.
|
|
249
249
|
- `createDotField` (function): function createDotField(): DotField — Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`.
|
|
@@ -462,7 +462,7 @@
|
|
|
462
462
|
- `TargetProfile` (interface): interface TargetProfile — What a target presents to the table: one resistance class and its armor points.
|
|
463
463
|
- `TimeToKill` (interface): interface TimeToKill — A quoted time to kill for one fixed hit against one health pool.
|
|
464
464
|
- `UnknownTargetProfileError` (class): class UnknownTargetProfileError extends Error — Raised when a hit names a target that was never registered — a wiring error, not a 0-damage hit.
|
|
465
|
-
- `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet.
|
|
465
|
+
- `createDamageRuntime` (function): function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime — Build a damage model from data. The number a hit lands is `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the same table and armor, so a burn on an armored target is mitigated like a bullet. Every application — a hit or a status tick — is announced as `combat.damage` carrying the target, source, type, amount, absorbed, remaining health and a lethal flag, so a HUD names the blow instead of guessing.
|
|
466
466
|
|
|
467
467
|
## @jgengine/core/combat/damageTable
|
|
468
468
|
|
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
|
|
502
502
|
## @jgengine/core/combat/deathReason
|
|
503
503
|
|
|
504
|
-
- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event.
|
|
504
|
+
- `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; source?: string; via?: { item?: string } } | { kind: "environment"; source: string; attacker?: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event. `source` names what dealt it: a player kill's killer instance id, or the hazard/item of an environment death; `attacker` carries the non-player entity's instance id when one dealt it.
|
|
505
505
|
|
|
506
506
|
## @jgengine/core/combat/defensiveWindow
|
|
507
507
|
|
|
@@ -541,6 +541,7 @@
|
|
|
541
541
|
- `AreaTarget` (interface): interface AreaTarget { instanceId: string; scale: number } — ⚠ undocumented
|
|
542
542
|
- `AreaTargetInput` (interface): interface AreaTargetInput { at: EntityPosition; radius: number; falloff?: "linear" | "none"; los?: boolean } — ⚠ undocumented
|
|
543
543
|
- `CombatSpatialDeps` (interface): interface CombatSpatialDeps { inRadius(center: EntityPosition, radius: number): string[]; hasLineOfSight(from: EntityPosition | string, to: string): boolean; positionOf(instanceId: string): EntityPosition | undefined } — ⚠ undocumented
|
|
544
|
+
- `EffectDamageApplication` (interface): interface EffectDamageApplication — One effect application that drained a target's pools, as an announcer needs it. The effect path has no damage table, so the caller names the channel (`effect`) and reads health its own way.
|
|
544
545
|
- `EffectInput` (type): type EffectInput = SingleTargetEffectInput | AreaEffectInput — ⚠ undocumented
|
|
545
546
|
- `EffectResult` (interface): interface EffectResult { instanceId: string; effect: string; applied: AppliedPoolDelta[]; lethal: boolean; slain?: SlainIdentity } — ⚠ undocumented
|
|
546
547
|
- `EffectSystem` (interface): interface EffectSystem { canReceive(instanceId: string, effect: string, magnitude?: number): string | null; preview(input: SingleTargetEffectInput): number; applyEffect(input: EffectInput): EffectResult[] } — ⚠ undocumented · used by `createEffectSystem`: Resolve direct or area resource effects through caller-owned stat pools, including ordered absorption, restoration, modifiers, and lethal co…
|
|
@@ -49,6 +49,8 @@ AoE: `inRadius(at, radius)` → LoS filter (default on) → `canReceive` per tar
|
|
|
49
49
|
|
|
50
50
|
Each per-target `EffectResult` carries `{ instanceId, effect, applied, lethal }`. On a **lethal** hit it additionally carries `slain: { catalogId, name?, userId? }` — the victim's identity captured **before** the death system despawns it, so kill credit / XP reads the slain kind straight off the call result without a spawn-time id→kind registry (a non-lethal hit omits `slain`). This is the synchronous complement to the `entity.died` event: use `slain` when the code awarding the kill is the same code that fired the effect; use `entity.died` for decoupled listeners (quests, feeds, leaderboards).
|
|
51
51
|
|
|
52
|
+
Every draining application also raises one `combat.damage` (`DamageAppliedEvent`) on `ctx.game.events` per target it lands on: `source` is the effect's `from`, `type` is the effect id, `amount` is the total drained, `absorbed` is the incoming magnitude the receive modifiers and depleted pools stopped, and `remaining`/`lethal` read the **last** stat in the receive order — the pool whose exhaustion counts as death. A restorative application (`via: { amount: -n }`) raises no damage event.
|
|
53
|
+
|
|
52
54
|
`canReceive`'s optional signed `magnitude` makes the check direction-aware: positive (or omitted) keeps the standard `"pools-depleted"` check; negative checks the opposite direction and returns `"pools-full"` only when every stat in the receive order is already at max — so a heal (`via: { amount: -n }`) correctly reaches a fully-depleted target and is rejected only when there's nothing left to restore.
|
|
53
55
|
|
|
54
56
|
**Projectiles** (aim-based — no target ids):
|
|
@@ -145,6 +147,8 @@ Genre systems layered over the same effects/projectiles/targeting/loot primitive
|
|
|
145
147
|
|
|
146
148
|
`createDamageRuntime` (`combat/damageRuntime`) is the stateful in-process composition of the table, the zones, the armor curve, and carried statuses. A number reads as `base × zone multiplier × type multiplier × (1 − armor mitigation)`; `hit()` returns every stage, `tick()` ticks statuses through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones, or definitions without dropping either. For multiplayer authority, keep the pure stages (`resolveMatchup`, `resolveReceivedDamage`, `resolveStatusApplication`) and replicate their results instead.
|
|
147
149
|
|
|
150
|
+
With `events: ctx.game.events` and `healthOf`, every hit and status tick raises one `combat.damage` carrying the table `type`, the final `amount`, what resistance stopped (`absorbed`), and `remaining`/`lethal`. The runtime and the effect path each announce their own application; neither calls the other, so a game that uses both gets one event per blow, not two. Prefer the runtime for damage that needs types, zones, armor or time-to-kill; the effect path stays for pools drained directly through the entity catalog's `receive` rules and now announces the same event with the effect id as `type`.
|
|
151
|
+
|
|
148
152
|
- **Type × resistance table** (`combat/damageTable`) — `{ types: [{ id, vs: { armor: 2, flesh: 0.5 }, bypassArmor? }], armor: { halfValue, maxMitigation }, defaultMultiplier? }`. `typeMultiplier` reads one pair; `resolveTableDamage` folds type, armor, and base into a row; `readDamageTable` returns every row at given armor values and `formatDamageTable` prints them. Armor uses `armor / (armor + halfValue)` — armor equal to `halfValue` halves the hit — so a designer reads the number rather than guessing a percentage per point. Unknown types throw; unknown resistance classes use the default.
|
|
149
153
|
- **Hit zones and crits** (`combat/hitZones`) — `{ zones: [{ id, multiplier?, critChance?, critMultiplier? }], defaultZone, critChance?, critMultiplier? }`. `resolveHitZone(table, zone, rng)` rolls once and returns **one** multiplier: a crit replaces the zone's normal multiplier (head 1.5× normal, 2.5× crit → 2.5×, never 3.75×). `hitZoneMultiplier(table, zone, crit)` quotes the same number without a roll, which is what `timeToKill({ crit: true })` uses.
|
|
150
154
|
- **Statuses** (`combat/statusRuntime`) — `createStatusRuntime({ statuses, rng?, applyDamage? })`; each `StatusDef` declares `{ id, stack, durationMs, intervalMs?, magnitude?, effect?, chance? }`. `stack` is the existing `StatusStackPolicy` (`refresh` / `stack` to `max` / `replace` / `ignore`), and it is the only thing that changes stacks: a tick deals `magnitude × stacks`, so two burns at 4 per tick deal 8, never 16. `effect` is `{ kind: "damage", channel }` (routed through `applyDamage`, so resistances and armor apply) or `{ kind: "stat", stat }` (read with `statDelta`, e.g. negative `moveSpeed` for slow or negative `armor` for corrode). Reapplication restarts the tick clock instead of banking a burst; ticks per call are bounded and the remainder carries. `dotField` stays the cheap no-stacking scheduler.
|
|
@@ -73,6 +73,25 @@ Validate generated quest catalogs with `validateQuestCatalog` (`game/questCatalo
|
|
|
73
73
|
Follow the [quest authoring recipe](recipes/quest-authoring.md) for bounded batches,
|
|
74
74
|
canonical facts, external unlock declarations, and precise repair diagnostics.
|
|
75
75
|
|
|
76
|
+
Run a mission with `createMissionRuntime` (`mission/mission`): a mission is data — a graph of
|
|
77
|
+
objectives the machine walks, with named fail states, per-objective timers, conditional branches and
|
|
78
|
+
rewards. `escortMission`, `chaseMission` and `defendMission` (`mission/compositions`) are compositions
|
|
79
|
+
of the same `reach`/`interact`/`survive`/`pursue`/`kill`/`collect` kinds, not archetypes: an escort is
|
|
80
|
+
a `reach` with an `actor-died` guard, a defend is a `survive` with a survivor floor, a chase is a
|
|
81
|
+
`pursue` with an escape guard. Start, advance and fail missions from authored scene markers with
|
|
82
|
+
`registerMissionTriggerActions` and `createMissionTriggerBridge` (`mission/triggers`); positional
|
|
83
|
+
objectives read `missionMarkers(document)`. Rewards route through the quest appliers and the loot
|
|
84
|
+
registry (`applyMissionRewards`). `validateMissionCatalog` is the content gate and it fails a mission
|
|
85
|
+
whose success path is counters alone — `createMissionRuntime` runs it at construction, so a
|
|
86
|
+
counter-only mission throws instead of shipping.
|
|
87
|
+
|
|
88
|
+
Report what happened instead of polling your own counters: `tick(dt, { events })` takes
|
|
89
|
+
`MissionWorldEvent`s (`kill`/`collect`/`deliver` with a target, actor and tags), an objective's `match`
|
|
90
|
+
filter credits only matching ones - a decoy, a wrong weapon or a rival's kill does not advance it - and
|
|
91
|
+
a `failOn` of kind `event` or `left-area` ends the run on a matching event or on an actor leaving an
|
|
92
|
+
authored marker. `view()` is everything a HUD needs: label, `remaining`, `counted`/`count`,
|
|
93
|
+
`step`/`steps`, `outcome` and `reason`.
|
|
94
|
+
|
|
76
95
|
For cross-catalog sources and skill gates, compose `validateContentReferences` and
|
|
77
96
|
`validateContentProgression` (`game/contentValidation`) with `validateRecipeCatalog`
|
|
78
97
|
(`crafting/recipeCatalog`). Follow [content validation](reference-content-validation.md).
|
|
@@ -102,6 +121,10 @@ Prefer `defineSystem` and `defineGame({ systems })` for scheduled capabilities.
|
|
|
102
121
|
|
|
103
122
|
`createNearAgents` (`sim/nearAgents`) is the default near half: a nav crowd with a need set and a utility brain per agent, counting its decisions for tests and probes. Give it the same need definitions the off-screen world decays, or the two halves disagree at the boundary.
|
|
104
123
|
|
|
124
|
+
`createVisitorPopulation` (`sim/population`) is the crowd a scene reports, and it is the bodies: a visitor joins only when your `VisitorWorld.admit` has made its body, and only while `VisitorWorld.has` holds it, so `onSite()` is the member count a HUD reads in O(1) and `arrivalsToday()` is the sum of the admissions the world accepted rather than a tally kept beside them. Arrival rate, capacity (a number or a function of the clock), the leaving rule and the `inside` bounds check are game policy; a refused body or one placed outside the bounds is released and never counted. `rollDay()` closes the day without emptying the park, `adopt` takes over a body a save or a seed made, and a restore only brings back members the world still holds.
|
|
125
|
+
|
|
126
|
+
Rides and their lines are `createRideSystem` and `createWaitQueue` (`sim/rides`, `sim/queue`): a ride cycles load → ride → unload at its own capacity, wears against its own breakdown curve, and reports boards, alights, breakdowns and repair costs through a sink; the wait a guest faces comes from the queue's own measured boarding throughput, not a configured constant. Guest choice stays on the utility brain — `rideChoiceOptions` compiles a guest's `RideTaste` (intensity comfort, nausea comfort, queue tolerance) and the park's ratings into options, `rideChoiceCandidates` costs each one by its own line's wait, and `createRideChooser` wires both to one guest. Ratings, cycle times, breakdown curves and costs are caller data, and `retune` changes them in play.
|
|
127
|
+
|
|
105
128
|
For staffed production, use `stationOutputRate` (`work/staffedStation`) to resolve
|
|
106
129
|
caller-owned worker stats and efficiency, then feed the rate to `tickProduction`.
|
|
107
130
|
Keep staffing and production progress in the same serializable store; the
|
|
@@ -151,6 +174,38 @@ A character kit-bashed from primitives/`ModelPart`s (no skeleton, no clips) anim
|
|
|
151
174
|
- `ctx.game.chat.send(userId, text)` targets global chat; `recent({ limit })` returns at most 100 messages. The default is 240 characters and one message per author per channel every two seconds. Rejected messages need visible UI feedback.
|
|
152
175
|
- Hosts reuse `validateChatMessage` and `decideRateWindow`, storing the rate window transactionally with each accepted operation. Preserve chat rate windows when saving/restoring chat state.
|
|
153
176
|
|
|
177
|
+
Price goods with `createMarket` (`economy/market`): `price = clamp(basePrice × (coverPeriods × demand / max(stock, 1))^elasticity)`
|
|
178
|
+
between `basePrice × minFactor` and `basePrice × maxFactor`, over finite integer
|
|
179
|
+
stock. `sell` refuses a sale it cannot cover and names the shortfall, `restock`
|
|
180
|
+
lowers the price, `recordDemand(unitsSold, unitsUnmet)` moves demand from what
|
|
181
|
+
buyers actually took, and `priceCurve`/`retune` change the policy. Amounts are
|
|
182
|
+
integer minor units, so a long simulation never drifts.
|
|
183
|
+
|
|
184
|
+
Keep the books with `createTransactionLedger` (`economy/transactionLedger`):
|
|
185
|
+
per-account balances in integer minor units plus a journal recording every
|
|
186
|
+
movement, the balance it produced and a caller-owned `reason`. `post` is the one
|
|
187
|
+
money path, and `totals()` keeps a per-reason count and net past the `maxEntries`
|
|
188
|
+
journal cap. Register upkeep, rent or a subscription with `addCharge` and settle
|
|
189
|
+
it on its cadence with `chargeDue` — bounded per charge and per advance, and a
|
|
190
|
+
cycle with no funds is counted as missed rather than overdrawing.
|
|
191
|
+
|
|
192
|
+
Lend and borrow with `buildLoanSchedule` / `createLoanBook` (`economy/loanBook`):
|
|
193
|
+
a schedule in integer minor units (`"amortized"` or `"flat"`), the principal
|
|
194
|
+
posted on the draw, interest and principal posted as separate reason-tagged
|
|
195
|
+
entries, and arrears that count once per advance until the loan defaults at
|
|
196
|
+
`maxMissed`. `repay` pays principal early; a save stores terms and progress and
|
|
197
|
+
the schedule is rebuilt on restore.
|
|
198
|
+
|
|
199
|
+
One document saves the whole world. `createGameSave` (`game/gameSave`) holds one versioned
|
|
200
|
+
`GameSaveDocument` with a section per registered system, named slots, caller-policy autosave and a
|
|
201
|
+
`migrate` path, over the same injected `SaveBackend` `createSaveStore` uses. Register each stateful
|
|
202
|
+
system once by `name` with its `snapshot()`/`restore()`; `capture()` builds the document,
|
|
203
|
+
`save()`/`touch()` persist it, `load()`/`switchSlot()` restore every section, and
|
|
204
|
+
`digest()`/`digests()` hash the state for a save/reload determinism test. Pass `rng: { seed, stream }`
|
|
205
|
+
so the world's shared seed and cursor survive the save. A snapshot field that means "never" must
|
|
206
|
+
serialize as `null` and be mapped back on restore - `Infinity` becomes `null` through JSON, which
|
|
207
|
+
silently changes a restored run.
|
|
208
|
+
|
|
154
209
|
For timed away missions, compose the pure transitions in `work/expedition` with
|
|
155
210
|
your existing `defineStore` and clock. The [expedition recipe](recipes/expeditions.md)
|
|
156
211
|
keeps bounded offline catch-up, finite supplies, return/death, capacity and RNG
|