@jgengine/core 0.24.0 → 0.26.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/ai/rvo.d.ts +106 -0
  3. package/dist/ai/rvo.js +581 -0
  4. package/dist/ai/utilityBrain.d.ts +173 -0
  5. package/dist/ai/utilityBrain.js +277 -0
  6. package/dist/combat/cover.d.ts +169 -0
  7. package/dist/combat/cover.js +264 -0
  8. package/dist/combat/damageRuntime.d.ts +150 -0
  9. package/dist/combat/damageRuntime.js +208 -0
  10. package/dist/combat/damageTable.d.ts +116 -0
  11. package/dist/combat/damageTable.js +165 -0
  12. package/dist/combat/hitZones.d.ts +70 -0
  13. package/dist/combat/hitZones.js +77 -0
  14. package/dist/combat/magazine.d.ts +8 -0
  15. package/dist/combat/magazine.js +20 -2
  16. package/dist/combat/squadCombat.d.ts +167 -0
  17. package/dist/combat/squadCombat.js +557 -0
  18. package/dist/combat/squadTactics.d.ts +73 -0
  19. package/dist/combat/squadTactics.js +49 -0
  20. package/dist/combat/statusRuntime.d.ts +142 -0
  21. package/dist/combat/statusRuntime.js +236 -0
  22. package/dist/combat/weaponDefinition.d.ts +106 -0
  23. package/dist/combat/weaponDefinition.js +108 -0
  24. package/dist/combat/weaponSystem.d.ts +189 -0
  25. package/dist/combat/weaponSystem.js +469 -0
  26. package/dist/combat.d.ts +9 -0
  27. package/dist/combat.js +9 -0
  28. package/dist/devtools/devtools.d.ts +4 -2
  29. package/dist/devtools/devtools.js +2 -1
  30. package/dist/devtools/discover.d.ts +2 -1
  31. package/dist/devtools/discover.js +38 -0
  32. package/dist/devtools/frame.d.ts +0 -2
  33. package/dist/devtools/frame.js +1 -2
  34. package/dist/devtools/logBus.d.ts +58 -0
  35. package/dist/devtools/logBus.js +79 -0
  36. package/dist/devtools/logs.d.ts +6 -3
  37. package/dist/devtools/logs.js +17 -13
  38. package/dist/devtools/report.d.ts +120 -0
  39. package/dist/devtools/report.js +49 -0
  40. package/dist/devtools/reportEndpoint.d.ts +17 -0
  41. package/dist/devtools/reportEndpoint.js +7 -0
  42. package/dist/devtools/types.d.ts +48 -11
  43. package/dist/editor/document.js +210 -0
  44. package/dist/editor/environment.d.ts +48 -1
  45. package/dist/editor/environment.js +161 -0
  46. package/dist/editor/index.d.ts +6 -2
  47. package/dist/editor/index.js +1 -1
  48. package/dist/editor/types.d.ts +54 -0
  49. package/dist/game/notices.d.ts +4 -0
  50. package/dist/game/notices.js +4 -3
  51. package/dist/game/playableGame.d.ts +9 -1
  52. package/dist/gameplay.d.ts +4 -0
  53. package/dist/gameplay.js +4 -0
  54. package/dist/meta/changelog.d.ts +1 -1
  55. package/dist/meta/changelog.js +78 -1
  56. package/dist/nav/navCrowd.d.ts +138 -0
  57. package/dist/nav/navCrowd.js +412 -0
  58. package/dist/nav/navRuntime.d.ts +108 -0
  59. package/dist/nav/navRuntime.js +256 -0
  60. package/dist/render/colorPipeline.d.ts +53 -0
  61. package/dist/render/colorPipeline.js +79 -0
  62. package/dist/render/lightProbes.d.ts +97 -0
  63. package/dist/render/lightProbes.js +221 -0
  64. package/dist/render/lookPreset.js +4 -4
  65. package/dist/render/physicalSky.d.ts +39 -0
  66. package/dist/render/physicalSky.js +89 -0
  67. package/dist/render/postProcessing.d.ts +40 -8
  68. package/dist/render/postProcessing.js +1 -1
  69. package/dist/render/skyOwnedLighting.d.ts +31 -0
  70. package/dist/render/skyOwnedLighting.js +46 -0
  71. package/dist/sensor/perception.d.ts +66 -5
  72. package/dist/sensor/perception.js +167 -42
  73. package/dist/sim/nearAgents.d.ts +68 -0
  74. package/dist/sim/nearAgents.js +152 -0
  75. package/dist/sim/needs.d.ts +60 -0
  76. package/dist/sim/needs.js +108 -0
  77. package/dist/sim/offScreen.d.ts +181 -0
  78. package/dist/sim/offScreen.js +450 -0
  79. package/dist/sim/routine.d.ts +79 -0
  80. package/dist/sim/routine.js +137 -0
  81. package/dist/visibility/visibilitySystem.js +21 -0
  82. package/dist/world/features.d.ts +8 -0
  83. package/dist/world.d.ts +5 -0
  84. package/dist/world.js +5 -0
  85. package/package.json +1 -1
  86. package/skills/jgengine/api.md +59 -6
  87. package/skills/jgengine/capabilities.md +4 -0
  88. package/skills/jgengine-combat/SKILL.md +17 -0
  89. package/skills/jgengine-combat/api.md +246 -0
  90. package/skills/jgengine-combat/capabilities.md +112 -0
  91. package/skills/jgengine-combat/reference.md +32 -0
  92. package/skills/jgengine-gameplay/SKILL.md +8 -0
  93. package/skills/jgengine-gameplay/api.md +71 -1
  94. package/skills/jgengine-gameplay/capabilities.md +16 -0
  95. package/skills/jgengine-world/SKILL.md +3 -1
  96. package/skills/jgengine-world/api.md +107 -3
  97. package/skills/jgengine-world/capabilities.md +22 -2
  98. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
