@jgengine/core 0.35.0 → 0.36.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 +32 -0
- package/dist/agents/system.d.ts +135 -0
- package/dist/agents/system.js +195 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +20 -1
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-gameplay/api.md +29 -0
- package/skills/jgengine-gameplay/capabilities.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,38 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.36.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.36.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.33.0`; `@jgengine/github` is `0.23.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createAgentSystem` (`@jgengine/core/agents/system`) takes a game's needs, options, mood weights, members and
|
|
38
|
+
schedules once and returns a running cast: one `tick(dt)` for everyone, one target selector for the whole system,
|
|
39
|
+
a fallback declared once, and a plain-JSON `save()`/`load()` that rides the game's own save.
|
|
40
|
+
- `--quick` on `bun run shoot` and `bun run drive` is the 960x540 iteration preset: one settled frame (a 2s settle window in drive), no video, no perf pass, written as `<name>-quick.png` and timed on its own line. It refuses a 1600x900 pixel claim, a frame-time claim, a non-desktop device and a recording flag by name instead of producing weaker evidence; full stays the default for PR shots. On a warm daemon a two-shot `drive` of ember-command went 9.3s to 8.2s and 4316KB to 1383KB of PNG.
|
|
41
|
+
- `createPlayScript` / `runPlayScript` (`@jgengine/shell/play`) play a real `PlayableGame` headlessly from a tick-based script: `move`, `aim`, `fire`, `use`, `wait` and `command` steps plus `setup`/`onTick` hooks, `expect` assertions read after setup and on their own tick, a tick timeline, and an FNV digest for replay comparison. It replays the shell's own frame pipeline — held movement through the walk controller, a bound action resolving to its command on press and repeat, interact prompts, hotbar — so a script proves the same code path a player's hands do. No browser, no wall clock, fixed `dt`.
|
|
42
|
+
- `bun run simulate <game> <script>` runs a committed `<game>/scripts/*.play.ts` against a game under `./Games`, prints the timeline, names a failed assertion and exits 1. It merges the game's own tsconfig `paths` with this checkout's source packages, so the game imports and the engine under test are one module graph.
|
|
43
|
+
- `HeadlessRunner.aim()` steers the walk controller mid-run; `HeadlessRunnerOptions` gains `seed` (deterministic `ctx.rng`) and `collision` (the shell's voxel config, previously dropped on the headless path).
|
|
44
|
+
|
|
45
|
+
Play scripts prove play in seconds instead of minutes of browser play, and the three proofs land beside their games: Scrap Signal (an enemy takes damage and closes in), Beacon Bastion (a composed wave is reached, its bounty scales, a boss phase flips) and Harbor Heat (heat is witness-gated, the search area is set, a courier is swept).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- The graphics quality tier now costs what it claims on the render path. A tier's post chain renders at its own resolution scale (`low` 0.7, `medium` 0.85, `high`/`ultra` native) instead of always at the canvas size, and a game's authored `shadowMapSize` and `cascades` are a ceiling the tier caps rather than a value it ignored — every reference game authored 3 cascades and 1024/2048, so no tier changed a shadow before this. `low` and `medium` also stretch the far cascades' refresh interval, and the smallest caster worth a depth draw grows as the map shrinks. `high` and `ultra` keep native resolution, the authored shadow sizes and the measured cadence, so their frames are unchanged.
|
|
50
|
+
- Measured uncapped at 1600x900: whole-frame draws at `low` fell 53% in harbor-heat (515 → 242) and 63% in drift-foundry (1,098 → 408), with the shadow pass there down from 911 ms to 344 ms over a 6 s window; `high` and `ultra` held their draw counts.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Terrain build no longer scans every road and path segment for every vertex. A baked path corridor, a draped road and a painted polyline region each answer "how far is this point?" from a precomputed segment grid, so Scrap Signal's world build drops from 16.6 s to 0.51 s on the same machine and its longest boot stall from 11.6–18.4 s to 1.2 s. Sampled heights and palettes are bit-identical to the previous full scan, and `withPathProfiles` still composes profiles in order.
|
|
55
|
+
- Load-time shader warmup compiles against the render state the first frame actually draws with. A scene whose post chain renders into its own buffer was linking two programs per material, one linear untone-mapped set the frame draws and one canvas set nothing ever drew; the cover no longer waits for the second set. Harbor Heat links 84 programs to 58 and Scrap Signal 124 to 83 before the cover lifts, with no visual change.
|
|
56
|
+
|
|
57
|
+
### Performance
|
|
58
|
+
|
|
59
|
+
- `createTerrainPaletteSampler` (`@jgengine/core/world/terrain`) tests only the material regions whose painted core or blend ring covers a sample, so a world with hundreds of authored regions no longer pays for all of them at every vertex.
|
|
60
|
+
|
|
29
61
|
## 0.35.0
|
|
30
62
|
|
|
31
63
|
### Migrate
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { UtilityInterrupt, UtilityOption, UtilityTunables } from "../ai/utilityBrain.js";
|
|
2
|
+
import type { NeedDefinition } from "../sim/needs.js";
|
|
3
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
4
|
+
import { type Agent, type AgentFallback, type AgentFrame, type AgentSnapshot, type AgentTarget } from "./agent.js";
|
|
5
|
+
import type { Mood, MoodTier, MoodWeight } from "./mood.js";
|
|
6
|
+
import type { AgentScheduleConfig } from "./schedule.js";
|
|
7
|
+
export type { Agent, AgentAction, AgentFallback, AgentFrame, AgentSnapshot, AgentTarget } from "./agent.js";
|
|
8
|
+
export type { AgentScheduleConfig, AgentScheduleStop, AgentSchedulePhase } from "./schedule.js";
|
|
9
|
+
export type { Mood, MoodTier, MoodWeight } from "./mood.js";
|
|
10
|
+
export type { NeedDefinition } from "../sim/needs.js";
|
|
11
|
+
export type { UtilityInterrupt, UtilityOption, UtilityTunables } from "../ai/utilityBrain.js";
|
|
12
|
+
/** The mood policy a member runs on: which needs matter, and the game's own words for the bands. */
|
|
13
|
+
export interface AgentMoodConfig {
|
|
14
|
+
readonly weights?: readonly MoodWeight[];
|
|
15
|
+
readonly tiers?: readonly MoodTier[];
|
|
16
|
+
}
|
|
17
|
+
/** What a selector is told about the member asking: who it is, its live agent, and where it stands. */
|
|
18
|
+
export interface AgentSelectView {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** The live agent, for a selector that reads needs, mood or the held action. */
|
|
21
|
+
readonly agent: Agent;
|
|
22
|
+
/** Body position from the system's `position` reader, or `null` when the game has no body for it. */
|
|
23
|
+
readonly at: Vec3 | null;
|
|
24
|
+
readonly now: number;
|
|
25
|
+
/** True while the member is off camera and takes no utility pass this tick. */
|
|
26
|
+
readonly far: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Everything the member could do right now, as the game's world offers it. Declared once per system. */
|
|
29
|
+
export type AgentSelector = (view: AgentSelectView) => readonly AgentTarget[];
|
|
30
|
+
/** Where a system keeps its save data: a key-value store, a save slot, or a test's map. */
|
|
31
|
+
export interface AgentSystemStorage {
|
|
32
|
+
read(): AgentSystemState | undefined;
|
|
33
|
+
write(next: AgentSystemState): void;
|
|
34
|
+
}
|
|
35
|
+
/** One member's saved state: its agent snapshot plus the off-camera flag the snapshot does not carry. */
|
|
36
|
+
export interface AgentMemberState {
|
|
37
|
+
readonly agent: AgentSnapshot;
|
|
38
|
+
readonly far: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The whole system as plain JSON - drop it in a save, read it back with {@link AgentSystem.load}. */
|
|
41
|
+
export interface AgentSystemState {
|
|
42
|
+
readonly version: 1;
|
|
43
|
+
readonly members: Readonly<Record<string, AgentMemberState>>;
|
|
44
|
+
}
|
|
45
|
+
/** One member: an id, and only what differs from the system's defaults. */
|
|
46
|
+
export interface AgentMemberSpec extends UtilityTunables {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
/** Needs for this member; defaults to the system's. */
|
|
49
|
+
readonly needs?: readonly NeedDefinition[];
|
|
50
|
+
/** Scored actions for this member; defaults to the system's. */
|
|
51
|
+
readonly options?: readonly UtilityOption[];
|
|
52
|
+
readonly interrupts?: readonly UtilityInterrupt[];
|
|
53
|
+
/** The member's authored day; omit for one that only reacts to targets. */
|
|
54
|
+
readonly schedule?: AgentScheduleConfig;
|
|
55
|
+
readonly mood?: AgentMoodConfig;
|
|
56
|
+
/** Targets for this member instead of the system's selector. */
|
|
57
|
+
readonly select?: AgentSelector;
|
|
58
|
+
/** `[0,1)` source; defaults to the system's `rng`. */
|
|
59
|
+
readonly rng?: () => number;
|
|
60
|
+
readonly devtoolsName?: string;
|
|
61
|
+
/** Start off camera: schedule and needs only, no utility pass, until `setFar(id, false)`. */
|
|
62
|
+
readonly far?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** The game's data, in one place: what its people need, what they can do, and how they feel. */
|
|
65
|
+
export interface AgentSystemConfig {
|
|
66
|
+
/** Needs every member carries unless it declares its own. */
|
|
67
|
+
readonly needs: readonly NeedDefinition[];
|
|
68
|
+
/** Scored actions every member chooses between unless it declares its own. */
|
|
69
|
+
readonly options: readonly UtilityOption[];
|
|
70
|
+
readonly interrupts?: readonly UtilityInterrupt[];
|
|
71
|
+
readonly mood?: AgentMoodConfig;
|
|
72
|
+
/** Members present at creation; more join later with {@link AgentSystem.add}. */
|
|
73
|
+
readonly members?: readonly AgentMemberSpec[];
|
|
74
|
+
/** Resolve every member's targets. Called once per on-camera member per tick, never per option. */
|
|
75
|
+
readonly select?: AgentSelector;
|
|
76
|
+
/** Fallback given to every target that declares none - one fallback per system, not per target. */
|
|
77
|
+
readonly fallback?: AgentFallback;
|
|
78
|
+
/** The world clock, so `tick(dt)` needs no `now` argument. */
|
|
79
|
+
readonly clock?: {
|
|
80
|
+
now(): number;
|
|
81
|
+
};
|
|
82
|
+
/** Where a member's body is, handed to the selector as `at`. */
|
|
83
|
+
readonly position?: (id: string) => Vec3 | null;
|
|
84
|
+
/** Save storage, hydrated on the first tick and written by {@link AgentSystem.save}. */
|
|
85
|
+
readonly storage?: AgentSystemStorage;
|
|
86
|
+
/** Deterministic source shared by members that do not declare their own. */
|
|
87
|
+
readonly rng?: () => number;
|
|
88
|
+
/** Dials every member starts with; a member may override any of them. */
|
|
89
|
+
readonly defaults?: UtilityTunables;
|
|
90
|
+
/** Devtools prefix: member `x` registers as `<devtoolsName>.x`. */
|
|
91
|
+
readonly devtoolsName?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A whole cast of agents behind one factory and one tick.
|
|
95
|
+
*
|
|
96
|
+
* Feed it a game's data once - needs, options, mood weights, the members and their schedules - and it
|
|
97
|
+
* owns the parts a game should not write: the roster, lazy creation, one tick for everyone, where a
|
|
98
|
+
* target came from, and a save blob that is plain JSON. Everything an individual {@link Agent} can do
|
|
99
|
+
* stays reachable through {@link get}.
|
|
100
|
+
*/
|
|
101
|
+
export interface AgentSystem {
|
|
102
|
+
/** Create and register one member; a member saved under this id resumes instead of starting fresh. */
|
|
103
|
+
add(spec: AgentMemberSpec): Agent;
|
|
104
|
+
get(id: string): Agent | null;
|
|
105
|
+
has(id: string): boolean;
|
|
106
|
+
remove(id: string): void;
|
|
107
|
+
/** Member ids in creation order. */
|
|
108
|
+
ids(): readonly string[];
|
|
109
|
+
size(): number;
|
|
110
|
+
/** One call per game tick: decay, day plan and decision for every member, near or far. */
|
|
111
|
+
tick(dt: number, now?: number): void;
|
|
112
|
+
/** What the last tick produced for a member, or `null` before it has ticked. */
|
|
113
|
+
frame(id: string): AgentFrame | null;
|
|
114
|
+
/** A member's mood, readable before its first tick. */
|
|
115
|
+
mood(id: string): Mood | null;
|
|
116
|
+
/** Mean mood across members, or `null` for an empty system. */
|
|
117
|
+
moodMean(): number | null;
|
|
118
|
+
/** Action of a member, `null` before its first tick. */
|
|
119
|
+
action(id: string): AgentFrame["action"] | null;
|
|
120
|
+
setFar(id: string, far: boolean): void;
|
|
121
|
+
isFar(id: string): boolean;
|
|
122
|
+
/** Plain-JSON state, also written to `storage` when one was given. */
|
|
123
|
+
save(): AgentSystemState;
|
|
124
|
+
/** Restore members saved under ids this system knows, and hold the rest until they join. */
|
|
125
|
+
load(next: AgentSystemState | undefined): void;
|
|
126
|
+
dispose(): void;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One call for a whole cast: a game hands over its needs, options, mood weights, schedules and one
|
|
130
|
+
* target selector, and gets back a running system with a single `tick(dt)` and a save blob that goes
|
|
131
|
+
* into the game's own save.
|
|
132
|
+
*
|
|
133
|
+
* @capability agents a needs, thought, action and mood loop an agent runs on the world clock
|
|
134
|
+
*/
|
|
135
|
+
export declare function createAgentSystem(config: AgentSystemConfig): AgentSystem;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createAgent, } from "./agent.js";
|
|
2
|
+
/**
|
|
3
|
+
* One call for a whole cast: a game hands over its needs, options, mood weights, schedules and one
|
|
4
|
+
* target selector, and gets back a running system with a single `tick(dt)` and a save blob that goes
|
|
5
|
+
* into the game's own save.
|
|
6
|
+
*
|
|
7
|
+
* @capability agents a needs, thought, action and mood loop an agent runs on the world clock
|
|
8
|
+
*/
|
|
9
|
+
export function createAgentSystem(config) {
|
|
10
|
+
const agents = new Map();
|
|
11
|
+
const frames = new Map();
|
|
12
|
+
const selectors = new Map();
|
|
13
|
+
const saved = new Map();
|
|
14
|
+
const order = [];
|
|
15
|
+
let hydrated = false;
|
|
16
|
+
let lastNow = 0;
|
|
17
|
+
function selectFor(id) {
|
|
18
|
+
return selectors.get(id) ?? config.select;
|
|
19
|
+
}
|
|
20
|
+
function restoreInto(agent, state) {
|
|
21
|
+
agent.restore(state.agent);
|
|
22
|
+
if (state.far)
|
|
23
|
+
agent.setFar(true);
|
|
24
|
+
}
|
|
25
|
+
function hydrate() {
|
|
26
|
+
if (hydrated)
|
|
27
|
+
return;
|
|
28
|
+
const state = config.storage?.read();
|
|
29
|
+
if (state === undefined)
|
|
30
|
+
return;
|
|
31
|
+
hydrated = true;
|
|
32
|
+
loadState(state);
|
|
33
|
+
}
|
|
34
|
+
function loadState(next) {
|
|
35
|
+
if (next === undefined)
|
|
36
|
+
return;
|
|
37
|
+
for (const [id, state] of Object.entries(next.members)) {
|
|
38
|
+
const agent = agents.get(id);
|
|
39
|
+
if (agent === undefined) {
|
|
40
|
+
saved.set(id, state);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
restoreInto(agent, state);
|
|
44
|
+
saved.delete(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function create(spec) {
|
|
48
|
+
const defaults = config.defaults;
|
|
49
|
+
const tuned = (key) => spec[key] ?? defaults?.[key];
|
|
50
|
+
const devtoolsName = spec.devtoolsName ?? (config.devtoolsName === undefined ? undefined : `${config.devtoolsName}.${spec.id}`);
|
|
51
|
+
const agent = createAgent({
|
|
52
|
+
id: spec.id,
|
|
53
|
+
needs: spec.needs ?? config.needs,
|
|
54
|
+
options: spec.options ?? config.options,
|
|
55
|
+
...optional("interrupts", spec.interrupts ?? config.interrupts),
|
|
56
|
+
...optional("schedule", spec.schedule),
|
|
57
|
+
mood: {
|
|
58
|
+
...optional("weights", spec.mood?.weights ?? config.mood?.weights),
|
|
59
|
+
...optional("tiers", spec.mood?.tiers ?? config.mood?.tiers),
|
|
60
|
+
},
|
|
61
|
+
...optional("switchMargin", tuned("switchMargin")),
|
|
62
|
+
...optional("minHoldSeconds", tuned("minHoldSeconds")),
|
|
63
|
+
...optional("costWeight", tuned("costWeight")),
|
|
64
|
+
...optional("jitter", tuned("jitter")),
|
|
65
|
+
...optional("rng", spec.rng ?? config.rng),
|
|
66
|
+
...optional("devtoolsName", devtoolsName),
|
|
67
|
+
});
|
|
68
|
+
agents.set(agent.id, agent);
|
|
69
|
+
order.push(agent.id);
|
|
70
|
+
if (spec.select !== undefined)
|
|
71
|
+
selectors.set(agent.id, spec.select);
|
|
72
|
+
if (spec.far === true)
|
|
73
|
+
agent.setFar(true);
|
|
74
|
+
const pending = saved.get(spec.id);
|
|
75
|
+
if (pending !== undefined) {
|
|
76
|
+
restoreInto(agent, pending);
|
|
77
|
+
saved.delete(spec.id);
|
|
78
|
+
}
|
|
79
|
+
return agent;
|
|
80
|
+
}
|
|
81
|
+
const system = {
|
|
82
|
+
add(spec) {
|
|
83
|
+
const existing = agents.get(spec.id);
|
|
84
|
+
if (existing !== undefined)
|
|
85
|
+
return existing;
|
|
86
|
+
return create(spec);
|
|
87
|
+
},
|
|
88
|
+
get: (id) => agents.get(id) ?? null,
|
|
89
|
+
has: (id) => agents.has(id),
|
|
90
|
+
remove(id) {
|
|
91
|
+
const agent = agents.get(id);
|
|
92
|
+
if (agent === undefined)
|
|
93
|
+
return;
|
|
94
|
+
agent.dispose();
|
|
95
|
+
agents.delete(id);
|
|
96
|
+
frames.delete(id);
|
|
97
|
+
selectors.delete(id);
|
|
98
|
+
saved.delete(id);
|
|
99
|
+
const at = order.indexOf(id);
|
|
100
|
+
if (at >= 0)
|
|
101
|
+
order.splice(at, 1);
|
|
102
|
+
},
|
|
103
|
+
ids: () => order,
|
|
104
|
+
size: () => order.length,
|
|
105
|
+
tick(dt, now) {
|
|
106
|
+
hydrate();
|
|
107
|
+
const time = now ?? config.clock?.now() ?? lastNow;
|
|
108
|
+
lastNow = time;
|
|
109
|
+
for (const id of order) {
|
|
110
|
+
const agent = agents.get(id);
|
|
111
|
+
if (agent === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
let targets = [];
|
|
114
|
+
if (!agent.isFar()) {
|
|
115
|
+
const selector = selectFor(id);
|
|
116
|
+
if (selector !== undefined) {
|
|
117
|
+
targets = selector({
|
|
118
|
+
id,
|
|
119
|
+
agent,
|
|
120
|
+
at: config.position?.(id) ?? null,
|
|
121
|
+
now: time,
|
|
122
|
+
far: false,
|
|
123
|
+
});
|
|
124
|
+
if (config.fallback !== undefined)
|
|
125
|
+
targets = withFallback(targets, config.fallback);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
frames.set(id, agent.update({ dt, now: time, targets }));
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
frame: (id) => frames.get(id) ?? null,
|
|
132
|
+
mood: (id) => agents.get(id)?.mood() ?? null,
|
|
133
|
+
moodMean() {
|
|
134
|
+
if (order.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
let total = 0;
|
|
137
|
+
for (const id of order) {
|
|
138
|
+
const agent = agents.get(id);
|
|
139
|
+
if (agent !== undefined)
|
|
140
|
+
total += agent.mood().score;
|
|
141
|
+
}
|
|
142
|
+
return total / order.length;
|
|
143
|
+
},
|
|
144
|
+
action: (id) => frames.get(id)?.action ?? null,
|
|
145
|
+
setFar(id, far) {
|
|
146
|
+
const agent = agents.get(id);
|
|
147
|
+
if (agent === undefined)
|
|
148
|
+
return;
|
|
149
|
+
agent.setFar(far);
|
|
150
|
+
if (far)
|
|
151
|
+
frames.delete(id);
|
|
152
|
+
},
|
|
153
|
+
isFar: (id) => agents.get(id)?.isFar() ?? false,
|
|
154
|
+
save() {
|
|
155
|
+
hydrate();
|
|
156
|
+
const members = {};
|
|
157
|
+
for (const id of order) {
|
|
158
|
+
const agent = agents.get(id);
|
|
159
|
+
if (agent !== undefined)
|
|
160
|
+
members[id] = { agent: agent.snapshot(), far: agent.isFar() };
|
|
161
|
+
}
|
|
162
|
+
const state = { version: 1, members };
|
|
163
|
+
config.storage?.write(state);
|
|
164
|
+
return state;
|
|
165
|
+
},
|
|
166
|
+
load: loadState,
|
|
167
|
+
dispose() {
|
|
168
|
+
for (const agent of agents.values())
|
|
169
|
+
agent.dispose();
|
|
170
|
+
agents.clear();
|
|
171
|
+
frames.clear();
|
|
172
|
+
saved.clear();
|
|
173
|
+
order.length = 0;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
hydrate();
|
|
177
|
+
for (const spec of config.members ?? [])
|
|
178
|
+
create(spec);
|
|
179
|
+
return system;
|
|
180
|
+
}
|
|
181
|
+
/** Targets keep their own fallback; the rest inherit the system's one. */
|
|
182
|
+
function withFallback(targets, fallback) {
|
|
183
|
+
let changed = false;
|
|
184
|
+
const next = targets.map((target) => {
|
|
185
|
+
if (target.fallback !== undefined)
|
|
186
|
+
return target;
|
|
187
|
+
changed = true;
|
|
188
|
+
return { ...target, fallback };
|
|
189
|
+
});
|
|
190
|
+
return changed ? next : targets;
|
|
191
|
+
}
|
|
192
|
+
/** Spread a config field only when the game set it, so `createAgent`'s own default stands otherwise. */
|
|
193
|
+
function optional(key, value) {
|
|
194
|
+
return value === undefined ? {} : { [key]: value };
|
|
195
|
+
}
|
package/dist/meta/changelog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.36.0";
|
|
3
3
|
/** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
|
|
4
4
|
export interface ChangelogEntry {
|
|
5
5
|
migrate: readonly string[];
|
package/dist/meta/changelog.js
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.36.0";
|
|
3
3
|
/** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
|
|
4
4
|
export const CHANGELOG = {
|
|
5
|
+
"0.36.0": {
|
|
6
|
+
migrate: [
|
|
7
|
+
"Bump lockstep SDK packages to `^0.36.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.33.0`; `@jgengine/github` is `0.23.0`.",
|
|
8
|
+
],
|
|
9
|
+
added: [
|
|
10
|
+
"`createAgentSystem` (`@jgengine/core/agents/system`) takes a game's needs, options, mood weights, members and schedules once and returns a running cast: one `tick(dt)` for everyone, one target selector for the whole system, a fallback declared once, and a plain-JSON `save()`/`load()` that rides the game's own save.",
|
|
11
|
+
"`--quick` on `bun run shoot` and `bun run drive` is the 960x540 iteration preset: one settled frame (a 2s settle window in drive), no video, no perf pass, written as `<name>-quick.png` and timed on its own line. It refuses a 1600x900 pixel claim, a frame-time claim, a non-desktop device and a recording flag by name instead of producing weaker evidence; full stays the default for PR shots. On a warm daemon a two-shot `drive` of ember-command went 9.3s to 8.2s and 4316KB to 1383KB of PNG.",
|
|
12
|
+
"`createPlayScript` / `runPlayScript` (`@jgengine/shell/play`) play a real `PlayableGame` headlessly from a tick-based script: `move`, `aim`, `fire`, `use`, `wait` and `command` steps plus `setup`/`onTick` hooks, `expect` assertions read after setup and on their own tick, a tick timeline, and an FNV digest for replay comparison. It replays the shell's own frame pipeline — held movement through the walk controller, a bound action resolving to its command on press and repeat, interact prompts, hotbar — so a script proves the same code path a player's hands do. No browser, no wall clock, fixed `dt`.",
|
|
13
|
+
"`bun run simulate <game> <script>` runs a committed `<game>/scripts/*.play.ts` against a game under `./Games`, prints the timeline, names a failed assertion and exits 1. It merges the game's own tsconfig `paths` with this checkout's source packages, so the game imports and the engine under test are one module graph.",
|
|
14
|
+
"`HeadlessRunner.aim()` steers the walk controller mid-run; `HeadlessRunnerOptions` gains `seed` (deterministic `ctx.rng`) and `collision` (the shell's voxel config, previously dropped on the headless path).",
|
|
15
|
+
],
|
|
16
|
+
changed: [
|
|
17
|
+
"The graphics quality tier now costs what it claims on the render path. A tier's post chain renders at its own resolution scale (`low` 0.7, `medium` 0.85, `high`/`ultra` native) instead of always at the canvas size, and a game's authored `shadowMapSize` and `cascades` are a ceiling the tier caps rather than a value it ignored — every reference game authored 3 cascades and 1024/2048, so no tier changed a shadow before this. `low` and `medium` also stretch the far cascades' refresh interval, and the smallest caster worth a depth draw grows as the map shrinks. `high` and `ultra` keep native resolution, the authored shadow sizes and the measured cadence, so their frames are unchanged.",
|
|
18
|
+
"Measured uncapped at 1600x900: whole-frame draws at `low` fell 53% in harbor-heat (515 → 242) and 63% in drift-foundry (1,098 → 408), with the shadow pass there down from 911 ms to 344 ms over a 6 s window; `high` and `ultra` held their draw counts.",
|
|
19
|
+
"Terrain build no longer scans every road and path segment for every vertex. A baked path corridor, a draped road and a painted polyline region each answer \"how far is this point?\" from a precomputed segment grid, so Scrap Signal's world build drops from 16.6 s to 0.51 s on the same machine and its longest boot stall from 11.6–18.4 s to 1.2 s. Sampled heights and palettes are bit-identical to the previous full scan, and `withPathProfiles` still composes profiles in order.",
|
|
20
|
+
"Load-time shader warmup compiles against the render state the first frame actually draws with. A scene whose post chain renders into its own buffer was linking two programs per material, one linear untone-mapped set the frame draws and one canvas set nothing ever drew; the cover no longer waits for the second set. Harbor Heat links 84 programs to 58 and Scrap Signal 124 to 83 before the cover lifts, with no visual change.",
|
|
21
|
+
],
|
|
22
|
+
removed: [],
|
|
23
|
+
},
|
|
5
24
|
"0.35.0": {
|
|
6
25
|
migrate: [
|
|
7
26
|
"Bump lockstep SDK packages to `^0.35.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.32.0`; `@jgengine/github` is `0.22.0`.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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.36.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.36.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -48,6 +48,35 @@
|
|
|
48
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
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
50
|
|
|
51
|
+
## @jgengine/core/agents/system
|
|
52
|
+
|
|
53
|
+
- `Agent` (interface): interface Agent — Needs, mood, schedule and a chosen action, in one object a game ticks.
|
|
54
|
+
- `AgentAction` (interface): interface AgentAction — What the agent is doing.
|
|
55
|
+
- `AgentFallback` (interface): interface AgentFallback — Where an agent goes when the thing it wanted is gone: a named option and a named place.
|
|
56
|
+
- `AgentFrame` (interface): interface AgentFrame — What one update produced: the action, the feeling, and where the day plan puts the agent.
|
|
57
|
+
- `AgentMemberSpec` (interface): interface AgentMemberSpec extends UtilityTunables — One member: an id, and only what differs from the system's defaults.
|
|
58
|
+
- `AgentMemberState` (interface): interface AgentMemberState — One member's saved state: its agent snapshot plus the off-camera flag the snapshot does not carry.
|
|
59
|
+
- `AgentMoodConfig` (interface): interface AgentMoodConfig — The mood policy a member runs on: which needs matter, and the game's own words for the bands.
|
|
60
|
+
- `AgentScheduleConfig` (interface): interface AgentScheduleConfig — An agent's whole day as data: what it does, where, and when.
|
|
61
|
+
- `AgentSchedulePhase` (interface): interface AgentSchedulePhase — Where a schedule puts an agent at one instant.
|
|
62
|
+
- `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.
|
|
63
|
+
- `AgentSelectView` (interface): interface AgentSelectView — What a selector is told about the member asking: who it is, its live agent, and where it stands.
|
|
64
|
+
- `AgentSelector` (type): type AgentSelector = (view: AgentSelectView) => readonly AgentTarget[] — Everything the member could do right now, as the game's world offers it. Declared once per system.
|
|
65
|
+
- `AgentSnapshot` (interface): interface AgentSnapshot — Serializable whole agent: needs, feeling, held action, day-plan cursor and the far flag.
|
|
66
|
+
- `AgentSystem` (interface): interface AgentSystem — A whole cast of agents behind one factory and one tick.
|
|
67
|
+
- `AgentSystemConfig` (interface): interface AgentSystemConfig — The game's data, in one place: what its people need, what they can do, and how they feel.
|
|
68
|
+
- `AgentSystemState` (interface): interface AgentSystemState — The whole system as plain JSON - drop it in a save, read it back with {@link AgentSystem.load}.
|
|
69
|
+
- `AgentSystemStorage` (interface): interface AgentSystemStorage — Where a system keeps its save data: a key-value store, a save slot, or a test's map.
|
|
70
|
+
- `AgentTarget` (interface): interface AgentTarget — One reachable thing the agent could do this tick, as the game sees its own world.
|
|
71
|
+
- `Mood` (interface): interface Mood — What the agent feels right now, on the caller's own tier labels.
|
|
72
|
+
- `MoodTier` (interface): interface MoodTier — One labelled band of the 0-100 mood score.
|
|
73
|
+
- `MoodWeight` (interface): interface MoodWeight — One need's say in the mood score; weights are relative, so they need no fixed total.
|
|
74
|
+
- `NeedDefinition` (type): type NeedDefinition = DecayMeterConfig — A decaying need is a survival decay meter (`max`/`min`/`start`/`rate`, optional moodle thresholds) whose satisfaction an agent brain reads to score actions. The meter owns the numbers; per-need appeal lives in the game's utility options.
|
|
75
|
+
- `UtilityInterrupt` (interface): interface UtilityInterrupt — A need level that overrides the sticky pick the moment it is crossed.
|
|
76
|
+
- `UtilityOption` (interface): interface UtilityOption — A thing an agent can decide to do, scored against its needs.
|
|
77
|
+
- `UtilityTunables` (interface): interface UtilityTunables — In-play dials that stay reachable through {@link UtilityBrain.retune}.
|
|
78
|
+
- `createAgentSystem` (function): function createAgentSystem(config: AgentSystemConfig): AgentSystem — One call for a whole cast: a game hands over its needs, options, mood weights, schedules and one target selector, and gets back a running system with a single `tick(dt)` and a save blob that goes into the game's own save.
|
|
79
|
+
|
|
51
80
|
## @jgengine/core/behaviour/behaviour
|
|
52
81
|
|
|
53
82
|
- `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.
|
|
@@ -23,6 +23,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
23
23
|
## agents — a needs, thought, action and mood loop an agent runs on the world clock
|
|
24
24
|
|
|
25
25
|
- `createAgent` (function) · `import { createAgent } from "@jgengine/core/agents/agent"`
|
|
26
|
+
- `createAgentSystem` (function) · `import { createAgentSystem } from "@jgengine/core/agents/system"`
|
|
26
27
|
|
|
27
28
|
## auction-book — timed bid auctions with escrowed bids, buyout, anti-snipe close extension, and collection-box settlement
|
|
28
29
|
|