@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.
- package/CHANGELOG.md +31 -0
- package/dist/agents/agent.d.ts +134 -0
- package/dist/agents/agent.js +224 -0
- package/dist/agents/mood.d.ts +74 -0
- package/dist/agents/mood.js +97 -0
- package/dist/agents/relationships.d.ts +92 -0
- package/dist/agents/relationships.js +132 -0
- package/dist/agents/schedule.d.ts +58 -0
- package/dist/agents/schedule.js +72 -0
- package/dist/ai/director/boss.d.ts +63 -0
- package/dist/ai/director/boss.js +87 -0
- package/dist/ai/director/encounter.d.ts +146 -0
- package/dist/ai/director/encounter.js +195 -0
- package/dist/ai/director/pressure.d.ts +58 -0
- package/dist/ai/director/pressure.js +70 -0
- package/dist/ai/director/roles.d.ts +61 -0
- package/dist/ai/director/roles.js +118 -0
- package/dist/ai/director/sound.d.ts +79 -0
- package/dist/ai/director/sound.js +113 -0
- package/dist/game/playableGame.d.ts +2 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +30 -1
- package/dist/mission/mission.d.ts +1 -1
- package/dist/mission/mission.js +4 -1
- package/dist/progression/careerBank.d.ts +63 -0
- package/dist/progression/careerBank.js +83 -0
- package/dist/progression/gates.d.ts +66 -0
- package/dist/progression/gates.js +88 -0
- package/dist/progression/standing.d.ts +77 -0
- package/dist/progression/standing.js +151 -0
- package/dist/progression/upgrades.d.ts +70 -0
- package/dist/progression/upgrades.js +128 -0
- package/dist/ui/accessibility.d.ts +8 -0
- package/dist/ui/accessibility.js +14 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-gameplay/SKILL.md +2 -0
- package/skills/jgengine-gameplay/api.md +89 -0
- package/skills/jgengine-gameplay/capabilities.md +34 -0
- package/skills/jgengine-gameplay/recipes/agent-lives.md +76 -0
- package/skills/jgengine-world/api.md +52 -0
- package/skills/jgengine-world/capabilities.md +32 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Agent lives: needs, mood, relationships, schedules
|
|
2
|
+
|
|
3
|
+
One system for every resident that wants something. `createAgent` (`@jgengine/core/agents/agent`)
|
|
4
|
+
composes four owners that already exist: `createNeedSet` (`sim/needs`) decays the needs,
|
|
5
|
+
`createUtilityBrain` (`ai/utilityBrain`) scores what to do, `resolveRoutine` (`sim/routine`) says
|
|
6
|
+
where the day plan puts the agent, and `createMoodReader` (`agents/mood`) reads how satisfied it is.
|
|
7
|
+
A game supplies the targets its world offers and owns the body, the nav and the art.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createAgent } from "@jgengine/core/agents/agent";
|
|
11
|
+
import { createRelationshipBook } from "@jgengine/core/agents/relationships";
|
|
12
|
+
|
|
13
|
+
const needs = [
|
|
14
|
+
{ id: "hunger", max: 100, rate: 0.9 },
|
|
15
|
+
{ id: "fun", max: 100, rate: 0.6 },
|
|
16
|
+
{ id: "energy", max: 100, rate: 0.5 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const agent = createAgent({
|
|
20
|
+
id: "guest:1",
|
|
21
|
+
needs,
|
|
22
|
+
options: [
|
|
23
|
+
{ id: "eat", terms: [{ need: "hunger", curve: { kind: "linear", from: 0.2, to: 0.95 } }] },
|
|
24
|
+
{ id: "ride", terms: [{ need: "fun", curve: { kind: "linear", from: 0.15, to: 0.9 } }] },
|
|
25
|
+
{ id: "seat", base: 0.05, terms: [{ need: "energy", curve: { kind: "linear", from: 0.1, to: 1 } }] },
|
|
26
|
+
{ id: "work", base: 0.2 },
|
|
27
|
+
],
|
|
28
|
+
schedule: {
|
|
29
|
+
cycleSeconds: 86_400,
|
|
30
|
+
stops: [
|
|
31
|
+
{ place: "home", arriveAt: 0, activity: "sleep", refill: { energy: 70 } },
|
|
32
|
+
{ place: "lobby", arriveAt: 30_000, activity: "work", beat: ["hall-a", "hall-b", "hall-c"] },
|
|
33
|
+
{ place: "cafe", arriveAt: 50_000, activity: "meal", refill: { hunger: 60 } },
|
|
34
|
+
],
|
|
35
|
+
travelSeconds: (from, to) => distances.get(`${from}>${to}`) ?? 60,
|
|
36
|
+
},
|
|
37
|
+
mood: { weights: [{ need: "hunger", weight: 2 }, { need: "fun", weight: 1 }] },
|
|
38
|
+
rng: ctx.rng,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
agent.update({
|
|
42
|
+
dt,
|
|
43
|
+
now: ctx.time.now(),
|
|
44
|
+
targets: [
|
|
45
|
+
{ option: "ride", target: "coaster", cost: queueSeconds, fallback: { option: "seat", target: "bench" } },
|
|
46
|
+
{ option: "eat", target: "burger-stall", cost: walkSeconds },
|
|
47
|
+
{ option: "seat", target: "bench" },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Read the frame, not the internals: `frame.action` (`option`, `target`, `usedFallback`), `frame.mood`
|
|
53
|
+
(`score`, `tier`, `label`), `frame.phase` and `frame.post` (where the schedule and the work beat put
|
|
54
|
+
the agent), and `frame.changed` with `frame.reason`. `targets` is the world as it is this tick, so a
|
|
55
|
+
sold-out stall simply is not in the list, and a pick whose target disappears moves to its declared
|
|
56
|
+
`fallback` instead of freezing.
|
|
57
|
+
|
|
58
|
+
- **Saved state.** `agent.snapshot()` carries needs (values, rates, modifiers), mood sources, the held
|
|
59
|
+
pick and its dials, the action, the far flag and the schedule stop the agent last ate at. The day
|
|
60
|
+
plan itself is authored data resolved from the clock, so it is never saved. Put every agent's
|
|
61
|
+
snapshot and one `book.snapshot()` in the save document.
|
|
62
|
+
- **Far agents.** `agent.setFar(true)` stops the utility pass: the agent keeps decaying and follows
|
|
63
|
+
its schedule, and its action reads off the phase. A far agent costs no decision and no nav.
|
|
64
|
+
- **Mood from the world.** `agent.moodSources.setSource("queue", -20)` and `clearSource` let a queue,
|
|
65
|
+
litter, a broken ride or a cold snap move the feeling without giving mood its own state machine.
|
|
66
|
+
- **One relationship book per world.** `book.socialise(a, b, { opinion: 6, familiarity: 0.1 })` returns
|
|
67
|
+
the pair before and after plus the milestones crossed. `book.prefersCompanyOf(a, b)` is a friendship
|
|
68
|
+
a game can act on by seating them together; `book.refusesToSplit(a, b)` is a bond that must not be
|
|
69
|
+
posted to separate shifts. A bond needs opinion *and* time shared, so one good afternoon is not
|
|
70
|
+
enough.
|
|
71
|
+
- **A work beat is not a special case.** A stop with `beat` walks those places in order for
|
|
72
|
+
`beatDwellSeconds` each; a janitor, a security guard and a tour guide are the same agent with
|
|
73
|
+
different authored beats. Off-hours behaviour is a stop without a beat.
|
|
74
|
+
- **Determinism.** Pass `ctx.rng` and set `jitter`; two agents built from the same seeded stream make
|
|
75
|
+
the same choices. The game owns the RNG cursor in its save (`createGameSave` `rng`), since one
|
|
76
|
+
stream is usually shared by the whole world.
|
|
@@ -46,6 +46,58 @@
|
|
|
46
46
|
- `planLookahead` (function): function planLookahead<S, M>(root: S, domain: LookaheadDomain<S, M>, profile: DifficultyProfile, rng: () => number): M | null — Pick the agent's next move by bounded lookahead: a depth-limited negamax (or single-perspective sequence search when `adversarial: false`) over the caller's {@link LookaheadDomain}, exploring at most `profile.planWidth` moves per ply to `profile.planDepth` plies, with the final root choice run through {@link pickScored} so `decisionNoise` still injects tier-appropriate blunders. Depth `1` is greedy — an easy profile literally cannot see the fork coming; an expert profile finds multi-ply tactics. Work is bounded by `planWidth ** planDepth` domain calls; run it at decision cadence (turns, ability windows, order re-evaluation), not per frame.
|
|
47
47
|
- `shouldUseAbility` (function): function shouldUseAbility(opportunity: number, profile: DifficultyProfile, rng: () => number): boolean — Should the agent spend an ability now? `opportunity` is the caller-scored quality of this moment in `0..1` (targets clumped for the AoE, player mid-animation, burn phase). The perceived opportunity is fuzzed by `decisionNoise` and compared against `abilityDiscipline`: an easy profile (low discipline, high noise) wastes cooldowns on weak moments and sometimes sits on a perfect window; an expert profile fires exactly on strong windows. One rng draw when noisy.
|
|
48
48
|
|
|
49
|
+
## @jgengine/core/ai/director/boss
|
|
50
|
+
|
|
51
|
+
- `BossBehaviour` (type): type BossBehaviour = Omit<SquadRole, "id" | "count"> — The role shape a boss fights with in one phase: the same dials a squad role uses.
|
|
52
|
+
- `BossPhase` (interface): interface BossPhase — One authored phase: the health it starts at, how the boss fights, and its tell.
|
|
53
|
+
- `BossState` (interface): interface BossState — Serializable boss state: which phase holds, how long it has held, and the tell still on screen.
|
|
54
|
+
- `BossStep` (interface): interface BossStep — One step of the boss clock.
|
|
55
|
+
- `BossTell` (interface): interface BossTell — The warning a boss gives while it changes phase, and how long the player has to read it.
|
|
56
|
+
- `advanceBoss` (function): function advanceBoss(phases: readonly BossPhase[], state: BossState, health: number, dt: number): BossStep — Advance the boss by `dt` at a health fraction and return the phase in effect.
|
|
57
|
+
- `bossPhase` (function): function bossPhase(phases: readonly BossPhase[], health: number): BossPhase — The phase a boss fights with at a health fraction. Pure read for a HUD; never mutates.
|
|
58
|
+
- `createBossState` (function): function createBossState(phases: readonly BossPhase[]): BossState — Fresh boss state for a phase list, opening in phase 0 with no tell showing.
|
|
59
|
+
|
|
60
|
+
## @jgengine/core/ai/director/encounter
|
|
61
|
+
|
|
62
|
+
- `Encounter` (interface): interface Encounter — Live encounter: opens waves, emits timed spawn orders, and round-trips through a save.
|
|
63
|
+
- `EncounterConfig` (interface): interface EncounterConfig — Construction options for {@link createEncounter}.
|
|
64
|
+
- `EncounterDefinition` (interface): interface EncounterDefinition — A whole encounter, authored as data instead of spawned in game code.
|
|
65
|
+
- `EncounterGroup` (interface): interface EncounterGroup — One authored group inside a wave: what arrives, how many, where from, and when. `unit` and `entry` are game-owned ids — the engine never resolves them.
|
|
66
|
+
- `EncounterOrder` (interface): interface EncounterOrder — One unit the director says should enter now.
|
|
67
|
+
- `EncounterPressure` (interface): interface EncounterPressure — The subset of a pressure plan the encounter layer applies.
|
|
68
|
+
- `EncounterState` (interface): interface EncounterState — Serializable encounter state.
|
|
69
|
+
- `EncounterView` (interface): interface EncounterView — Pooled per-frame readout of where the encounter stands.
|
|
70
|
+
- `EncounterWave` (interface): interface EncounterWave — One authored wave.
|
|
71
|
+
- `createEncounter` (function): function createEncounter(config: EncounterConfig): Encounter — Create a live encounter over an authored definition: open a wave, step its clock, take the orders whose time has come, and close the wave when the game says it is cleared.
|
|
72
|
+
- `expandWave` (function): function expandWave(config: EncounterConfig, waveIndex: number, pressure: EncounterPressure = {}): EncounterOrder[] — Expand one authored wave into the units it sends, at the times it sends them.
|
|
73
|
+
|
|
74
|
+
## @jgengine/core/ai/director/pressure
|
|
75
|
+
|
|
76
|
+
- `PressurePlan` (interface): interface PressurePlan — One bounded answer to "how hard now", plus the effects the encounter layer applies.
|
|
77
|
+
- `PressureReading` (interface): interface PressureReading — Encounter pressure: how hard a director leans on a player who is struggling, read from the player's own state, quantized to a tier, and bounded by the rule's cap. The plan is a value, not a callback, so a test can sweep it and a probe can print it.
|
|
78
|
+
- `PressureRule` (interface): interface PressureRule — The weights and window that turn a reading into an index, plus the hard cap.
|
|
79
|
+
- `evaluatePressure` (function): function evaluatePressure(reading: PressureReading, rule: PressureRule = {}): PressurePlan — Turn one reading into a bounded pressure plan.
|
|
80
|
+
|
|
81
|
+
## @jgengine/core/ai/director/roles
|
|
82
|
+
|
|
83
|
+
- `FightIntent` (type): type FightIntent = "retreat" | "hold" | "flank" | "cover" | "assault" — The five duties a fighter can be given in a fight. A duty is *who does what*; the squad role a game dials (range, angles, fire discipline) is *how that duty fights* and stays content.
|
|
84
|
+
- `FightOrders` (interface): interface FightOrders — What the director decided, per fighter, and why.
|
|
85
|
+
- `FightSituation` (interface): interface FightSituation — The squad as it stands and the enemy it is facing, or no enemy at all.
|
|
86
|
+
- `FighterState` (interface): interface FighterState — One fighter the director can order, with the state the decision reads.
|
|
87
|
+
- `assignFightIntents` (function): function assignFightIntents(situation: FightSituation): FightOrders — Assign each fighter a duty from the squad's own state — who is hurt, who is under fire, and who already stands on the outside of the formation — never from an authored order or a spawn index.
|
|
88
|
+
- `compositionForIntents` (function): function compositionForIntents(order: readonly string[], intents: ReadonlyMap<string, FightIntent>, roleFor: (intent: FightIntent) => SquadRole): SquadComposition — Turn duties into a squad composition `createSquadCombat` accepts: one slot per fighter, in the order the game gives its members, each mapped to the role that game dials for the duty. Build it through `composition` so squad combat executes the director's assignment instead of its own static list.
|
|
89
|
+
|
|
90
|
+
## @jgengine/core/ai/director/sound
|
|
91
|
+
|
|
92
|
+
- `Investigation` (interface): interface Investigation — A hunter still working a noise, as the squad layer reads it this tick.
|
|
93
|
+
- `NoiseEvent` (interface): interface NoiseEvent — One noise in the world, as the game produces it.
|
|
94
|
+
- `SoundHunt` (interface): interface SoundHunt — Sound hunting: raise a noise, get the bounded set of enemies that go and look.
|
|
95
|
+
- `SoundHuntConfig` (interface): interface SoundHuntConfig — Everything the hunt reads and its bounds.
|
|
96
|
+
- `SoundHuntEntry` (interface): interface SoundHuntEntry — One noise and the hunters sent to it. Serializable, so a hunt survives a save.
|
|
97
|
+
- `SoundHuntSnapshot` (interface): interface SoundHuntSnapshot — Serializable book-keeping: which noises are still being worked, and by whom.
|
|
98
|
+
- `SoundHunter` (interface): interface SoundHunter — A unit that could hear a noise: its id and where it stands when the noise lands.
|
|
99
|
+
- `createSoundHunt` (function): function createSoundHunt(config: SoundHuntConfig): SoundHunt — Make enemies investigate a noise, reusing the hearing owner.
|
|
100
|
+
|
|
49
101
|
## @jgengine/core/ai/driver
|
|
50
102
|
|
|
51
103
|
- `DrivePathPoint` (type): type DrivePathPoint = readonly [number, number] — A 2D polyline point (`[x, z]`), structurally compatible with authored road path centerlines.
|
|
@@ -192,6 +192,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
192
192
|
|
|
193
193
|
- `createBodyBind` (function) · `import { createBodyBind } from "@jgengine/core/world"`
|
|
194
194
|
|
|
195
|
+
## boss-phase-state — fresh serializable boss phase state that opens in the first phase
|
|
196
|
+
|
|
197
|
+
- `createBossState` (function) · `import { createBossState } from "@jgengine/core/ai/director/boss"`
|
|
198
|
+
|
|
199
|
+
## boss-phases — data-authored boss phases that change behaviour and fire a tell at health thresholds
|
|
200
|
+
|
|
201
|
+
- `advanceBoss` (function) · `import { advanceBoss } from "@jgengine/core/ai/director/boss"`
|
|
202
|
+
|
|
195
203
|
## bounded-grid-route — distinguish no path from exhausted grid search work
|
|
196
204
|
|
|
197
205
|
- `findPathResult` (function) · `import { findPathResult } from "@jgengine/core/nav/navGrid"`
|
|
@@ -334,6 +342,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
334
342
|
|
|
335
343
|
- `summarizeElevation` (function) · `import { summarizeElevation } from "@jgengine/core/world"`
|
|
336
344
|
|
|
345
|
+
## encounter-expand — turn an authored wave into a deterministic, pressure-adjusted list of timed spawn orders
|
|
346
|
+
|
|
347
|
+
- `expandWave` (function) · `import { expandWave } from "@jgengine/core/ai/director/encounter"`
|
|
348
|
+
|
|
349
|
+
## encounter-pressure — bounded difficulty tier read from player health, encounter progress and time
|
|
350
|
+
|
|
351
|
+
- `evaluatePressure` (function) · `import { evaluatePressure } from "@jgengine/core/ai/director/pressure"`
|
|
352
|
+
|
|
353
|
+
## encounter-runner — open authored waves, emit their timed orders, and track wave progress through a save
|
|
354
|
+
|
|
355
|
+
- `createEncounter` (function) · `import { createEncounter } from "@jgengine/core/ai/director/encounter"`
|
|
356
|
+
|
|
337
357
|
## engine-layers — layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
|
|
338
358
|
|
|
339
359
|
- `createEngineLayers` (function) · `import { createEngineLayers } from "@jgengine/core/audio/engineLayers"`
|
|
@@ -354,6 +374,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
354
374
|
|
|
355
375
|
- `createFeedbackMixer` (function) · `import { createFeedbackMixer } from "@jgengine/core/vfx/feedbackMixer"`
|
|
356
376
|
|
|
377
|
+
## fight-intent-composition — map assigned duties onto a squad combat composition in member order
|
|
378
|
+
|
|
379
|
+
- `compositionForIntents` (function) · `import { compositionForIntents } from "@jgengine/core/ai/director/roles"`
|
|
380
|
+
|
|
381
|
+
## fight-intents — assign hold, flank, cover, assault and retreat duties from live squad state
|
|
382
|
+
|
|
383
|
+
- `assignFightIntents` (function) · `import { assignFightIntents } from "@jgengine/core/ai/director/roles"`
|
|
384
|
+
|
|
357
385
|
## find-effect — look up one named particle effect in the shared library, or undefined when unknown
|
|
358
386
|
|
|
359
387
|
- `findEffect` (function) · `import { findEffect } from "@jgengine/core/vfx/effectLibrary"`
|
|
@@ -812,6 +840,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
812
840
|
|
|
813
841
|
- `planSolidRoute` (function) · `import { planSolidRoute } from "@jgengine/core/nav/solidRoute"`
|
|
814
842
|
|
|
843
|
+
## sound-hunting — send a bounded set of nearby enemies to investigate a noise with decaying memory
|
|
844
|
+
|
|
845
|
+
- `createSoundHunt` (function) · `import { createSoundHunt } from "@jgengine/core/ai/director/sound"`
|
|
846
|
+
|
|
815
847
|
## spatial-incremental — Update one entity's spatial cell after a committed write without scanning the population
|
|
816
848
|
|
|
817
849
|
- `createSpatialApi` (function) · `import { createSpatialApi } from "@jgengine/core/scene/spatial"`
|