@@ -0,0 +1,108 @@
1
+ import { decayMeters, initDecayMeters, } from "../survival/decayMeter.js";
2
+ import { clamp } from "../math/scalar.js";
3
+ /**
4
+ * Create the needs an agent or a park guest carries. Decay reuses the survival meter formulas
5
+ * (`survival/decayMeter`), so moodle thresholds, clamping and rate modifiers behave exactly like
6
+ * every other meter; this set adds index-addressed reads and single-need retuning for hot AI loops.
7
+ * Score these needs into an action with `createUtilityBrain` (`@jgengine/core/world`).
8
+ *
9
+ * @capability needs decaying needs an agent reads by index to decide what it wants next
10
+ */
11
+ export function createNeedSet(definitions) {
12
+ const defs = definitions.map((definition) => ({ ...definition }));
13
+ const index = new Map();
14
+ const ids = [];
15
+ const spans = new Float64Array(defs.length);
16
+ for (let i = 0; i < defs.length; i += 1) {
17
+ const def = defs[i];
18
+ index.set(def.id, i);
19
+ ids.push(def.id);
20
+ spans[i] = def.max - (def.min ?? 0);
21
+ }
22
+ let values = initDecayMeters(defs);
23
+ let modifiers = {};
24
+ const lookup = (id) => {
25
+ const at = index.get(id);
26
+ if (at === undefined)
27
+ throw new Error(`unknown need "${id}"`);
28
+ return at;
29
+ };
30
+ const readValue = (at) => {
31
+ const current = values[ids[at]];
32
+ return current === undefined ? defs[at].start ?? defs[at].max : current;
33
+ };
34
+ const readFraction = (at) => {
35
+ const span = spans[at];
36
+ return span > 0 ? clamp((readValue(at) - (defs[at].min ?? 0)) / span, 0, 1) : 0;
37
+ };
38
+ return {
39
+ tick(dt) {
40
+ if (dt > 0)
41
+ values = decayMeters(values, defs, dt, modifiers);
42
+ },
43
+ ids: () => ids,
44
+ indexOf: (id) => index.get(id) ?? -1,
45
+ valueAt: (at) => (at >= 0 && at < defs.length ? readValue(at) : 0),
46
+ fractionAt: (at) => (at >= 0 && at < defs.length ? readFraction(at) : 0),
47
+ value: (id) => readValue(lookup(id)),
48
+ fraction: (id) => readFraction(lookup(id)),
49
+ refill(id, amount) {
50
+ const at = lookup(id);
51
+ const def = defs[at];
52
+ const min = def.min ?? 0;
53
+ values = { ...values, [id]: clamp(readValue(at) + amount, min, def.max) };
54
+ },
55
+ setRate(id, rate) {
56
+ defs[lookup(id)].rate = rate;
57
+ },
58
+ setRateModifier(id, multiplier) {
59
+ if (id === null) {
60
+ for (const needId of ids)
61
+ modifiers[needId] = multiplier;
62
+ return;
63
+ }
64
+ lookup(id);
65
+ modifiers = { ...modifiers, [id]: multiplier };
66
+ },
67
+ mostUrgent() {
68
+ let worst = -1;
69
+ let worstFraction = Infinity;
70
+ for (let at = 0; at < defs.length; at += 1) {
71
+ const fraction = readFraction(at);
72
+ if (fraction < worstFraction) {
73
+ worst = at;
74
+ worstFraction = fraction;
75
+ }
76
+ }
77
+ if (worst < 0)
78
+ return null;
79
+ const id = ids[worst];
80
+ return { id, value: readValue(worst), fraction: worstFraction };
81
+ },
82
+ snapshot() {
83
+ const out = {};
84
+ const rates = {};
85
+ for (let at = 0; at < defs.length; at += 1) {
86
+ const id = ids[at];
87
+ out[id] = readValue(at);
88
+ rates[id] = defs[at].rate;
89
+ }
90
+ return { values: out, rates, modifiers: { ...modifiers } };
91
+ },
92
+ restore(next) {
93
+ const restored = {};
94
+ for (let at = 0; at < defs.length; at += 1) {
95
+ const def = defs[at];
96
+ const id = ids[at];
97
+ const min = def.min ?? 0;
98
+ const saved = next.values[id];
99
+ restored[id] = saved === undefined || !Number.isFinite(saved) ? readValue(at) : clamp(saved, min, def.max);
100
+ const rate = next.rates[id];
101
+ if (rate !== undefined && Number.isFinite(rate))
102
+ def.rate = rate;
103
+ }
104
+ values = restored;
105
+ modifiers = { ...next.modifiers };
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,181 @@
1
+ import type { Vec3 } from "../world/geometry.js";
2
+ import type { NeedDefinition } from "./needs.js";
3
+ import type { RoutineTimeline } from "./routine.js";
4
+ /** Which side of the distance policy a resident is on: simulated near the viewer, or cheap state. */
5
+ export type OffScreenMode = "near" | "far";
6
+ /**
7
+ * Where the near/far boundary is and what it costs. The radius, the cost model and the budget are
8
+ * the game's call — a city block and a haunted-house hallway do not share a number.
9
+ */
10
+ export interface OffScreenPolicy {
11
+ /** Distance at or below which a resident is simulated near the viewer. */
12
+ readonly nearRadius: number;
13
+ /** Distance past which a near resident becomes cheap state. Defaults to `nearRadius` (no band). */
14
+ readonly farRadius?: number;
15
+ /**
16
+ * Estimated per-tick cost of keeping one resident near, in the caller's own units. Only read when
17
+ * `nearBudget` is set.
18
+ */
19
+ readonly costModel?: (id: string, distance: number) => number;
20
+ /** Ceiling on the summed near cost; over-budget residents nearest the boundary go cheap first. */
21
+ readonly nearBudget?: number;
22
+ /** Most spawn/despawn switches one tick may perform, so a teleporting viewer cannot hitch a frame. Default 32. */
23
+ readonly switchBudget?: number;
24
+ }
25
+ /** Everything a near-side simulator is told when a resident crosses into range. */
26
+ export interface NearSpawn {
27
+ readonly id: string;
28
+ /** Cheap-state position, so the agent appears where it already was — never at the origin. */
29
+ readonly position: Vec3;
30
+ /** Place the resident occupies, `null` for a resident with no authored routine. */
31
+ readonly place: string | null;
32
+ readonly activity: string | null;
33
+ /** Current need values from the cheap state; the near side resumes from these. */
34
+ readonly needs: Readonly<Record<string, number>>;
35
+ /** Where it should walk in the current phase (its place, or the place it is walking to). */
36
+ readonly goal: Vec3;
37
+ readonly stop: number;
38
+ readonly travelling: boolean;
39
+ }
40
+ /** What a near-side simulator hands back when a resident leaves range. */
41
+ export interface NearHandoff {
42
+ readonly position: Vec3;
43
+ readonly needs: Readonly<Record<string, number>>;
44
+ }
45
+ /**
46
+ * The near half of the world: the game's real simulation. Only residents inside the radius are ever
47
+ * spawned here, which is what keeps far agents free of navigation and utility scoring.
48
+ */
49
+ export interface NearAgents {
50
+ spawn(next: NearSpawn): void;
51
+ /** Remove a near resident and hand back its live position and needs for the cheap state. */
52
+ despawn(id: string): NearHandoff | null;
53
+ /** Live position, or `null` when the resident is not spawned. */
54
+ position(id: string): Vec3 | null;
55
+ /** Live need values, when the near side keeps them; the cheap state takes these on handoff. */
56
+ needs?(id: string): Readonly<Record<string, number>> | null;
57
+ /** Retarget a near resident when its routine moves on to another place. */
58
+ setGoal(id: string, goal: Vec3): void;
59
+ /** Advance every near resident by `dt` seconds. */
60
+ tick(dt: number, now: number): void;
61
+ /** Utility decisions taken since creation, when the near side keeps brains. */
62
+ decisions?(): number;
63
+ dispose?(): void;
64
+ }
65
+ /** One resident of the world: an identity, where it belongs, and the day it follows. */
66
+ export interface OffScreenResidentSpec {
67
+ readonly id: string;
68
+ /** Where the resident stands when its routine says nothing; also the fallback for a missing place. */
69
+ readonly home: Vec3;
70
+ /** Authored day; omitted means it stays home. */
71
+ readonly routine?: RoutineTimeline;
72
+ /** Saved need values; omitted needs start at their definition's `start ?? max`. */
73
+ readonly needs?: Readonly<Record<string, number>>;
74
+ /** Force this resident near regardless of distance (a cutscene, a quest NPC). */
75
+ readonly pinned?: boolean;
76
+ }
77
+ /** A resident as a game or the devtools overlay reads it. Allocates a need copy — debug and tests. */
78
+ export interface OffScreenAgentView {
79
+ readonly id: string;
80
+ readonly mode: OffScreenMode;
81
+ readonly position: Vec3;
82
+ readonly place: string | null;
83
+ readonly activity: string | null;
84
+ readonly stop: number;
85
+ readonly travelling: boolean;
86
+ readonly needs: Readonly<Record<string, number>>;
87
+ readonly pinned: boolean;
88
+ /** Distance from the viewer at the last tick. */
89
+ readonly distance: number;
90
+ }
91
+ /** Counters a devtools probe or a test reads to prove far agents cost nothing. */
92
+ export interface OffScreenStats {
93
+ agents: number;
94
+ near: number;
95
+ far: number;
96
+ promotions: number;
97
+ demotions: number;
98
+ /** Cheap-state boundary updates — the only per-tick work a far resident can cause. */
99
+ farRefreshes: number;
100
+ /** Near-side utility decisions, summed across the near residents' brains. */
101
+ decisions: number;
102
+ }
103
+ /** Serializable world state: the policy and every resident, near ones included. */
104
+ export interface OffScreenSnapshot {
105
+ policy: {
106
+ nearRadius: number;
107
+ farRadius: number;
108
+ nearBudget: number | null;
109
+ switchBudget: number;
110
+ };
111
+ residents: {
112
+ id: string;
113
+ home: Vec3;
114
+ mode: OffScreenMode;
115
+ position: Vec3;
116
+ place: string | null;
117
+ activity: string | null;
118
+ stop: number;
119
+ travelling: boolean;
120
+ pinned: boolean;
121
+ updatedAt: number;
122
+ staleAt: number;
123
+ needs: Record<string, number>;
124
+ }[];
125
+ }
126
+ /** A policy change that stays reachable in play. */
127
+ export interface OffScreenPolicyPatch {
128
+ readonly nearRadius?: number;
129
+ readonly farRadius?: number;
130
+ readonly costModel?: (id: string, distance: number) => number;
131
+ readonly nearBudget?: number;
132
+ readonly switchBudget?: number;
133
+ }
134
+ /** Options for {@link createOffScreenWorld}. */
135
+ export interface OffScreenOptions {
136
+ /** Routine place id → world anchor. Every place a resident's routine names must exist here. */
137
+ readonly places: Readonly<Record<string, Vec3>>;
138
+ /** Need definitions every resident shares; the far model decays these closed-form. */
139
+ readonly needs: readonly NeedDefinition[];
140
+ readonly policy: OffScreenPolicy;
141
+ /** The game's near simulation. Far residents never touch it. */
142
+ readonly near: NearAgents;
143
+ readonly residents?: readonly OffScreenResidentSpec[];
144
+ /** Register a devtools probe under this name so the overlay shows near/far counts. */
145
+ readonly devtoolsName?: string;
146
+ }
147
+ /**
148
+ * The off-screen world: every resident exists, but only the ones within the policy radius are
149
+ * simulated. The rest are a position, a routine phase and a needs record that only move when the
150
+ * clock crosses a routine boundary — no navigation, no utility scoring, no per-frame need decay.
151
+ *
152
+ * Crossing the boundary is a handoff, not a respawn: a resident arrives where the cheap state says
153
+ * it is, with its schedule progress and needs intact, and leaves with the same identity.
154
+ */
155
+ export interface OffScreenWorld {
156
+ add(spec: OffScreenResidentSpec): boolean;
157
+ remove(id: string): boolean;
158
+ has(id: string): boolean;
159
+ count(): number;
160
+ ids(): readonly string[];
161
+ agent(id: string): OffScreenAgentView | null;
162
+ /** Keep a resident near regardless of distance; `false` clears the pin. */
163
+ pin(id: string, pinned?: boolean): boolean;
164
+ /** Cheap-state needs as of `now`, resolved through any routine boundary since the last tick. */
165
+ needsAt(id: string, now: number): Readonly<Record<string, number>> | null;
166
+ /** Split the world by distance, run the near simulation, and advance cheap routine phase. */
167
+ tick(dt: number, now: number, viewer: Vec3): void;
168
+ stats(): OffScreenStats;
169
+ retune(patch: OffScreenPolicyPatch): void;
170
+ snapshot(): OffScreenSnapshot;
171
+ restore(next: OffScreenSnapshot): void;
172
+ dispose(): void;
173
+ }
174
+ /**
175
+ * Create the world that decides which agents deserve a simulation. Residents are cheap state until
176
+ * they come near, and cheap again when they leave; the cheap state is the schedule, the needs and
177
+ * the last place, so a returning resident never pops in at the origin.
178
+ *
179
+ * @capability offscreen-world schedules agents near the player as real state and far ones as cheap state
180
+ */
181
+ export declare function createOffScreenWorld(options: OffScreenOptions): OffScreenWorld;