@jgengine/core 0.14.0 → 0.15.0-next.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 +483 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +1 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/gameContext.js +6 -1
- package/dist/runtime/gameContextTypes.d.ts +18 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +18 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +1 -44
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +17 -2
- package/dist/world.js +17 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +378 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
package/dist/ai/flock.js
CHANGED
|
@@ -88,40 +88,28 @@ export function flockSteer(agent, neighbors, config, target) {
|
|
|
88
88
|
return [steerX * scale, steerY * scale, steerZ * scale];
|
|
89
89
|
}
|
|
90
90
|
const CELL_KEY_STRIDE = 1_048_576;
|
|
91
|
-
const gridCells = new Map();
|
|
92
|
-
const freeBuckets = [];
|
|
93
|
-
const neighborScratch = [];
|
|
94
|
-
let steerScratch = new Float64Array(0);
|
|
95
91
|
function cellKey(cx, cz) {
|
|
96
92
|
return cx * CELL_KEY_STRIDE + cz;
|
|
97
93
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const needed = agentCount * 3;
|
|
103
|
-
if (steerScratch.length < needed) {
|
|
104
|
-
steerScratch = new Float64Array(needed);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
94
|
+
/**
|
|
95
|
+
* Per-call spatial index + scratch — never module-global.
|
|
96
|
+
* Two `stepFlock` worlds in one process must not share a grid (#1545 / sdk remediation Phase 3a).
|
|
97
|
+
*/
|
|
107
98
|
function buildAgentGrid(agents, invCell) {
|
|
108
|
-
|
|
109
|
-
bucket.length = 0;
|
|
110
|
-
freeBuckets.push(bucket);
|
|
111
|
-
}
|
|
112
|
-
gridCells.clear();
|
|
99
|
+
const gridCells = new Map();
|
|
113
100
|
for (let i = 0; i < agents.length; i += 1) {
|
|
114
101
|
const position = agents[i].position;
|
|
115
102
|
const key = cellKey(Math.floor(position[0] * invCell), Math.floor(position[2] * invCell));
|
|
116
103
|
let bucket = gridCells.get(key);
|
|
117
104
|
if (bucket === undefined) {
|
|
118
|
-
bucket =
|
|
105
|
+
bucket = [];
|
|
119
106
|
gridCells.set(key, bucket);
|
|
120
107
|
}
|
|
121
108
|
bucket.push(i);
|
|
122
109
|
}
|
|
110
|
+
return gridCells;
|
|
123
111
|
}
|
|
124
|
-
function fillNeighbors(agents, agentIndex, invCell) {
|
|
112
|
+
function fillNeighbors(agents, agentIndex, invCell, gridCells, neighborScratch) {
|
|
125
113
|
const agent = agents[agentIndex];
|
|
126
114
|
const cx = Math.floor(agent.position[0] * invCell);
|
|
127
115
|
const cz = Math.floor(agent.position[2] * invCell);
|
|
@@ -152,10 +140,12 @@ export function stepFlock(agents, config, dt, target) {
|
|
|
152
140
|
return;
|
|
153
141
|
const cellSize = config.neighborRadius > 1e-6 ? config.neighborRadius : 1;
|
|
154
142
|
const invCell = 1 / cellSize;
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
// All scratch is call-local so concurrent worlds (or re-entrant steps) never share a grid.
|
|
144
|
+
const gridCells = buildAgentGrid(agents, invCell);
|
|
145
|
+
const neighborScratch = [];
|
|
146
|
+
const steerScratch = new Float64Array(count * 3);
|
|
157
147
|
for (let i = 0; i < count; i += 1) {
|
|
158
|
-
fillNeighbors(agents, i, invCell);
|
|
148
|
+
fillNeighbors(agents, i, invCell, gridCells, neighborScratch);
|
|
159
149
|
const steer = flockSteer(agents[i], neighborScratch, config, target);
|
|
160
150
|
const base = i * 3;
|
|
161
151
|
steerScratch[base] = steer[0];
|
package/dist/ai/laneSelect.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
1
2
|
/**
|
|
2
3
|
* Live cost-based selection over parallel corridors (#286.7) — the racing-line / lane-merge pick
|
|
3
4
|
* every AI driver hand-rolled. Evaluate each lane's cost this tick (crowding, hazards, distance)
|
|
@@ -9,7 +10,7 @@ export function pickLane(candidates, options = {}) {
|
|
|
9
10
|
return null;
|
|
10
11
|
const stickiness = options.stickiness ?? 1.2;
|
|
11
12
|
const tieEpsilon = options.tieEpsilon ?? 0;
|
|
12
|
-
const rng = options.rng
|
|
13
|
+
const rng = resolveRng(options.rng, "selectLane");
|
|
13
14
|
let best = candidates[0];
|
|
14
15
|
for (const candidate of candidates) {
|
|
15
16
|
if (candidate.cost < best.cost)
|
package/dist/ai/mobBrain.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createThreatTable } from "./threat.js";
|
|
2
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
2
3
|
const HOLD = {
|
|
3
4
|
targetId: null,
|
|
4
5
|
moveTo: null,
|
|
@@ -18,7 +19,7 @@ export function createMobBrain(config, deps) {
|
|
|
18
19
|
const wander = config.wander === false || config.wander === undefined ? null : config.wander;
|
|
19
20
|
const wanderInterval = wander?.intervalSeconds ?? 4;
|
|
20
21
|
const wanderArrive = wander?.arriveRadius ?? 0.6;
|
|
21
|
-
const rng = deps.rng
|
|
22
|
+
const rng = resolveRng(deps.rng, "createMobBrain");
|
|
22
23
|
let currentMode = "idle";
|
|
23
24
|
let currentTarget = null;
|
|
24
25
|
let evading = false;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type DirectorContext, type SpawnDirectorConfig, type SpawnDirectorState, type SpawnRequest } from "./spawnDirector.js";
|
|
2
|
+
/**
|
|
3
|
+
* A callback the wave runner hands each {@link SpawnRequest} the underlying spawn
|
|
4
|
+
* director emits. The runner never instantiates entities itself — it forwards the
|
|
5
|
+
* request (a free-string `entryId` "kind", cost, wave, optional point/lane) to the
|
|
6
|
+
* game, which decides what to build. Keeps the runner genre-agnostic.
|
|
7
|
+
*/
|
|
8
|
+
export type WaveSpawnSink = (request: SpawnRequest) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for {@link createWaveRunner}: the full {@link SpawnDirectorConfig}
|
|
11
|
+
* (escalating waves, budgets, seed, spawn points) plus an optional {@link WaveSpawnSink}.
|
|
12
|
+
* The runner owns the director state — you never pass a `SpawnDirectorState` here.
|
|
13
|
+
*/
|
|
14
|
+
export interface WaveRunnerConfig extends SpawnDirectorConfig {
|
|
15
|
+
/** Optional sink invoked once per spawned {@link SpawnRequest} during {@link WaveRunner.update}. */
|
|
16
|
+
onSpawn?: WaveSpawnSink;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A pooled, per-frame readout of the current wave — the "brain behind WAVE 3".
|
|
20
|
+
* Reused across {@link WaveRunner.view} calls (never per-frame allocated), so read
|
|
21
|
+
* it and render it, don't retain it.
|
|
22
|
+
*/
|
|
23
|
+
export interface WaveView {
|
|
24
|
+
/** 1-based wave number for display ("WAVE N"). The 0-based index lives on {@link WaveRunner.state}. */
|
|
25
|
+
wave: number;
|
|
26
|
+
/**
|
|
27
|
+
* Progress `0..1` through the current wave: `waveElapsed / duration` when the
|
|
28
|
+
* wave declares a `duration`, otherwise a budget-drain estimate (`1 - budget /
|
|
29
|
+
* waveBudget`). A best-effort readout for a progress bar, not an exact schedule.
|
|
30
|
+
*/
|
|
31
|
+
waveProgress: number;
|
|
32
|
+
/** Spawn budget currently banked in the director. */
|
|
33
|
+
budget: number;
|
|
34
|
+
/** How many entities have spawned during the current wave. */
|
|
35
|
+
spawnedThisWave: number;
|
|
36
|
+
/** How many entities have spawned across every wave so far. */
|
|
37
|
+
spawnedTotal: number;
|
|
38
|
+
/** Current alert level `0..1` (raised by {@link WaveRunner.raiseAlert}, decays over time). */
|
|
39
|
+
alert: number;
|
|
40
|
+
/** True once the final non-looping wave has finished. */
|
|
41
|
+
done: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** A live, observable, serializable wave/spawn runner over a seeded spawn director. */
|
|
44
|
+
export interface WaveRunner {
|
|
45
|
+
/**
|
|
46
|
+
* Advance the director by `dt` seconds: bank budget, roll escalation/alert, cross
|
|
47
|
+
* wave boundaries, and forward every emitted {@link SpawnRequest} to the configured
|
|
48
|
+
* `onSpawn` sink. Pass a {@link DirectorContext} (defaults to `{ alive: 0 }`) to feed
|
|
49
|
+
* back the live alive-count / player positions. Notifies subscribers on change.
|
|
50
|
+
*/
|
|
51
|
+
update(dt: number, ctx?: DirectorContext): void;
|
|
52
|
+
/** The pooled current-wave readout to render (do not retain). */
|
|
53
|
+
view(): WaveView;
|
|
54
|
+
/** Force an immediate jump to the next wave (delegates to `advanceWave`). Notifies. */
|
|
55
|
+
forceNextWave(): void;
|
|
56
|
+
/** Raise the alert level by `amount` (clamped `0..1`), boosting spawn budget. Notifies. */
|
|
57
|
+
raiseAlert(amount: number): void;
|
|
58
|
+
/** The raw underlying {@link SpawnDirectorState} (0-based `wave`); read-only snapshot of internals. */
|
|
59
|
+
state(): SpawnDirectorState;
|
|
60
|
+
/** Observe changes (update, forceNextWave, raiseAlert, restore). Returns an unsubscribe fn. */
|
|
61
|
+
subscribe(listener: () => void): () => void;
|
|
62
|
+
/** Serializable state for a save — the plain {@link SpawnDirectorState} object. */
|
|
63
|
+
snapshot(): SpawnDirectorState;
|
|
64
|
+
/** Restore from a {@link snapshot} (a plain {@link SpawnDirectorState}). Notifies. */
|
|
65
|
+
restore(snapshot: SpawnDirectorState): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A thin, stateful, observable wrapper around the seeded {@link createSpawnDirectorState}
|
|
69
|
+
* / {@link advanceSpawnDirector} model. It OWNS a {@link SpawnDirectorState}, ticks it
|
|
70
|
+
* from `update(dt, ctx?)`, keeps the new state, and forwards each emitted
|
|
71
|
+
* {@link SpawnRequest} to an optional `onSpawn` sink so the model never instantiates
|
|
72
|
+
* entities. `view()` returns a pooled per-frame readout — the "brain behind WAVE 3":
|
|
73
|
+
* the 1-based wave number, wave progress `0..1`, banked budget, per-wave and total
|
|
74
|
+
* spawn counts, alert, and done flag. `forceNextWave` / `raiseAlert` drive it,
|
|
75
|
+
* `subscribe` observes it, and `snapshot`/`restore` round-trip the plain serializable
|
|
76
|
+
* state object through a save. Nothing here is genre-specific: spawn-entry `kind`s
|
|
77
|
+
* (the director's `entryId`s) stay free strings the runner never interprets.
|
|
78
|
+
*
|
|
79
|
+
* @capability wave-runner observable serializable wave/spawn runner over the seeded spawn director — WAVE N, wave progress, budget/alert readout, onSpawn sink, snapshot/restore
|
|
80
|
+
*/
|
|
81
|
+
export declare function createWaveRunner(config: WaveRunnerConfig): WaveRunner;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { advanceSpawnDirector, advanceWave, createSpawnDirectorState, raiseAlert as raiseDirectorAlert, } from "./spawnDirector.js";
|
|
2
|
+
const DEFAULT_CONTEXT = { alive: 0 };
|
|
3
|
+
function clamp01(value) {
|
|
4
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A thin, stateful, observable wrapper around the seeded {@link createSpawnDirectorState}
|
|
8
|
+
* / {@link advanceSpawnDirector} model. It OWNS a {@link SpawnDirectorState}, ticks it
|
|
9
|
+
* from `update(dt, ctx?)`, keeps the new state, and forwards each emitted
|
|
10
|
+
* {@link SpawnRequest} to an optional `onSpawn` sink so the model never instantiates
|
|
11
|
+
* entities. `view()` returns a pooled per-frame readout — the "brain behind WAVE 3":
|
|
12
|
+
* the 1-based wave number, wave progress `0..1`, banked budget, per-wave and total
|
|
13
|
+
* spawn counts, alert, and done flag. `forceNextWave` / `raiseAlert` drive it,
|
|
14
|
+
* `subscribe` observes it, and `snapshot`/`restore` round-trip the plain serializable
|
|
15
|
+
* state object through a save. Nothing here is genre-specific: spawn-entry `kind`s
|
|
16
|
+
* (the director's `entryId`s) stay free strings the runner never interprets.
|
|
17
|
+
*
|
|
18
|
+
* @capability wave-runner observable serializable wave/spawn runner over the seeded spawn director — WAVE N, wave progress, budget/alert readout, onSpawn sink, snapshot/restore
|
|
19
|
+
*/
|
|
20
|
+
export function createWaveRunner(config) {
|
|
21
|
+
const onSpawn = config.onSpawn;
|
|
22
|
+
let state = createSpawnDirectorState(config);
|
|
23
|
+
const listeners = new Set();
|
|
24
|
+
// Pooled readout — reused across view() calls, never per-frame allocated.
|
|
25
|
+
const readout = {
|
|
26
|
+
wave: 1,
|
|
27
|
+
waveProgress: 0,
|
|
28
|
+
budget: 0,
|
|
29
|
+
spawnedThisWave: 0,
|
|
30
|
+
spawnedTotal: 0,
|
|
31
|
+
alert: 0,
|
|
32
|
+
done: false,
|
|
33
|
+
};
|
|
34
|
+
function notify() {
|
|
35
|
+
for (const listener of listeners)
|
|
36
|
+
listener();
|
|
37
|
+
}
|
|
38
|
+
/** True if any field a HUD cares about differs between two states. */
|
|
39
|
+
function changed(a, b) {
|
|
40
|
+
return (a.wave !== b.wave ||
|
|
41
|
+
a.budget !== b.budget ||
|
|
42
|
+
a.spawnedThisWave !== b.spawnedThisWave ||
|
|
43
|
+
a.spawnedTotal !== b.spawnedTotal ||
|
|
44
|
+
a.alert !== b.alert ||
|
|
45
|
+
a.done !== b.done);
|
|
46
|
+
}
|
|
47
|
+
function waveProgress() {
|
|
48
|
+
const manifest = config.waves[state.wave];
|
|
49
|
+
if (manifest === undefined)
|
|
50
|
+
return state.done ? 1 : 0;
|
|
51
|
+
if (manifest.duration !== undefined && manifest.duration > 0) {
|
|
52
|
+
return clamp01(state.waveElapsed / manifest.duration);
|
|
53
|
+
}
|
|
54
|
+
if (manifest.budget > 0) {
|
|
55
|
+
return clamp01(1 - state.budget / manifest.budget);
|
|
56
|
+
}
|
|
57
|
+
return state.done ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
update(dt, ctx = DEFAULT_CONTEXT) {
|
|
61
|
+
const previous = state;
|
|
62
|
+
const step = advanceSpawnDirector(config, state, dt, ctx);
|
|
63
|
+
state = step.state;
|
|
64
|
+
if (onSpawn !== undefined) {
|
|
65
|
+
for (const spawn of step.spawns)
|
|
66
|
+
onSpawn(spawn);
|
|
67
|
+
}
|
|
68
|
+
if (step.spawns.length > 0 || changed(previous, state))
|
|
69
|
+
notify();
|
|
70
|
+
},
|
|
71
|
+
view() {
|
|
72
|
+
readout.wave = state.wave + 1;
|
|
73
|
+
readout.waveProgress = waveProgress();
|
|
74
|
+
readout.budget = state.budget;
|
|
75
|
+
readout.spawnedThisWave = state.spawnedThisWave;
|
|
76
|
+
readout.spawnedTotal = state.spawnedTotal;
|
|
77
|
+
readout.alert = state.alert;
|
|
78
|
+
readout.done = state.done;
|
|
79
|
+
return readout;
|
|
80
|
+
},
|
|
81
|
+
forceNextWave() {
|
|
82
|
+
state = advanceWave(config, state);
|
|
83
|
+
notify();
|
|
84
|
+
},
|
|
85
|
+
raiseAlert(amount) {
|
|
86
|
+
state = raiseDirectorAlert(state, amount);
|
|
87
|
+
notify();
|
|
88
|
+
},
|
|
89
|
+
state() {
|
|
90
|
+
return state;
|
|
91
|
+
},
|
|
92
|
+
subscribe(listener) {
|
|
93
|
+
listeners.add(listener);
|
|
94
|
+
return () => listeners.delete(listener);
|
|
95
|
+
},
|
|
96
|
+
snapshot() {
|
|
97
|
+
return state;
|
|
98
|
+
},
|
|
99
|
+
restore(snapshot) {
|
|
100
|
+
state = snapshot;
|
|
101
|
+
notify();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/authoring.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Core-side convenience barrel for pure authored helpers (loot, world features, ownership,
|
|
3
|
+
* spawn selection, RNG). **Not the game entrypoint** — game projects start at
|
|
4
|
+
* `@jgengine/shell/gameKit` (`defineGame`, `GameHost`, stores, systems, HUD). Prefer that
|
|
5
|
+
* surface; import from here only when you need a core helper the kit does not re-export.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* concepts without requiring callers to understand the repository layout.
|
|
7
|
+
* @deprecated Prefer `@jgengine/shell/gameKit` as the start-here surface for games. This
|
|
8
|
+
* module remains for deep core helpers and existing pure-import consumers; it is not removed.
|
|
7
9
|
*/
|
|
8
10
|
export { type GameDefinition, type GameDefinitionConfig, type GameLoop, type InventoryDeclaration, type PhysicsConfig, type GameServerConfig, } from "./game/defineGame.js";
|
|
9
11
|
export { lootTable, type LootTableDef, type LootEntry, type Drop, } from "./game/lootTable.js";
|
package/dist/authoring.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Core-side convenience barrel for pure authored helpers (loot, world features, ownership,
|
|
3
|
+
* spawn selection, RNG). **Not the game entrypoint** — game projects start at
|
|
4
|
+
* `@jgengine/shell/gameKit` (`defineGame`, `GameHost`, stores, systems, HUD). Prefer that
|
|
5
|
+
* surface; import from here only when you need a core helper the kit does not re-export.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* concepts without requiring callers to understand the repository layout.
|
|
7
|
+
* @deprecated Prefer `@jgengine/shell/gameKit` as the start-here surface for games. This
|
|
8
|
+
* module remains for deep core helpers and existing pure-import consumers; it is not removed.
|
|
7
9
|
*/
|
|
8
10
|
export {} from "./game/defineGame.js";
|
|
9
11
|
export { lootTable, } from "./game/lootTable.js";
|
|
@@ -3,6 +3,15 @@ export interface CastConfig {
|
|
|
3
3
|
castTimeMs: number;
|
|
4
4
|
/** Total movement distance tolerated before the cast auto-interrupts; default `0.04`. `Infinity` allows casting on the move. */
|
|
5
5
|
moveTolerance?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Per-tick movement at or below this is ignored rather than added to the running total — the
|
|
8
|
+
* caster's noise floor. A 3D game's pose jitters every frame from terrain-following ground snap
|
|
9
|
+
* and float drift; since `moveTolerance` accumulates, feeding those raw deltas straight in will
|
|
10
|
+
* silently cancel casts the player never interrupted, and the failure is intermittent rather than
|
|
11
|
+
* obvious. Only the game knows its own noise floor, so declare it once here instead of filtering
|
|
12
|
+
* at every `tick` call site. Default `0` (count every reported movement).
|
|
13
|
+
*/
|
|
14
|
+
moveDeadzone?: number;
|
|
6
15
|
}
|
|
7
16
|
export interface CastBarSnapshot {
|
|
8
17
|
abilityId: string;
|
|
@@ -34,7 +43,10 @@ export interface CastRunner {
|
|
|
34
43
|
state(): CastBarSnapshot | null;
|
|
35
44
|
/** Interrupt the in-flight cast now; returns the interruption event, or `null` when idle. */
|
|
36
45
|
interrupt(reason?: CastInterruptReason): CastEvent | null;
|
|
37
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Advance by `dtSeconds`; `movedDistance` accumulates toward the config's `moveTolerance`, except
|
|
48
|
+
* for per-tick movement at or below `moveDeadzone`, which is treated as jitter and ignored.
|
|
49
|
+
*/
|
|
38
50
|
tick(dtSeconds: number, movedDistance?: number): CastEvent | null;
|
|
39
51
|
}
|
|
40
52
|
/**
|
|
@@ -26,6 +26,7 @@ export function createCastRunner() {
|
|
|
26
26
|
abilityId: config.abilityId,
|
|
27
27
|
castTimeMs: Math.max(0, config.castTimeMs),
|
|
28
28
|
moveTolerance: config.moveTolerance ?? DEFAULT_MOVE_TOLERANCE,
|
|
29
|
+
moveDeadzone: Math.max(0, config.moveDeadzone ?? 0),
|
|
29
30
|
elapsedMs: 0,
|
|
30
31
|
movedTotal: 0,
|
|
31
32
|
};
|
|
@@ -43,7 +44,10 @@ export function createCastRunner() {
|
|
|
43
44
|
tick(dtSeconds, movedDistance = 0) {
|
|
44
45
|
if (active === null)
|
|
45
46
|
return null;
|
|
46
|
-
|
|
47
|
+
const moved = Math.max(0, movedDistance);
|
|
48
|
+
// Below the caster's noise floor this is jitter, not travel — do not accumulate it.
|
|
49
|
+
if (moved > active.moveDeadzone)
|
|
50
|
+
active.movedTotal += moved;
|
|
47
51
|
if (active.movedTotal > active.moveTolerance) {
|
|
48
52
|
const abilityId = active.abilityId;
|
|
49
53
|
active = null;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One tier threshold in a {@link ComboMeter}. Tiers gate on the integer combo
|
|
3
|
+
* `count` (not a filled fraction), and `id` is a free string the game owns and
|
|
4
|
+
* styles ("good", "great", "savage", …) — the model never interprets it.
|
|
5
|
+
*/
|
|
6
|
+
export interface ComboTier {
|
|
7
|
+
/** Combo count at or above which this tier becomes active. Tiers sort ascending. */
|
|
8
|
+
threshold: number;
|
|
9
|
+
/** Free-string tier id the game styles; the model never branches on it. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Explicit multiplier while this tier is active. Overrides any fallback curve. */
|
|
12
|
+
multiplier?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link createComboMeter}. */
|
|
15
|
+
export interface ComboMeterConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Decay window in ms: how long after the last {@link ComboMeter.hit} the combo
|
|
18
|
+
* survives before it drops. Each `hit` resets this window; when it elapses the
|
|
19
|
+
* combo drops (to 0, or by {@link ComboMeterConfig.dropStep}).
|
|
20
|
+
*/
|
|
21
|
+
windowMs: number;
|
|
22
|
+
/** Tier thresholds on the integer count. Sorted ascending internally; ids are free strings. */
|
|
23
|
+
tiers?: readonly ComboTier[];
|
|
24
|
+
/**
|
|
25
|
+
* When the window elapses, drop the count by this many instead of clearing to 0,
|
|
26
|
+
* re-arming the window so a long combo bleeds down step by step. Omit → drop to 0.
|
|
27
|
+
*/
|
|
28
|
+
dropStep?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Fallback multiplier growth when the active tier has no explicit `multiplier`:
|
|
31
|
+
* `1 + (tierIndex + 1) * multiplierPerTier`. Below the first tier the multiplier
|
|
32
|
+
* is always 1. Ignored where a tier sets its own `multiplier`.
|
|
33
|
+
*/
|
|
34
|
+
multiplierPerTier?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Last-resort multiplier curve when neither the active tier's `multiplier` nor
|
|
37
|
+
* `multiplierPerTier` applies. Receives the current count and active tier index
|
|
38
|
+
* (`-1` below the first tier). Not serialized — snapshot/restore carry state, not config.
|
|
39
|
+
*/
|
|
40
|
+
multiplierCurve?: (count: number, tierIndex: number) => number;
|
|
41
|
+
/** Injected clock (ms). Default `Date.now`. Ignored once {@link ComboMeter.update} drives time. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
/** Initial combo count (pre-seed a demo/save at a high chain). Default 0. */
|
|
44
|
+
count?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A pooled, read-only snapshot of the live meter for a renderer to draw each
|
|
48
|
+
* frame. Reused across {@link ComboMeter.view} calls and overwritten on the next
|
|
49
|
+
* one — read it, do not retain it. For a save, use {@link ComboMeter.snapshot}.
|
|
50
|
+
*/
|
|
51
|
+
export interface ComboMeterView {
|
|
52
|
+
/** Current combo count. */
|
|
53
|
+
count: number;
|
|
54
|
+
/** Highest count reached since the last {@link ComboMeter.reset}. */
|
|
55
|
+
peak: number;
|
|
56
|
+
/** Active tier id, or `null` below the first tier. */
|
|
57
|
+
tier: string | null;
|
|
58
|
+
/** Active tier index into the sorted tier list, or `-1` below the first tier. */
|
|
59
|
+
tierIndex: number;
|
|
60
|
+
/** Derived multiplier for the active tier (default 1). */
|
|
61
|
+
multiplier: number;
|
|
62
|
+
/** Milliseconds left in the decay window before the next drop (0 when idle). */
|
|
63
|
+
remainingMs: number;
|
|
64
|
+
/** Window remaining as `0..1` — for a draining ring/bar. */
|
|
65
|
+
fraction: number;
|
|
66
|
+
/** Free-string `kind` of the most recent hit, or `""` — carried through, never interpreted. */
|
|
67
|
+
kind: string;
|
|
68
|
+
}
|
|
69
|
+
/** Serializable state for save/restore — clock-agnostic (stores time left, not an absolute deadline). */
|
|
70
|
+
export interface ComboMeterSnapshot {
|
|
71
|
+
/** Combo count. */
|
|
72
|
+
count: number;
|
|
73
|
+
/** Peak count reached. */
|
|
74
|
+
peak: number;
|
|
75
|
+
/** Milliseconds remaining in the decay window at snapshot time. */
|
|
76
|
+
remainingMs: number;
|
|
77
|
+
/** Active tier id at snapshot time (recomputed on restore; kept for readability). */
|
|
78
|
+
tier: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A live, observable combo/multiplier meter — an integer hit chain with a decay
|
|
82
|
+
* window, free-string tier thresholds, a derived multiplier, and peak tracking.
|
|
83
|
+
*/
|
|
84
|
+
export interface ComboMeter {
|
|
85
|
+
/**
|
|
86
|
+
* Register a landed hit: bump the count by one and reset the decay window. `kind`
|
|
87
|
+
* is a free string carried through to {@link ComboMeter.view} for the renderer to
|
|
88
|
+
* color/skin; the model never interprets it. Returns the new count.
|
|
89
|
+
*/
|
|
90
|
+
hit(kind?: string): number;
|
|
91
|
+
/**
|
|
92
|
+
* Advance the decay window by `dtSeconds`, dropping the combo when the window
|
|
93
|
+
* elapses. Calling this once switches the meter to delta-driven time (the injected
|
|
94
|
+
* `now` clock is then ignored) — drive it each frame, or omit it and let reads read
|
|
95
|
+
* `now`. Notifies subscribers when the count changes.
|
|
96
|
+
*/
|
|
97
|
+
update(dtSeconds: number): void;
|
|
98
|
+
/** Current combo count (processes any pending window expiry against the clock first). */
|
|
99
|
+
count(): number;
|
|
100
|
+
/** Highest count reached since the last {@link ComboMeter.reset}. */
|
|
101
|
+
peak(): number;
|
|
102
|
+
/** Active tier id, or `null` below the first tier. */
|
|
103
|
+
tier(): string | null;
|
|
104
|
+
/** Derived multiplier for the active tier (default 1). */
|
|
105
|
+
multiplier(): number;
|
|
106
|
+
/** Milliseconds left in the decay window before the next drop (0 when idle). */
|
|
107
|
+
remainingMs(): number;
|
|
108
|
+
/** Window remaining as `0..1`, for a draining ring/bar. */
|
|
109
|
+
fraction(): number;
|
|
110
|
+
/** The pooled view to draw this frame (do not retain). */
|
|
111
|
+
view(): ComboMeterView;
|
|
112
|
+
/** Clear the chain, peak, and window to empty. Notifies subscribers. */
|
|
113
|
+
reset(): void;
|
|
114
|
+
/** Patch config (window, tiers, drop step, multiplier curve) in place. Notifies subscribers. */
|
|
115
|
+
configure(patch: Partial<ComboMeterConfig>): void;
|
|
116
|
+
/** Observe changes (hit, drop, reset, configure, restore). Returns an unsubscribe fn. */
|
|
117
|
+
subscribe(listener: () => void): () => void;
|
|
118
|
+
/** Serializable state for a save. */
|
|
119
|
+
snapshot(): ComboMeterSnapshot;
|
|
120
|
+
/** Restore from a {@link ComboMeterSnapshot}, re-anchoring the window to the current clock. */
|
|
121
|
+
restore(snapshot: ComboMeterSnapshot): void;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A count-based combo / multiplier meter: an integer hit chain that climbs with
|
|
125
|
+
* every `hit()` and drops when a decay window elapses, crossing free-string tier
|
|
126
|
+
* thresholds ("good" → "great" → "savage") that derive a score multiplier, while
|
|
127
|
+
* tracking the peak reached. Time is injected (`now`, default `Date.now`) and/or
|
|
128
|
+
* driven by `update(dt)`; `view()` returns a pooled draw-state and
|
|
129
|
+
* `snapshot`/`restore` round-trips the chain through a save.
|
|
130
|
+
*
|
|
131
|
+
* Distinct from the existing meters — reused none of them because none is this
|
|
132
|
+
* shape: `stats/eventMeter` is a value/heat gauge that rises by tagged gains and
|
|
133
|
+
* decays continuously (not an integer chain count with a reset-on-hit window);
|
|
134
|
+
* `combat/comboPoints` is a finisher-point pool that hard-expires with no tiers,
|
|
135
|
+
* multiplier, or subscribe; `stats/accumulatorMeter` is a float gauge crossing
|
|
136
|
+
* `at`-value tiers, not a count with a draining window and derived multiplier.
|
|
137
|
+
*
|
|
138
|
+
* @capability combo-meter count-based combo/multiplier meter with a decay window, free-string tier thresholds, derived multiplier, peak tracking and snapshot/restore
|
|
139
|
+
*/
|
|
140
|
+
export declare function createComboMeter(config: ComboMeterConfig): ComboMeter;
|