@jgengine/core 0.28.0 → 0.30.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 (42) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agents/agent.d.ts +134 -0
  3. package/dist/agents/agent.js +224 -0
  4. package/dist/agents/mood.d.ts +74 -0
  5. package/dist/agents/mood.js +97 -0
  6. package/dist/agents/relationships.d.ts +92 -0
  7. package/dist/agents/relationships.js +132 -0
  8. package/dist/agents/schedule.d.ts +58 -0
  9. package/dist/agents/schedule.js +72 -0
  10. package/dist/ai/director/boss.d.ts +63 -0
  11. package/dist/ai/director/boss.js +87 -0
  12. package/dist/ai/director/encounter.d.ts +146 -0
  13. package/dist/ai/director/encounter.js +195 -0
  14. package/dist/ai/director/pressure.d.ts +58 -0
  15. package/dist/ai/director/pressure.js +70 -0
  16. package/dist/ai/director/roles.d.ts +61 -0
  17. package/dist/ai/director/roles.js +118 -0
  18. package/dist/ai/director/sound.d.ts +79 -0
  19. package/dist/ai/director/sound.js +113 -0
  20. package/dist/game/playableGame.d.ts +2 -2
  21. package/dist/meta/changelog.d.ts +1 -1
  22. package/dist/meta/changelog.js +30 -1
  23. package/dist/mission/mission.d.ts +1 -1
  24. package/dist/mission/mission.js +4 -1
  25. package/dist/progression/careerBank.d.ts +63 -0
  26. package/dist/progression/careerBank.js +83 -0
  27. package/dist/progression/gates.d.ts +66 -0
  28. package/dist/progression/gates.js +88 -0
  29. package/dist/progression/standing.d.ts +77 -0
  30. package/dist/progression/standing.js +151 -0
  31. package/dist/progression/upgrades.d.ts +70 -0
  32. package/dist/progression/upgrades.js +128 -0
  33. package/dist/ui/accessibility.d.ts +8 -0
  34. package/dist/ui/accessibility.js +14 -0
  35. package/package.json +1 -1
  36. package/skills/jgengine/api.md +2 -2
  37. package/skills/jgengine-gameplay/SKILL.md +2 -0
  38. package/skills/jgengine-gameplay/api.md +89 -0
  39. package/skills/jgengine-gameplay/capabilities.md +34 -0
  40. package/skills/jgengine-gameplay/recipes/agent-lives.md +76 -0
  41. package/skills/jgengine-world/api.md +52 -0
  42. package/skills/jgengine-world/capabilities.md +32 -0
@@ -0,0 +1,151 @@
1
+ import { createReputationLedger, DEFAULT_REPUTATION_TIERS, } from "../faction/reputation.js";
2
+ function check(condition, message) {
3
+ if (!condition)
4
+ throw new RangeError(message);
5
+ }
6
+ function isFiniteNumber(value) {
7
+ return typeof value === "number" && Number.isFinite(value);
8
+ }
9
+ /** Check authored standing content before use; throws `RangeError` naming the offending field. */
10
+ export function assertStandingConfig(config) {
11
+ const trackIds = new Set();
12
+ for (const track of config.tracks) {
13
+ const id = track.id.trim();
14
+ check(id !== "", "Standing track id must not be empty");
15
+ check(!trackIds.has(id), `Standing track "${id}" is declared twice`);
16
+ trackIds.add(id);
17
+ if (track.min !== undefined)
18
+ check(isFiniteNumber(track.min), `Standing track "${id}" min must be finite`);
19
+ if (track.max !== undefined)
20
+ check(isFiniteNumber(track.max), `Standing track "${id}" max must be finite`);
21
+ check(track.min === undefined || track.max === undefined || track.min <= track.max, `Standing track "${id}" needs min <= max`);
22
+ if (track.initial !== undefined) {
23
+ check(isFiniteNumber(track.initial), `Standing track "${id}" initial must be finite`);
24
+ check((track.min === undefined || track.initial >= track.min) && (track.max === undefined || track.initial <= track.max), `Standing track "${id}" initial ${track.initial} is outside its bounds`);
25
+ }
26
+ const tiers = track.tiers ?? DEFAULT_REPUTATION_TIERS;
27
+ check(tiers.length > 0, `Standing track "${id}" needs at least one tier`);
28
+ const tierIds = new Set();
29
+ for (const tier of tiers) {
30
+ check(tier.id.trim() !== "", `Standing track "${id}" has a tier with an empty id`);
31
+ check(!tierIds.has(tier.id), `Standing track "${id}" declares tier "${tier.id}" twice`);
32
+ check(isFiniteNumber(tier.min), `Standing track "${id}" tier "${tier.id}" min must be finite`);
33
+ tierIds.add(tier.id);
34
+ }
35
+ }
36
+ const outcomeIds = new Set();
37
+ for (const outcome of config.outcomes ?? []) {
38
+ const id = outcome.id.trim();
39
+ check(id !== "", "Standing outcome id must not be empty");
40
+ check(!outcomeIds.has(id), `Standing outcome "${id}" is declared twice`);
41
+ outcomeIds.add(id);
42
+ const impacts = Object.entries(outcome.impacts);
43
+ check(impacts.length > 0, `Standing outcome "${id}" moves no track`);
44
+ for (const [trackId, delta] of impacts) {
45
+ check(trackIds.has(trackId), `Standing outcome "${id}" names unknown track "${trackId}"`);
46
+ check(isFiniteNumber(delta), `Standing outcome "${id}" impact on "${trackId}" must be finite`);
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * Per-faction or per-district standing that rises and falls from named outcomes, over authored
52
+ * tracks. Numbers live in the shared reputation ledger (`faction/reputation`); this adds the
53
+ * authored outcome table, tier-per-track and the one snapshot a save stores. An outcome id or
54
+ * track id that resolves to nothing throws at the call, so authored content fails loudly.
55
+ *
56
+ * @capability standing per-track standing moved by authored outcomes, with tiers and one snapshot
57
+ */
58
+ export function createStanding(config) {
59
+ assertStandingConfig(config);
60
+ const tracks = new Map();
61
+ for (const track of config.tracks)
62
+ tracks.set(track.id, track);
63
+ const outcomes = new Map();
64
+ for (const outcome of config.outcomes ?? [])
65
+ outcomes.set(outcome.id, outcome);
66
+ const ledgers = new Map();
67
+ for (const track of config.tracks) {
68
+ ledgers.set(track.id, createReputationLedger({
69
+ tiers: track.tiers ?? DEFAULT_REPUTATION_TIERS,
70
+ initial: { [track.id]: track.initial ?? 0 },
71
+ ...(track.min === undefined ? {} : { min: track.min }),
72
+ ...(track.max === undefined ? {} : { max: track.max }),
73
+ }));
74
+ }
75
+ function ledgerFor(trackId) {
76
+ const ledger = ledgers.get(trackId);
77
+ if (ledger === undefined)
78
+ throw new RangeError(`Standing track "${trackId}" is not authored`);
79
+ return ledger;
80
+ }
81
+ return {
82
+ tracks: () => config.tracks.slice(),
83
+ hasTrack: (trackId) => tracks.has(trackId),
84
+ hasOutcome: (outcomeId) => outcomes.has(outcomeId),
85
+ standing(actorId, trackId) {
86
+ return ledgerFor(trackId).standing(actorId, trackId);
87
+ },
88
+ tier: (actorId, trackId) => ledgerFor(trackId).tier(actorId, trackId),
89
+ standings(actorId) {
90
+ const out = {};
91
+ for (const trackId of ledgers.keys())
92
+ out[trackId] = ledgerFor(trackId).standing(actorId, trackId);
93
+ return out;
94
+ },
95
+ apply(actorId, outcomeId) {
96
+ const outcome = outcomes.get(outcomeId);
97
+ if (outcome === undefined)
98
+ throw new RangeError(`Standing outcome "${outcomeId}" is not authored`);
99
+ const changes = [];
100
+ for (const [trackId, delta] of Object.entries(outcome.impacts)) {
101
+ const ledger = ledgerFor(trackId);
102
+ const before = ledger.standing(actorId, trackId);
103
+ const previousTier = ledger.tier(actorId, trackId).id;
104
+ ledger.gain(actorId, trackId, delta);
105
+ const after = ledger.standing(actorId, trackId);
106
+ changes.push({ trackId, before, after, delta: after - before, previousTier, tier: ledger.tier(actorId, trackId).id });
107
+ }
108
+ return changes;
109
+ },
110
+ adjust(actorId, trackId, delta) {
111
+ const ledger = ledgerFor(trackId);
112
+ const before = ledger.standing(actorId, trackId);
113
+ const previousTier = ledger.tier(actorId, trackId).id;
114
+ ledger.gain(actorId, trackId, delta);
115
+ const after = ledger.standing(actorId, trackId);
116
+ return { trackId, before, after, delta: after - before, previousTier, tier: ledger.tier(actorId, trackId).id };
117
+ },
118
+ meets(actorId, requirement) {
119
+ const value = ledgerFor(requirement.trackId).standing(actorId, requirement.trackId);
120
+ return (requirement.min === undefined || value >= requirement.min) && (requirement.max === undefined || value <= requirement.max);
121
+ },
122
+ snapshot() {
123
+ const records = {};
124
+ for (const [trackId, ledger] of ledgers) {
125
+ for (const [actorId, row] of Object.entries(ledger.snapshot().records)) {
126
+ const target = records[actorId] ?? (records[actorId] = {});
127
+ const value = row[trackId];
128
+ if (value !== undefined)
129
+ target[trackId] = value;
130
+ }
131
+ }
132
+ return { records };
133
+ },
134
+ restore(next) {
135
+ const byTrack = new Map();
136
+ for (const [actorId, row] of Object.entries(next.records ?? {})) {
137
+ for (const [trackId, value] of Object.entries(row)) {
138
+ if (!ledgers.has(trackId) || !isFiniteNumber(value))
139
+ continue;
140
+ const target = byTrack.get(trackId);
141
+ if (target === undefined)
142
+ byTrack.set(trackId, { [actorId]: { [trackId]: value } });
143
+ else
144
+ target[actorId] = { [trackId]: value };
145
+ }
146
+ }
147
+ for (const [trackId, ledger] of ledgers)
148
+ ledger.restore({ records: byTrack.get(trackId) ?? {} });
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,70 @@
1
+ import { type Curve } from "../game/progression.js";
2
+ import type { Standing, StandingRequirement } from "./standing.js";
3
+ /**
4
+ * One upgrade node. Its price is the shared progression {@link Curve} evaluated at the rank being
5
+ * bought, so a game authors one curve and every rank of every node follows it.
6
+ */
7
+ export interface UpgradeNodeDef {
8
+ id: string;
9
+ label?: string;
10
+ /** Cost of rank `n`, priced by this curve at `n` (1-based). */
11
+ cost: Curve;
12
+ /** Ranks purchasable (default `1`). */
13
+ maxRank?: number;
14
+ /** Node ids that must be owned before this one can be bought. */
15
+ requires?: readonly string[];
16
+ standing?: StandingRequirement;
17
+ }
18
+ /** Plain JSON state: node id → ranks owned. Nodes at rank `0` are absent. */
19
+ export interface UpgradeState {
20
+ ranks: Readonly<Record<string, number>>;
21
+ }
22
+ /** Why a purchase was refused; the tree is left untouched on any rejection. */
23
+ export type UpgradeRejection = "unknown-node" | "max-rank" | "missing-prerequisites" | "standing" | "insufficient-funds";
24
+ /** Result of {@link UpgradeTree.buy}: the rank reached and what it cost, or why it was refused. */
25
+ export type UpgradeBuy = {
26
+ ok: true;
27
+ nodeId: string;
28
+ rank: number;
29
+ cost: number;
30
+ } | {
31
+ ok: false;
32
+ reason: UpgradeRejection;
33
+ };
34
+ /** Authored upgrade nodes with a per-node cost curve, prerequisites, a standing gate and one snapshot. */
35
+ export interface UpgradeTree {
36
+ nodes(): readonly UpgradeNodeDef[];
37
+ node(nodeId: string): UpgradeNodeDef | null;
38
+ /** Ranks owned for `nodeId`; `0` when unknown or unowned. */
39
+ rank(nodeId: string): number;
40
+ maxRank(nodeId: string): number;
41
+ /** Price of `rank` (default: the next rank), or `null` when unknown or past the cap. */
42
+ costOf(nodeId: string, rank?: number): number | null;
43
+ /** What buying the next rank needs, without changing anything. */
44
+ canBuy(nodeId: string, actorId: string): UpgradeBuy;
45
+ /** Buy the next rank, charging `pay` only after every gate passes. */
46
+ buy(nodeId: string, actorId: string, pay: (cost: number) => boolean): UpgradeBuy;
47
+ owned(): readonly string[];
48
+ spendTotal(): number;
49
+ snapshot(): UpgradeState;
50
+ restore(next: UpgradeState): void;
51
+ }
52
+ /** Authored nodes plus the standing a node's requirement reads. */
53
+ export interface UpgradeTreeConfig {
54
+ nodes: readonly UpgradeNodeDef[];
55
+ /** The standing a node's requirement reads, for both validation and purchase. */
56
+ standing?: Pick<Standing, "hasTrack" | "standing">;
57
+ /** Rehydrate from a prior {@link UpgradeTree.snapshot}. */
58
+ state?: UpgradeState;
59
+ }
60
+ /** Check authored upgrade nodes; throws `RangeError` naming the offending node or field. */
61
+ export declare function assertUpgrades(config: UpgradeTreeConfig): void;
62
+ /**
63
+ * An upgrade tree whose prices come from the shared curve primitive: each node authors one cost
64
+ * curve, each rank is that curve at the rank, and a purchase passes prerequisites, a standing gate
65
+ * and the caller's own payment step. State is one rank map that JSON round-trips, so a career save
66
+ * stores what a player paid for rather than the arithmetic behind it.
67
+ *
68
+ * @capability upgrade-tree ranked upgrades priced by a cost curve, gated by prerequisites and standing
69
+ */
70
+ export declare function createUpgradeTree(config: UpgradeTreeConfig): UpgradeTree;
@@ -0,0 +1,128 @@
1
+ import { evalCurve } from "../game/progression.js";
2
+ function check(condition, message) {
3
+ if (!condition)
4
+ throw new RangeError(message);
5
+ }
6
+ /** Check authored upgrade nodes; throws `RangeError` naming the offending node or field. */
7
+ export function assertUpgrades(config) {
8
+ const nodes = new Set();
9
+ for (const node of config.nodes) {
10
+ const id = node.id.trim();
11
+ check(id !== "", "Upgrade node id must not be empty");
12
+ check(!nodes.has(id), `Upgrade node "${id}" is declared twice`);
13
+ nodes.add(id);
14
+ const maxRank = node.maxRank ?? 1;
15
+ check(Number.isInteger(maxRank) && maxRank >= 1, `Upgrade node "${id}" maxRank must be an integer >= 1`);
16
+ for (let rank = 1; rank <= maxRank; rank += 1) {
17
+ const cost = evalCurve(node.cost, rank);
18
+ check(Number.isFinite(cost) && cost >= 0, `Upgrade node "${id}" cost at rank ${rank} must be finite and >= 0`);
19
+ }
20
+ const standing = node.standing;
21
+ if (standing !== undefined) {
22
+ check(standing.trackId.trim() !== "", `Upgrade node "${id}" names an empty standing track`);
23
+ check(config.standing === undefined || config.standing.hasTrack(standing.trackId), `Upgrade node "${id}" reads unknown standing track "${standing.trackId}"`);
24
+ check(standing.min === undefined || standing.max === undefined || standing.min <= standing.max, `Upgrade node "${id}" needs standing min <= max`);
25
+ }
26
+ }
27
+ for (const node of config.nodes) {
28
+ check(!(node.requires ?? []).includes(node.id), `Upgrade node "${node.id}" requires itself`);
29
+ for (const required of node.requires ?? []) {
30
+ check(nodes.has(required), `Upgrade node "${node.id}" requires unknown node "${required}"`);
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * An upgrade tree whose prices come from the shared curve primitive: each node authors one cost
36
+ * curve, each rank is that curve at the rank, and a purchase passes prerequisites, a standing gate
37
+ * and the caller's own payment step. State is one rank map that JSON round-trips, so a career save
38
+ * stores what a player paid for rather than the arithmetic behind it.
39
+ *
40
+ * @capability upgrade-tree ranked upgrades priced by a cost curve, gated by prerequisites and standing
41
+ */
42
+ export function createUpgradeTree(config) {
43
+ assertUpgrades(config);
44
+ const definitions = config.nodes.slice();
45
+ const byId = new Map();
46
+ for (const node of definitions)
47
+ byId.set(node.id, node);
48
+ const ranks = new Map();
49
+ let spent = 0;
50
+ function load(state) {
51
+ ranks.clear();
52
+ spent = 0;
53
+ for (const [nodeId, rank] of Object.entries(state.ranks ?? {})) {
54
+ const node = byId.get(nodeId);
55
+ if (node === undefined || !Number.isInteger(rank) || rank <= 0)
56
+ continue;
57
+ const capped = Math.min(rank, node.maxRank ?? 1);
58
+ ranks.set(nodeId, capped);
59
+ for (let index = 1; index <= capped; index += 1)
60
+ spent += evalCurve(node.cost, index);
61
+ }
62
+ }
63
+ if (config.state !== undefined)
64
+ load(config.state);
65
+ const rankOf = (nodeId) => ranks.get(nodeId) ?? 0;
66
+ function costAt(node, rank) {
67
+ if (!Number.isInteger(rank) || rank < 1 || rank > (node.maxRank ?? 1))
68
+ return null;
69
+ return evalCurve(node.cost, rank);
70
+ }
71
+ function checkBuy(node, actorId) {
72
+ const current = rankOf(node.id);
73
+ if (current >= (node.maxRank ?? 1))
74
+ return { ok: false, reason: "max-rank" };
75
+ const missing = (node.requires ?? []).filter((required) => rankOf(required) < 1);
76
+ if (missing.length > 0)
77
+ return { ok: false, reason: "missing-prerequisites" };
78
+ const standing = node.standing;
79
+ if (standing !== undefined) {
80
+ const reader = config.standing;
81
+ if (reader === undefined)
82
+ return { ok: false, reason: "standing" };
83
+ const value = reader.standing(actorId, standing.trackId);
84
+ if ((standing.min !== undefined && value < standing.min) || (standing.max !== undefined && value > standing.max)) {
85
+ return { ok: false, reason: "standing" };
86
+ }
87
+ }
88
+ const cost = costAt(node, current + 1);
89
+ if (cost === null)
90
+ return { ok: false, reason: "max-rank" };
91
+ return { ok: true, nodeId: node.id, rank: current + 1, cost };
92
+ }
93
+ return {
94
+ nodes: () => definitions.slice(),
95
+ node: (nodeId) => byId.get(nodeId) ?? null,
96
+ rank: rankOf,
97
+ maxRank: (nodeId) => byId.get(nodeId)?.maxRank ?? 0,
98
+ costOf(nodeId, rank) {
99
+ const node = byId.get(nodeId);
100
+ if (node === undefined)
101
+ return null;
102
+ return costAt(node, rank ?? rankOf(nodeId) + 1);
103
+ },
104
+ canBuy: (nodeId, actorId) => {
105
+ const node = byId.get(nodeId);
106
+ if (node === undefined)
107
+ return { ok: false, reason: "unknown-node" };
108
+ return checkBuy(node, actorId);
109
+ },
110
+ buy(nodeId, actorId, pay) {
111
+ const node = byId.get(nodeId);
112
+ if (node === undefined)
113
+ return { ok: false, reason: "unknown-node" };
114
+ const checkResult = checkBuy(node, actorId);
115
+ if (!checkResult.ok)
116
+ return checkResult;
117
+ if (!pay(checkResult.cost))
118
+ return { ok: false, reason: "insufficient-funds" };
119
+ ranks.set(nodeId, checkResult.rank);
120
+ spent += checkResult.cost;
121
+ return checkResult;
122
+ },
123
+ owned: () => Array.from(ranks.keys()),
124
+ spendTotal: () => spent,
125
+ snapshot: () => ({ ranks: Object.fromEntries(ranks) }),
126
+ restore: load,
127
+ };
128
+ }
@@ -29,6 +29,14 @@ export declare function reducedMotionDuration(state: AccessibilityState, ms: num
29
29
  * maps to `null` (no filter). Consumed by the React `ColorblindFilters` defs.
30
30
  */
31
31
  export declare const COLORBLIND_MATRICES: Record<ColorblindMode, string | null>;
32
+ /**
33
+ * The CSS filter that applies `mode` to one rendered frame — a `url(#jg-cb-<mode>)` reference to the
34
+ * `feColorMatrix` defs {@link COLORBLIND_MATRICES} drives. `"none"` maps to `null` (no filter), so a
35
+ * caller can spread it onto the canvas alone and leave HUD and menu text at its own contrast.
36
+ *
37
+ * @capability colorblind-frame-filter resolve a colorblind mode to the CSS filter for the 3D frame alone
38
+ */
39
+ export declare function colorblindFrameFilter(mode: ColorblindMode): string | null;
32
40
  /** Observable, serializable accessibility-preferences store. */
33
41
  export interface AccessibilityStore {
34
42
  get(): AccessibilityState;
@@ -32,6 +32,20 @@ export const COLORBLIND_MATRICES = {
32
32
  tritanopia: "0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0",
33
33
  grayscale: "0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0",
34
34
  };
35
+ /**
36
+ * The CSS filter that applies `mode` to one rendered frame — a `url(#jg-cb-<mode>)` reference to the
37
+ * `feColorMatrix` defs {@link COLORBLIND_MATRICES} drives. `"none"` maps to `null` (no filter), so a
38
+ * caller can spread it onto the canvas alone and leave HUD and menu text at its own contrast.
39
+ *
40
+ * @capability colorblind-frame-filter resolve a colorblind mode to the CSS filter for the 3D frame alone
41
+ */
42
+ export function colorblindFrameFilter(mode) {
43
+ const matrix = COLORBLIND_MATRICES[mode];
44
+ // An unknown id at runtime would reference a filter that does not exist, and a broken `url()` filter hides the element.
45
+ if (matrix === null || matrix === undefined)
46
+ return null;
47
+ return `url(#jg-cb-${mode})`;
48
+ }
35
49
  /**
36
50
  * Serializable, observable store of accessibility preferences (reduced motion,
37
51
  * high contrast, text scale, colorblind mode, captions). Genre-agnostic plumbing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.28.0",
3
+ "version": "0.30.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",
@@ -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.28.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.30.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.28.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
288
+ - `VERSION` (const): const VERSION: "0.30.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
289
289
 
290
290
  ## @jgengine/core/runtime/adapter
291
291
 
@@ -121,6 +121,8 @@ Prefer `defineSystem` and `defineGame({ systems })` for scheduled capabilities.
121
121
 
122
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.
123
123
 
124
+ `createAgent` (`agents/agent`) is one resident's whole life without the nav: needs decay through `sim/needs`, the utility brain picks among the targets the world offers, `agents/mood` reads a feeling from those needs plus named sources a queue or a litter patch raises, and an authored day plan (`agents/schedule`, which resolves `sim/routine`) puts the agent at work, at a meal or at home - including a work beat, where a stop names the places it patrols. A target that disappears falls to the agent's declared fallback. `createRelationshipBook` (`agents/relationships`) keeps familiarity and opinion per pair: a social action moves both, friends prefer each other's company, and a bonded pair refuses to split. Every part snapshots and restores. [Agent lives](recipes/agent-lives.md) wires both a park guest and a household being from the same factory.
125
+
124
126
  `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
127
 
126
128
  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.
@@ -2,6 +2,52 @@
2
2
 
3
3
  # jgengine-gameplay — exported API surface
4
4
 
5
+ ## @jgengine/core/agents/agent
6
+
7
+ - `Agent` (interface): interface Agent — Needs, mood, schedule and a chosen action, in one object a game ticks.
8
+ - `AgentAction` (interface): interface AgentAction — What the agent is doing.
9
+ - `AgentActionReason` (type): type AgentActionReason = UtilitySwitchReason | "fallback" | "schedule" | "cleared" — Why the action last changed.
10
+ - `AgentConfig` (interface): interface AgentConfig — Everything an agent is made of. Policy is data; nothing about a game is compiled in.
11
+ - `AgentFallback` (interface): interface AgentFallback — Where an agent goes when the thing it wanted is gone: a named option and a named place.
12
+ - `AgentFrame` (interface): interface AgentFrame — What one update produced: the action, the feeling, and where the day plan puts the agent.
13
+ - `AgentSnapshot` (interface): interface AgentSnapshot — Serializable whole agent: needs, feeling, held action, day-plan cursor and the far flag.
14
+ - `AgentTarget` (interface): interface AgentTarget — One reachable thing the agent could do this tick, as the game sees its own world.
15
+ - `AgentUpdate` (interface): interface AgentUpdate — One call per tick: the clock, and the targets the game's world currently offers.
16
+ - `createAgent` (function): function createAgent(config: AgentConfig): Agent — Create one agent. The same factory serves a park guest, a household being, a patrolling janitor and a soldier: `options` is what it can choose between, `schedule` is where its day puts it.
17
+
18
+ ## @jgengine/core/agents/mood
19
+
20
+ - `DEFAULT_MOOD_TIERS` (const): const DEFAULT_MOOD_TIERS: readonly MoodTier[] — Five bands a park guest, a household being or a soldier can share. A game overrides them when its own tone needs different words; the numeric bands are what HUD counters and tests read.
21
+ - `Mood` (interface): interface Mood — What the agent feels right now, on the caller's own tier labels.
22
+ - `MoodReader` (interface): interface MoodReader — A mood reader that keeps the game-raised biases, so a queue or a litter patch moves mood too.
23
+ - `MoodSnapshot` (interface): interface MoodSnapshot — Serializable mood state: the game-raised biases that sit on top of need satisfaction.
24
+ - `MoodTier` (interface): interface MoodTier — One labelled band of the 0-100 mood score.
25
+ - `MoodWeight` (interface): interface MoodWeight — One need's say in the mood score; weights are relative, so they need no fixed total.
26
+ - `createMoodReader` (function): function createMoodReader(config: { readonly needs: Pick<NeedSet, "ids" | "fraction">; readonly weights?: readonly MoodWeight[]; readonly tiers?: readonly MoodTier[]; }): MoodReader — Create the mood an agent carries. Needs are the agent's own (shared with its utility brain), and every bias is named data the game can remove again - there is no decaying hidden penalty.
27
+ - `moodTierOf` (function): function moodTierOf(score: number, tiers: readonly MoodTier[] = DEFAULT_MOOD_TIERS): MoodTier — The band a score reads as, from tiers sorted by `at` descending. Unknown or unsorted tiers fall back to the lowest declared band rather than throwing, because a HUD reads this every frame.
28
+ - `readMood` (function): function readMood(needs: Pick<NeedSet, "ids" | "fraction">, weights: readonly MoodWeight[], tiers: readonly MoodTier[] = DEFAULT_MOOD_TIERS, sources?: Readonly<Record<string, number>>): Mood — A mood read from a need set: each declared need contributes its `0..1` satisfaction times its weight, normalized by the total weight, so an agent with four needs and one with six read on the same 0-100 scale. Pure and allocation-light - the per-frame read a HUD and a thought bubble share.
29
+
30
+ ## @jgengine/core/agents/relationships
31
+
32
+ - `DEFAULT_RELATIONSHIP_MILESTONES` (const): const DEFAULT_RELATIONSHIP_MILESTONES: readonly RelationshipMilestone[] — The pair levels every game starts from; a game overrides the words and the cut points.
33
+ - `DEFAULT_SOCIAL_CONTACT` (const): const DEFAULT_SOCIAL_CONTACT: Required<SocialContact> — What a plain social action adds: a shared moment nudges both dials, never maxes either.
34
+ - `Relationship` (interface): interface Relationship — How one pair stands: how much time they have shared, and what they think of each other.
35
+ - `RelationshipBook` (interface): interface RelationshipBook — Everyone's view of everyone else: familiarity and opinion per pair, changed by social actions, with the pair levels that make a friendship a preference and a bond a refusal to split.
36
+ - `RelationshipBookSnapshot` (interface): interface RelationshipBookSnapshot — Serializable relationship state: every known pair plus the dials in play.
37
+ - `RelationshipMilestone` (interface): interface RelationshipMilestone — One labelled bond level on the opinion scale.
38
+ - `RelationshipPolicy` (interface): interface RelationshipPolicy — Tunables a game owns, every one reachable through {@link RelationshipBook.retune}.
39
+ - `SocialContact` (interface): interface SocialContact — Opinion and familiarity one interaction adds; a game passes its own for a hug, a row or a shift.
40
+ - `SocialOutcome` (interface): interface SocialOutcome — What one social action did to a pair.
41
+ - `createRelationshipBook` (function): function createRelationshipBook(policy: RelationshipPolicy = {}): RelationshipBook — Create the shared book of pair state. Keys are undirected and delimiter-safe through `createPairKeyCodec`, and milestone crossings come from `crossThresholds`, so a notice feed and a journal both read the same event.
42
+
43
+ ## @jgengine/core/agents/schedule
44
+
45
+ - `AgentSchedule` (interface): interface AgentSchedule — A day plan resolved against the world clock. No state to tick: a far agent reads the same day.
46
+ - `AgentScheduleConfig` (interface): interface AgentScheduleConfig — An agent's whole day as data: what it does, where, and when.
47
+ - `AgentSchedulePhase` (interface): interface AgentSchedulePhase — Where a schedule puts an agent at one instant.
48
+ - `AgentScheduleStop` (interface): interface AgentScheduleStop extends RoutineStopSpec — One authored stop in an agent's day. The shape is the routine owner's (`sim/routine`) plus a beat: the places a working stop walks in order, so a janitor on patrol is an agent with a route rather than a special case.
49
+ - `createAgentSchedule` (function): function createAgentSchedule(config: AgentScheduleConfig): AgentSchedule — Resolve an authored day plan into a clock-driven schedule. Timing, travel and arrival slips come from `sim/routine`; this adds the work beat and the refills a stop carries, so an off-hours stop is just another authored stop and a patrol is not a second scheduler.
50
+
5
51
  ## @jgengine/core/behaviour/behaviour
6
52
 
7
53
  - `Behaviour` (class): class Behaviour — Subclass and override the lifecycle hooks. A behaviour only joins the per-frame update dispatch if it actually overrides `onUpdate` (prototype-identity check at each activation), so hook-only behaviours cost nothing per frame.
@@ -2349,6 +2395,26 @@
2349
2395
  - `missionMarkers` (function): function missionMarkers(document: SceneDocumentLike): Record<string, ScenePoint3> — Marker positions from an authored scene, keyed by marker id — what a positional objective reads.
2350
2396
  - `registerMissionTriggerActions` (function): function registerMissionTriggerActions(): void — Register {@link MISSION_TRIGGER_ACTIONS} so the editor lists them. Idempotent; call at module load next to the game's catalogs.
2351
2397
 
2398
+ ## @jgengine/core/progression/careerBank
2399
+
2400
+ - `CareerBank` (interface): interface CareerBank — Persistent career money plus the run in progress, which a failed run loses.
2401
+ - `CareerBankConfig` (interface): interface CareerBankConfig — Configuration for {@link createCareerBank}.
2402
+ - `CareerBankState` (interface): interface CareerBankState — Everything a career bank stores, as plain JSON. `banked` is the persistent balance; `haul` is the run in progress and is the part a failed run loses. `deposited`, `lost`, `bestHaul` and `runs` are lifetime counters a results screen reads.
2403
+ - `createCareerBank` (function): function createCareerBank(config: CareerBankConfig): CareerBank — Money that means something between runs: a persistent balance plus a run's haul that is banked on a cash-out and lost on a failure. Amounts are snapped to the currency's minor units, so a save round-trip and a display never drift. Pair it with `economy/upgrades` for a career upgrade tree.
2404
+
2405
+ ## @jgengine/core/progression/gates
2406
+
2407
+ - `GateDef` (interface): interface GateDef — A milestone: content that opens exactly when its requirements hold and names the unlock it grants. `unlock` must resolve in the catalog the set is built with, or the set refuses to load.
2408
+ - `GateFacts` (interface): interface GateFacts — What a gate is evaluated against: standing by track and money by currency.
2409
+ - `GateMiss` (type): type GateMiss = "standing" | "balance" — Which named condition a closed gate still needs.
2410
+ - `GateRequirement` (interface): interface GateRequirement — Every condition a gate can require; all named conditions must hold.
2411
+ - `GateSet` (interface): interface GateSet — Evaluated gate content: the table, its status against facts, and the grants a caller applies.
2412
+ - `GateSetConfig` (interface): interface GateSetConfig — Content and the resolvers a {@link GateSet} is built from.
2413
+ - `GateStatus` (interface): interface GateStatus — One gate against live facts.
2414
+ - `assertGates` (function): function assertGates(config: GateSetConfig): void — Check an authored gate table against the content it grants; throws `RangeError` naming the gate.
2415
+ - `createGateSet` (function): function createGateSet(config: GateSetConfig): GateSet — Authored unlock gates: each one names the unlock it grants and the standing or balance it reads. Evaluated against caller-supplied facts, so one table serves a single-player save, a server actor or a probe. Load refuses a gate whose unlock id resolves to nothing (the D3.8 class), so a dead milestone fails at startup instead of in play. A gate with no requirements is open from the start.
2416
+ - `gateFacts` (function): function gateFacts(standing: Pick<Standing, "standing">, actorId: string, balances: Readonly<Record<string, number>> = {}): GateFacts — Read a {@link Standing} and a balance record as {@link GateFacts}.
2417
+
2352
2418
  ## @jgengine/core/progression/gearScore
2353
2419
 
2354
2420
  - `GearScore` (interface): interface GearScore — A scored loadout: the total, each slot's share, each stat's signed contribution, and how complete it is.
@@ -2372,6 +2438,18 @@
2372
2438
  - `assertSkillTreeConfig` (function): function assertSkillTreeConfig<TStat extends string>(config: SkillTreeConfig<TStat>): void — Check tree data before play; throws `RangeError` naming the offending node, requirement or synergy.
2373
2439
  - `createSkillTree` (function): function createSkillTree<TStat extends string = string>(config: SkillTreeConfig<TStat>, options: { devtoolsName?: string } = {}): SkillTree<TStat> — A data-authored skill tree: nodes carry a point cost, prerequisites, passives and unlocked actions, synergies pay off two or more nodes together, and `respec` refunds exactly the points spent. Spend against `canSpend` for a reason a UI can show; read `resolved()` for the stat modifiers and action list to feed `createStats` / `createAbilityKit`.
2374
2440
 
2441
+ ## @jgengine/core/progression/standing
2442
+
2443
+ - `Standing` (interface): interface Standing — Live standing over authored tracks, moved by named outcomes or direct adjustments, with the tier on each track and a serializable snapshot.
2444
+ - `StandingChange` (interface): interface StandingChange — One track's movement, with the tier on both sides so a caller can announce a crossing.
2445
+ - `StandingConfig` (interface): interface StandingConfig — Authored standing content: the tracks an actor stands with, and the outcomes that move them.
2446
+ - `StandingOutcomeDef` (interface): interface StandingOutcomeDef — One outcome a game can report, and the standing it moves on each track.
2447
+ - `StandingRequirement` (interface): interface StandingRequirement — A threshold on one track, as an unlock or an upgrade reads it.
2448
+ - `StandingState` (interface): interface StandingState — Plain JSON state of a {@link Standing}; tracks left at their authored `initial` are absent.
2449
+ - `StandingTrackDef` (interface): interface StandingTrackDef — One standing track: a faction, a district, a licence board — whatever an authored outcome moves.
2450
+ - `assertStandingConfig` (function): function assertStandingConfig(config: StandingConfig): void — Check authored standing content before use; throws `RangeError` naming the offending field.
2451
+ - `createStanding` (function): function createStanding(config: StandingConfig): Standing — Per-faction or per-district standing that rises and falls from named outcomes, over authored tracks. Numbers live in the shared reputation ledger (`faction/reputation`); this adds the authored outcome table, tier-per-track and the one snapshot a save stores. An outcome id or track id that resolves to nothing throws at the call, so authored content fails loudly.
2452
+
2375
2453
  ## @jgengine/core/progression/statGraph
2376
2454
 
2377
2455
  - `StatContribution` (interface): interface StatContribution — One folded contribution to a value, retained so a sheet can explain "why is this value 42?" — every step carries the source that produced it.
@@ -2389,6 +2467,17 @@
2389
2467
  - `createStatGraph` (function): function createStatGraph(def: StatGraphDef): StatGraph — A data-driven stat graph: game-owned named inputs feed caller-authored derived formulas, with contribution provenance, uncommitted previews, cycle detection, and selective recomputation. Formula semantics and numeric tables stay entirely game-defined.
2390
2468
  - `statModifierContributions` (function): function statModifierContributions<TStat extends string>(source: string, set: StatModifierSet<TStat>): Record<string, StatContribution[]> — Bridge the shared {@link StatModifierSet} shape (add/multiply, used by talents, items, and buffs) into stat-graph contributions, so a ranked talent tree or gear roll feeds the graph as one named source instead of a parallel store.
2391
2469
 
2470
+ ## @jgengine/core/progression/upgrades
2471
+
2472
+ - `UpgradeBuy` (type): type UpgradeBuy = { ok: true; nodeId: string; rank: number; cost: number } | { ok: false; reason: UpgradeRejection } — Result of {@link UpgradeTree.buy}: the rank reached and what it cost, or why it was refused.
2473
+ - `UpgradeNodeDef` (interface): interface UpgradeNodeDef — One upgrade node. Its price is the shared progression {@link Curve} evaluated at the rank being bought, so a game authors one curve and every rank of every node follows it.
2474
+ - `UpgradeRejection` (type): type UpgradeRejection = "unknown-node" | "max-rank" | "missing-prerequisites" | "standing" | "insufficient-funds" — Why a purchase was refused; the tree is left untouched on any rejection.
2475
+ - `UpgradeState` (interface): interface UpgradeState — Plain JSON state: node id → ranks owned. Nodes at rank `0` are absent.
2476
+ - `UpgradeTree` (interface): interface UpgradeTree — Authored upgrade nodes with a per-node cost curve, prerequisites, a standing gate and one snapshot.
2477
+ - `UpgradeTreeConfig` (interface): interface UpgradeTreeConfig — Authored nodes plus the standing a node's requirement reads.
2478
+ - `assertUpgrades` (function): function assertUpgrades(config: UpgradeTreeConfig): void — Check authored upgrade nodes; throws `RangeError` naming the offending node or field.
2479
+ - `createUpgradeTree` (function): function createUpgradeTree(config: UpgradeTreeConfig): UpgradeTree — An upgrade tree whose prices come from the shared curve primitive: each node authors one cost curve, each rank is that curve at the rank, and a purchase passes prerequisites, a standing gate and the caller's own payment step. State is one rank map that JSON round-trips, so a career save stores what a player paid for rather than the arithmetic behind it.
2480
+
2392
2481
  ## @jgengine/core/progression/xpCurve
2393
2482
 
2394
2483
  - `XpCurve` (interface): interface XpCurve — A resolved, validated {@link XpCurveDef}: thresholds, lifetime totals, a stated rule, and a leveling track bound to the same numbers.
@@ -16,6 +16,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
16
16
 
17
17
  - `createAffixRoller` (function) · `import { createAffixRoller } from "@jgengine/core/gameplay"`
18
18
 
19
+ ## agent-schedule — an authored per-agent day plan with a work beat and off-hours behaviour
20
+
21
+ - `createAgentSchedule` (function) · `import { createAgentSchedule } from "@jgengine/core/agents/schedule"`
22
+
23
+ ## agents — a needs, thought, action and mood loop an agent runs on the world clock
24
+
25
+ - `createAgent` (function) · `import { createAgent } from "@jgengine/core/agents/agent"`
26
+
19
27
  ## auction-book — timed bid auctions with escrowed bids, buyout, anti-snipe close extension, and collection-box settlement
20
28
 
21
29
  - `createAuctionBook` (function) · `import { createAuctionBook } from "@jgengine/core/gameplay"`
@@ -36,6 +44,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
36
44
 
37
45
  - `cancelJob` (function) · `import { cancelJob } from "@jgengine/core/gameplay"`
38
46
 
47
+ ## career-bank — persistent career money with a run haul that can be banked or lost
48
+
49
+ - `createCareerBank` (function) · `import { createCareerBank } from "@jgengine/core/progression/careerBank"`
50
+
39
51
  ## carry-encumbrance — gate a pickup against the carrier's remaining capacity
40
52
 
41
53
  - `canCarry` (function) · `import { canCarry } from "@jgengine/core/inventory/carryLoad"`
@@ -392,6 +404,12 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
392
404
 
393
405
  - `slotAccepts` (function) · `import { slotAccepts } from "@jgengine/core/gameplay"`
394
406
 
407
+ ## mood — a mood derived from how satisfied an agent's needs are
408
+
409
+ - `createMoodReader` (function) · `import { createMoodReader } from "@jgengine/core/agents/mood"`
410
+ - `moodTierOf` (function) · `import { moodTierOf } from "@jgengine/core/agents/mood"`
411
+ - `readMood` (function) · `import { readMood } from "@jgengine/core/agents/mood"`
412
+
395
413
  ## move — a keyed value toward a target by a bounded step
396
414
 
397
415
  - `towardValue` (function) · `import { towardValue } from "@jgengine/core/gameplay"`
@@ -537,6 +555,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
537
555
 
538
556
  - `attachRefusalNotices` (function) · `import { attachRefusalNotices } from "@jgengine/core/game/notices"`
539
557
 
558
+ ## relationships — familiarity and opinion between agents, with a bond that changes behaviour
559
+
560
+ - `createRelationshipBook` (function) · `import { createRelationshipBook } from "@jgengine/core/agents/relationships"`
561
+
540
562
  ## resolve — the current tier/band label for a keyed or scalar value
541
563
 
542
564
  - `tierAt` (function) · `import { tierAt } from "@jgengine/core/gameplay"`
@@ -651,6 +673,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
651
673
 
652
674
  - `stationOutputRate` (function) · `import { stationOutputRate } from "@jgengine/core/gameplay"`
653
675
 
676
+ ## standing — per-track standing moved by authored outcomes, with tiers and one snapshot
677
+
678
+ - `createStanding` (function) · `import { createStanding } from "@jgengine/core/progression/standing"`
679
+
654
680
  ## stat-graph — derive game-defined stats from named inputs with provenance and preview
655
681
 
656
682
  - `createStatGraph` (function) · `import { createStatGraph } from "@jgengine/core/gameplay"`
@@ -724,6 +750,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
724
750
 
725
751
  - `unitTrainingConfig` (function) · `import { unitTrainingConfig } from "@jgengine/core/gameplay"`
726
752
 
753
+ ## unlock-gates — milestones gated by standing or money, validated against the content they grant
754
+
755
+ - `createGateSet` (function) · `import { createGateSet } from "@jgengine/core/progression/gates"`
756
+
727
757
  ## unlock-points — spendable point pool earned per level and spent to unlock nodes, with refund and respec
728
758
 
729
759
  - `createUnlockPoints` (function) · `import { createUnlockPoints } from "@jgengine/core/economy/unlockPoints"`
@@ -732,6 +762,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
732
762
 
733
763
  - `createUnlockCatalog` (function) · `import { createUnlockCatalog } from "@jgengine/core/gameplay"`
734
764
 
765
+ ## upgrade-tree — ranked upgrades priced by a cost curve, gated by prerequisites and standing
766
+
767
+ - `createUpgradeTree` (function) · `import { createUpgradeTree } from "@jgengine/core/progression/upgrades"`
768
+
735
769
  ## visitor-population — a crowd whose on-site and daily counts are the bodies the game made
736
770
 
737
771
  - `createVisitorPopulation` (function) · `import { createVisitorPopulation } from "@jgengine/core/gameplay"`