@jgengine/core 0.11.0 → 0.12.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 +26 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat.d.ts +1 -0
- package/dist/combat.js +1 -0
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/playableGame.d.ts +34 -435
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +19 -0
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +24 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +130 -0
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +59 -0
- package/dist/runtime/context/scene.js +291 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +88 -501
- package/dist/runtime/gameContextTypes.d.ts +467 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +4 -2
- package/dist/runtime/headlessRunner.js +1 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +5 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +58 -0
- package/dist/scene/colliders.js +108 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +10 -14
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +59 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/features.d.ts +18 -3
- package/dist/world/features.js +10 -1
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/pathNetwork.d.ts +159 -0
- package/dist/world/pathNetwork.js +684 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/scatter.js +2 -20
- package/dist/world/streets.d.ts +29 -0
- package/dist/world/streets.js +55 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +45 -0
- package/dist/world/terrain.js +78 -0
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +5 -1
- package/skills/jgengine-combat/api.md +36 -0
- package/skills/jgengine-combat/capabilities.md +44 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/reference.md +12 -2
- package/skills/jgengine-gameplay/api.md +152 -26
- package/skills/jgengine-gameplay/capabilities.md +78 -2
- package/skills/jgengine-gameplay/reference-systems.md +1 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,32 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
13
|
the latest and surface the migration steps.
|
|
14
14
|
|
|
15
|
+
## 0.12.0
|
|
16
|
+
|
|
17
|
+
### Migrate
|
|
18
|
+
|
|
19
|
+
- **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
|
|
20
|
+
- **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
|
|
21
|
+
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
|
|
26
|
+
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
|
|
27
|
+
- **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
|
|
28
|
+
- **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
|
|
29
|
+
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
|
|
30
|
+
- **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
|
|
31
|
+
- **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
|
|
32
|
+
- **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
|
|
33
|
+
- **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
|
|
34
|
+
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
|
|
39
|
+
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
|
|
40
|
+
|
|
15
41
|
## 0.11.0
|
|
16
42
|
|
|
17
43
|
### Migrate
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { NavPoint } from "../nav/navGrid.js";
|
|
2
|
+
import { type RandomSeed } from "../random/rng.js";
|
|
3
|
+
/** One species that ambiently populates a region: how heavily it is favoured and its alive ceiling. */
|
|
4
|
+
export interface PopulationSpeciesEntry {
|
|
5
|
+
/** Opaque species/creature id the engine never interprets. */
|
|
6
|
+
species: string;
|
|
7
|
+
/** Relative pick weight among species below their cap (default 1). */
|
|
8
|
+
weight?: number;
|
|
9
|
+
/** Target/maximum number of this species alive in the region at once. */
|
|
10
|
+
cap: number;
|
|
11
|
+
}
|
|
12
|
+
/** Ambient population budget for a single region: its species ceilings, refill timing, and spawn geometry. */
|
|
13
|
+
export interface PopulationRegionConfig {
|
|
14
|
+
/** Opaque region id the engine never interprets. */
|
|
15
|
+
region: string;
|
|
16
|
+
species: readonly PopulationSpeciesEntry[];
|
|
17
|
+
/** Seconds a freed slot waits before it becomes a spawn candidate (overrides the director default). */
|
|
18
|
+
respawnDelay?: number;
|
|
19
|
+
/** Candidate spawn locations; when present each request is tagged with one. */
|
|
20
|
+
spawnPoints?: readonly NavPoint[];
|
|
21
|
+
}
|
|
22
|
+
/** Full ambient-population configuration: per-region species budgets plus global refill limits and PRNG seed. */
|
|
23
|
+
export interface PopulationDirectorConfig {
|
|
24
|
+
regions: readonly PopulationRegionConfig[];
|
|
25
|
+
/** Default respawn delay in game-seconds for regions without their own (default 0 — refill on the next tick). */
|
|
26
|
+
respawnDelay?: number;
|
|
27
|
+
/** Upper bound on spawn requests emitted per {@link PopulationDirector.tick} call (default 32). */
|
|
28
|
+
maxSpawnsPerTick?: number;
|
|
29
|
+
/** Seed for the persisted, serializable PRNG cursor (default 1). */
|
|
30
|
+
seed?: number;
|
|
31
|
+
}
|
|
32
|
+
/** A single ambient spawn the game should realise, refilling a region toward its species caps. */
|
|
33
|
+
export interface PopulationSpawnRequest {
|
|
34
|
+
region: string;
|
|
35
|
+
species: string;
|
|
36
|
+
/** Chosen spawn location when the region declares `spawnPoints`. */
|
|
37
|
+
point?: NavPoint;
|
|
38
|
+
}
|
|
39
|
+
/** Per-call spawn-point selection hook, letting a game apply bias (distance from players, etc.) at tick time. */
|
|
40
|
+
export interface PopulationTickContext {
|
|
41
|
+
/**
|
|
42
|
+
* Choose a spawn point for a request. Receives the region id, its configured candidates, and a
|
|
43
|
+
* deterministic roll in `[0, 1)` drawn from the persisted cursor. Return `null` to leave the
|
|
44
|
+
* request unplaced. When omitted, a candidate is picked uniformly from the roll.
|
|
45
|
+
*/
|
|
46
|
+
pickSpawnPoint?(region: string, candidates: readonly NavPoint[], roll: number): NavPoint | null;
|
|
47
|
+
}
|
|
48
|
+
/** Live census of alive counts keyed `region -> species -> count`, used to reconcile the director to reality. */
|
|
49
|
+
export type PopulationCensus = Record<string, Record<string, number>>;
|
|
50
|
+
/**
|
|
51
|
+
* A queued refill slot awaiting its {@link PopulationDirectorState.elapsed} deadline.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export interface PopulationRespawnSlot {
|
|
55
|
+
region: string;
|
|
56
|
+
readyAt: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Opaque, JSON-serializable persistence for a {@link PopulationDirector}. Round-trip it through
|
|
60
|
+
* `snapshot`/`hydrate`; never read or mutate the fields directly.
|
|
61
|
+
*/
|
|
62
|
+
export interface PopulationDirectorState {
|
|
63
|
+
/** Accumulated game-seconds. */
|
|
64
|
+
elapsed: number;
|
|
65
|
+
/** Believed alive counts, `region -> species -> count`. */
|
|
66
|
+
alive: Record<string, Record<string, number>>;
|
|
67
|
+
/** Pending refill slots, each ready at its `readyAt` game-second. */
|
|
68
|
+
queue: readonly PopulationRespawnSlot[];
|
|
69
|
+
/** Opaque PRNG cursor — round-trip as part of the state, never read or set it directly. */
|
|
70
|
+
rng: RandomSeed;
|
|
71
|
+
}
|
|
72
|
+
/** Stateful ambient population manager for one world; drive it with `tick` and lifecycle notifications. */
|
|
73
|
+
export interface PopulationDirector {
|
|
74
|
+
/**
|
|
75
|
+
* Advance by `dt` game-seconds and emit spawn requests for refill slots whose delay has elapsed,
|
|
76
|
+
* up to the per-tick cap. Only touches the bounded respawn queue — never scans the world.
|
|
77
|
+
*/
|
|
78
|
+
tick(dt: number, ctx?: PopulationTickContext): PopulationSpawnRequest[];
|
|
79
|
+
/** Register that a creature left the region (killed/despawned/tamed), freeing a slot to refill after the delay. */
|
|
80
|
+
notifyRemoved(region: string, species: string, id?: string): void;
|
|
81
|
+
/** Register a creature the game introduced itself (not via a request), so caps account for it. */
|
|
82
|
+
notifySpawned(region: string, species: string, id?: string): void;
|
|
83
|
+
/** Snap believed alive counts to a live census and re-queue any deficit toward the caps. */
|
|
84
|
+
reconcile(census: PopulationCensus): void;
|
|
85
|
+
/** Believed alive count for a `(region, species)` pair. */
|
|
86
|
+
alive(region: string, species: string): number;
|
|
87
|
+
/** Serializable copy of the full director state (deep, safe to persist). */
|
|
88
|
+
snapshot(): PopulationDirectorState;
|
|
89
|
+
/** Replace the director state from a previously taken (and possibly JSON round-tripped) snapshot. */
|
|
90
|
+
hydrate(state: PopulationDirectorState): void;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Ambient open-world population director. Holds a target creature population per region and refills
|
|
94
|
+
* it over time, distinct from wave/horde spawning: instead of a budget draining down a wave, each
|
|
95
|
+
* region has per-species caps and a timed respawn queue. Removing a creature frees a slot; after a
|
|
96
|
+
* respawn delay `tick` emits weighted spawn requests (bounded per tick) that refill toward the caps
|
|
97
|
+
* without ever exceeding them. Species are chosen through a persisted, serializable {@link RandomSeed}
|
|
98
|
+
* cursor, so the same seed and the same lifecycle events always yield the same requests. Regions are
|
|
99
|
+
* independent, spawn points are optional, and the whole state round-trips through `snapshot`/`hydrate`.
|
|
100
|
+
*
|
|
101
|
+
* @capability population-director ambient per-region creature population manager with timed weighted respawns and caps
|
|
102
|
+
*/
|
|
103
|
+
export declare function createPopulationDirector(config: PopulationDirectorConfig): PopulationDirector;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { randomSeedFrom, stepRandomSeed } from "../random/rng.js";
|
|
2
|
+
const DEFAULT_MAX_SPAWNS_PER_TICK = 32;
|
|
3
|
+
function clamp01(value) {
|
|
4
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
5
|
+
}
|
|
6
|
+
function cloneState(state) {
|
|
7
|
+
const alive = {};
|
|
8
|
+
for (const region of Object.keys(state.alive))
|
|
9
|
+
alive[region] = { ...state.alive[region] };
|
|
10
|
+
return {
|
|
11
|
+
elapsed: state.elapsed,
|
|
12
|
+
alive,
|
|
13
|
+
queue: state.queue.map((slot) => ({ region: slot.region, readyAt: slot.readyAt })),
|
|
14
|
+
rng: state.rng,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ambient open-world population director. Holds a target creature population per region and refills
|
|
19
|
+
* it over time, distinct from wave/horde spawning: instead of a budget draining down a wave, each
|
|
20
|
+
* region has per-species caps and a timed respawn queue. Removing a creature frees a slot; after a
|
|
21
|
+
* respawn delay `tick` emits weighted spawn requests (bounded per tick) that refill toward the caps
|
|
22
|
+
* without ever exceeding them. Species are chosen through a persisted, serializable {@link RandomSeed}
|
|
23
|
+
* cursor, so the same seed and the same lifecycle events always yield the same requests. Regions are
|
|
24
|
+
* independent, spawn points are optional, and the whole state round-trips through `snapshot`/`hydrate`.
|
|
25
|
+
*
|
|
26
|
+
* @capability population-director ambient per-region creature population manager with timed weighted respawns and caps
|
|
27
|
+
*/
|
|
28
|
+
export function createPopulationDirector(config) {
|
|
29
|
+
const regions = new Map();
|
|
30
|
+
for (const region of config.regions)
|
|
31
|
+
regions.set(region.region, region);
|
|
32
|
+
const maxSpawnsPerTick = config.maxSpawnsPerTick ?? DEFAULT_MAX_SPAWNS_PER_TICK;
|
|
33
|
+
const defaultDelay = config.respawnDelay ?? 0;
|
|
34
|
+
let state = {
|
|
35
|
+
elapsed: 0,
|
|
36
|
+
alive: {},
|
|
37
|
+
queue: [],
|
|
38
|
+
rng: randomSeedFrom(config.seed ?? 1),
|
|
39
|
+
};
|
|
40
|
+
const totalCap = (region) => {
|
|
41
|
+
let sum = 0;
|
|
42
|
+
for (const entry of region.species)
|
|
43
|
+
sum += entry.cap;
|
|
44
|
+
return sum;
|
|
45
|
+
};
|
|
46
|
+
const totalAlive = (regionId) => {
|
|
47
|
+
const counts = state.alive[regionId];
|
|
48
|
+
if (counts === undefined)
|
|
49
|
+
return 0;
|
|
50
|
+
let sum = 0;
|
|
51
|
+
for (const key of Object.keys(counts))
|
|
52
|
+
sum += counts[key];
|
|
53
|
+
return sum;
|
|
54
|
+
};
|
|
55
|
+
const queuedCount = (regionId) => {
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (const slot of state.queue)
|
|
58
|
+
if (slot.region === regionId)
|
|
59
|
+
sum += 1;
|
|
60
|
+
return sum;
|
|
61
|
+
};
|
|
62
|
+
const delayFor = (region) => region.respawnDelay ?? defaultDelay;
|
|
63
|
+
const aliveFor = (regionId) => {
|
|
64
|
+
let counts = state.alive[regionId];
|
|
65
|
+
if (counts === undefined) {
|
|
66
|
+
counts = {};
|
|
67
|
+
state.alive[regionId] = counts;
|
|
68
|
+
}
|
|
69
|
+
return counts;
|
|
70
|
+
};
|
|
71
|
+
/** Top up the queue so `alive + queued` reaches the region cap, appending ready-at slots. */
|
|
72
|
+
const ensureDeficitQueued = (region, readyAt) => {
|
|
73
|
+
const deficit = totalCap(region) - totalAlive(region.region) - queuedCount(region.region);
|
|
74
|
+
if (deficit <= 0)
|
|
75
|
+
return;
|
|
76
|
+
const next = state.queue.slice();
|
|
77
|
+
for (let i = 0; i < deficit; i += 1)
|
|
78
|
+
next.push({ region: region.region, readyAt });
|
|
79
|
+
state.queue = next;
|
|
80
|
+
};
|
|
81
|
+
// Prime each region to fill toward its caps on the first ticks.
|
|
82
|
+
for (const region of config.regions) {
|
|
83
|
+
aliveFor(region.region);
|
|
84
|
+
ensureDeficitQueued(region, 0);
|
|
85
|
+
}
|
|
86
|
+
const pickSpecies = (region) => {
|
|
87
|
+
const counts = state.alive[region.region] ?? {};
|
|
88
|
+
let total = 0;
|
|
89
|
+
const eligible = [];
|
|
90
|
+
for (const entry of region.species) {
|
|
91
|
+
if ((counts[entry.species] ?? 0) >= entry.cap)
|
|
92
|
+
continue;
|
|
93
|
+
eligible.push(entry);
|
|
94
|
+
total += entry.weight ?? 1;
|
|
95
|
+
}
|
|
96
|
+
if (eligible.length === 0 || total <= 0)
|
|
97
|
+
return null;
|
|
98
|
+
const [roll, nextRng] = stepRandomSeed(state.rng);
|
|
99
|
+
state.rng = nextRng;
|
|
100
|
+
let cursor = roll * total;
|
|
101
|
+
for (const entry of eligible) {
|
|
102
|
+
cursor -= entry.weight ?? 1;
|
|
103
|
+
if (cursor <= 0)
|
|
104
|
+
return entry.species;
|
|
105
|
+
}
|
|
106
|
+
return eligible[eligible.length - 1].species;
|
|
107
|
+
};
|
|
108
|
+
const choosePoint = (region, ctx) => {
|
|
109
|
+
const points = region.spawnPoints;
|
|
110
|
+
if (points === undefined || points.length === 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
const [roll, nextRng] = stepRandomSeed(state.rng);
|
|
113
|
+
state.rng = nextRng;
|
|
114
|
+
if (ctx?.pickSpawnPoint !== undefined) {
|
|
115
|
+
const chosen = ctx.pickSpawnPoint(region.region, points, roll);
|
|
116
|
+
return chosen ?? undefined;
|
|
117
|
+
}
|
|
118
|
+
return points[Math.min(points.length - 1, Math.floor(clamp01(roll) * points.length))];
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
tick(dt, ctx) {
|
|
122
|
+
if (dt <= 0)
|
|
123
|
+
return [];
|
|
124
|
+
state.elapsed += dt;
|
|
125
|
+
const now = state.elapsed;
|
|
126
|
+
const requests = [];
|
|
127
|
+
const remaining = [];
|
|
128
|
+
for (const slot of state.queue) {
|
|
129
|
+
if (requests.length >= maxSpawnsPerTick || slot.readyAt > now) {
|
|
130
|
+
remaining.push(slot);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const region = regions.get(slot.region);
|
|
134
|
+
if (region === undefined)
|
|
135
|
+
continue; // region no longer configured — drop the slot
|
|
136
|
+
const species = pickSpecies(region);
|
|
137
|
+
if (species === null)
|
|
138
|
+
continue; // region already at cap (filled externally) — drop the slot
|
|
139
|
+
aliveFor(region.region)[species] = (state.alive[region.region][species] ?? 0) + 1;
|
|
140
|
+
const request = { region: region.region, species };
|
|
141
|
+
const point = choosePoint(region, ctx);
|
|
142
|
+
if (point !== undefined)
|
|
143
|
+
request.point = point;
|
|
144
|
+
requests.push(request);
|
|
145
|
+
}
|
|
146
|
+
state.queue = remaining;
|
|
147
|
+
return requests;
|
|
148
|
+
},
|
|
149
|
+
notifyRemoved(region, species, _id) {
|
|
150
|
+
const counts = state.alive[region];
|
|
151
|
+
if (counts !== undefined) {
|
|
152
|
+
const current = counts[species] ?? 0;
|
|
153
|
+
if (current > 0)
|
|
154
|
+
counts[species] = current - 1;
|
|
155
|
+
}
|
|
156
|
+
const regionConfig = regions.get(region);
|
|
157
|
+
if (regionConfig === undefined)
|
|
158
|
+
return;
|
|
159
|
+
const next = state.queue.slice();
|
|
160
|
+
next.push({ region, readyAt: state.elapsed + delayFor(regionConfig) });
|
|
161
|
+
state.queue = next;
|
|
162
|
+
},
|
|
163
|
+
notifySpawned(region, species, _id) {
|
|
164
|
+
if (!regions.has(region))
|
|
165
|
+
return;
|
|
166
|
+
const counts = aliveFor(region);
|
|
167
|
+
counts[species] = (counts[species] ?? 0) + 1;
|
|
168
|
+
},
|
|
169
|
+
reconcile(census) {
|
|
170
|
+
for (const region of config.regions) {
|
|
171
|
+
const observed = census[region.region] ?? {};
|
|
172
|
+
const counts = {};
|
|
173
|
+
for (const entry of region.species)
|
|
174
|
+
counts[entry.species] = observed[entry.species] ?? 0;
|
|
175
|
+
state.alive[region.region] = counts;
|
|
176
|
+
}
|
|
177
|
+
for (const region of config.regions)
|
|
178
|
+
ensureDeficitQueued(region, state.elapsed);
|
|
179
|
+
},
|
|
180
|
+
alive(region, species) {
|
|
181
|
+
return state.alive[region]?.[species] ?? 0;
|
|
182
|
+
},
|
|
183
|
+
snapshot() {
|
|
184
|
+
return cloneState(state);
|
|
185
|
+
},
|
|
186
|
+
hydrate(next) {
|
|
187
|
+
state = cloneState(next);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable pursuit/aggro step: the "I have a target — now chase it, stop at reach, hit it on a
|
|
3
|
+
* cooldown, and break off if led too far from home" loop that melee mobs, RTS units, and stationary
|
|
4
|
+
* turrets all re-implement by hand. This owns exactly that state machine and the per-entity attack
|
|
5
|
+
* cooldown; target *selection* stays separate (see {@link ./targetAcquisition}), and every side
|
|
6
|
+
* effect — move, attack, break off — is performed by the caller in response to the returned action.
|
|
7
|
+
*
|
|
8
|
+
* Deterministic and allocation-free: distances are measured by the caller and passed in as scalars,
|
|
9
|
+
* the only state is one serializable number ({@link PursuitState.attackCooldown}) advanced in place,
|
|
10
|
+
* and the returned {@link PursuitAction} is a string literal. No object is built per entity per step,
|
|
11
|
+
* and nothing here scans the world — feed it a target you already resolved.
|
|
12
|
+
*/
|
|
13
|
+
/** The action a pursuit step resolves to; the caller performs the matching side effect. */
|
|
14
|
+
export type PursuitAction =
|
|
15
|
+
/** Home distance exceeded the leash — abandon the target and return home. */
|
|
16
|
+
"leash"
|
|
17
|
+
/** Out of range — advance toward the target. */
|
|
18
|
+
| "pursue"
|
|
19
|
+
/** In range and the cooldown is ready — perform the attack, then {@link armPursuit}. */
|
|
20
|
+
| "attack"
|
|
21
|
+
/** In range but the cooldown has not elapsed — hold (face the target, idle, etc.). */
|
|
22
|
+
| "wait"
|
|
23
|
+
/** No engageable target this step (distance was `null`). */
|
|
24
|
+
| "idle";
|
|
25
|
+
/**
|
|
26
|
+
* When the attack cooldown ticks down.
|
|
27
|
+
* - `"always"` (default): every step, like a wall-clock weapon timer that keeps counting during the
|
|
28
|
+
* chase, so the entity can swing the instant it reaches its target.
|
|
29
|
+
* - `"inRange"`: only while within `stopDistance`, so the timer freezes during the chase and the
|
|
30
|
+
* full interval is paid out after each contact.
|
|
31
|
+
*/
|
|
32
|
+
export type CooldownMode = "always" | "inRange";
|
|
33
|
+
/** Per-entity cooldown state owned by the primitive. One serializable number; no methods. */
|
|
34
|
+
export interface PursuitState {
|
|
35
|
+
/** Seconds remaining until the next attack may fire. Advanced toward `0` by {@link advancePursuit}. */
|
|
36
|
+
attackCooldown: number;
|
|
37
|
+
}
|
|
38
|
+
/** A fresh pursuit state, ready to attack immediately (`attackCooldown` at `0`). */
|
|
39
|
+
export declare function createPursuitState(attackCooldown?: number): PursuitState;
|
|
40
|
+
/** Set the cooldown after an attack fires (clamped at `0`). Call on the step that returned `"attack"`. */
|
|
41
|
+
export declare function armPursuit(state: PursuitState, intervalSeconds: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Advance one pursuit step for a single entity, mutating `state.attackCooldown`, and return the
|
|
44
|
+
* action the caller should perform. All spatial input is scalar and measured this frame:
|
|
45
|
+
*
|
|
46
|
+
* - `targetDistance` — self→target distance, or `null` when there is no engageable target (`"idle"`).
|
|
47
|
+
* - `stopDistance` — attack once within this distance; pursue while beyond it.
|
|
48
|
+
* - `homeDistance` / `leashRange` — optional leash checked first: when `homeDistance > leashRange`
|
|
49
|
+
* the step is `"leash"`. Leave at their defaults (`-Infinity` / `+Infinity`) to disable leashing.
|
|
50
|
+
* - `cooldownMode` — see {@link CooldownMode}.
|
|
51
|
+
*
|
|
52
|
+
* On `"attack"` the caller applies the hit and calls {@link armPursuit} with the next interval.
|
|
53
|
+
*
|
|
54
|
+
* @capability pursuit chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
|
|
55
|
+
*/
|
|
56
|
+
export declare function advancePursuit(state: PursuitState, dt: number, targetDistance: number | null, stopDistance: number, cooldownMode?: CooldownMode, homeDistance?: number, leashRange?: number): PursuitAction;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable pursuit/aggro step: the "I have a target — now chase it, stop at reach, hit it on a
|
|
3
|
+
* cooldown, and break off if led too far from home" loop that melee mobs, RTS units, and stationary
|
|
4
|
+
* turrets all re-implement by hand. This owns exactly that state machine and the per-entity attack
|
|
5
|
+
* cooldown; target *selection* stays separate (see {@link ./targetAcquisition}), and every side
|
|
6
|
+
* effect — move, attack, break off — is performed by the caller in response to the returned action.
|
|
7
|
+
*
|
|
8
|
+
* Deterministic and allocation-free: distances are measured by the caller and passed in as scalars,
|
|
9
|
+
* the only state is one serializable number ({@link PursuitState.attackCooldown}) advanced in place,
|
|
10
|
+
* and the returned {@link PursuitAction} is a string literal. No object is built per entity per step,
|
|
11
|
+
* and nothing here scans the world — feed it a target you already resolved.
|
|
12
|
+
*/
|
|
13
|
+
/** A fresh pursuit state, ready to attack immediately (`attackCooldown` at `0`). */
|
|
14
|
+
export function createPursuitState(attackCooldown = 0) {
|
|
15
|
+
return { attackCooldown };
|
|
16
|
+
}
|
|
17
|
+
/** Set the cooldown after an attack fires (clamped at `0`). Call on the step that returned `"attack"`. */
|
|
18
|
+
export function armPursuit(state, intervalSeconds) {
|
|
19
|
+
state.attackCooldown = intervalSeconds > 0 ? intervalSeconds : 0;
|
|
20
|
+
}
|
|
21
|
+
function tickCooldown(state, dt) {
|
|
22
|
+
if (state.attackCooldown > 0) {
|
|
23
|
+
const next = state.attackCooldown - dt;
|
|
24
|
+
state.attackCooldown = next > 0 ? next : 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Advance one pursuit step for a single entity, mutating `state.attackCooldown`, and return the
|
|
29
|
+
* action the caller should perform. All spatial input is scalar and measured this frame:
|
|
30
|
+
*
|
|
31
|
+
* - `targetDistance` — self→target distance, or `null` when there is no engageable target (`"idle"`).
|
|
32
|
+
* - `stopDistance` — attack once within this distance; pursue while beyond it.
|
|
33
|
+
* - `homeDistance` / `leashRange` — optional leash checked first: when `homeDistance > leashRange`
|
|
34
|
+
* the step is `"leash"`. Leave at their defaults (`-Infinity` / `+Infinity`) to disable leashing.
|
|
35
|
+
* - `cooldownMode` — see {@link CooldownMode}.
|
|
36
|
+
*
|
|
37
|
+
* On `"attack"` the caller applies the hit and calls {@link armPursuit} with the next interval.
|
|
38
|
+
*
|
|
39
|
+
* @capability pursuit chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
|
|
40
|
+
*/
|
|
41
|
+
export function advancePursuit(state, dt, targetDistance, stopDistance, cooldownMode = "always", homeDistance = Number.NEGATIVE_INFINITY, leashRange = Number.POSITIVE_INFINITY) {
|
|
42
|
+
if (cooldownMode === "always")
|
|
43
|
+
tickCooldown(state, dt);
|
|
44
|
+
if (homeDistance > leashRange)
|
|
45
|
+
return "leash";
|
|
46
|
+
if (targetDistance === null)
|
|
47
|
+
return "idle";
|
|
48
|
+
if (targetDistance > stopDistance)
|
|
49
|
+
return "pursue";
|
|
50
|
+
if (cooldownMode === "inRange")
|
|
51
|
+
tickCooldown(state, dt);
|
|
52
|
+
return state.attackCooldown <= 0 ? "attack" : "wait";
|
|
53
|
+
}
|
package/dist/authoring.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* advanced composition, but this module intentionally exposes common authored
|
|
6
6
|
* concepts without requiring callers to understand the repository layout.
|
|
7
7
|
*/
|
|
8
|
-
export {
|
|
8
|
+
export { type GameDefinition, type GameDefinitionConfig, type GameLoop, type InventoryDeclaration, type PhysicsConfig, type GameServerConfig, } from "./game/defineGame.js";
|
|
9
9
|
export { lootTable, type LootTableDef, type LootEntry, type Drop, } from "./game/lootTable.js";
|
|
10
10
|
export { biomes, voxel, plots, tilemap, flat, environment, terrain, rain, snow, grass, ocean, building, road, type WorldFeature, } from "./world/features.js";
|
|
11
11
|
export { seededRng, seededStreams } from "./random/rng.js";
|
package/dist/authoring.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* advanced composition, but this module intentionally exposes common authored
|
|
6
6
|
* concepts without requiring callers to understand the repository layout.
|
|
7
7
|
*/
|
|
8
|
-
export {
|
|
8
|
+
export {} from "./game/defineGame.js";
|
|
9
9
|
export { lootTable, } from "./game/lootTable.js";
|
|
10
10
|
export { biomes, voxel, plots, tilemap, flat, environment, terrain, rain, snow, grass, ocean, building, road, } from "./world/features.js";
|
|
11
11
|
export { seededRng, seededStreams } from "./random/rng.js";
|
package/dist/combat.d.ts
CHANGED
|
@@ -32,3 +32,4 @@ export { createAccumulatorMeter, tierAt, type MeterAddResult } from "./stats/acc
|
|
|
32
32
|
export { createEventMeter, type EventMeter, type EventMeterFeedResult } from "./stats/eventMeter.js";
|
|
33
33
|
export { rollCheck, type CheckAdvantage, type CheckResult } from "./stats/rollCheck.js";
|
|
34
34
|
export { createStats, type Stats } from "./stats/statModifiers.js";
|
|
35
|
+
export { applyStatPoolDelta, changeStatPool, createStatPool, patchStatPool, type StatPool, type StatPoolAccess, type StatPoolAccessResult, type StatPoolChange, type StatPoolInput, type StatPoolPatch, } from "./stats/statPool.js";
|
package/dist/combat.js
CHANGED
|
@@ -32,3 +32,4 @@ export { createAccumulatorMeter, tierAt } from "./stats/accumulatorMeter.js";
|
|
|
32
32
|
export { createEventMeter } from "./stats/eventMeter.js";
|
|
33
33
|
export { rollCheck } from "./stats/rollCheck.js";
|
|
34
34
|
export { createStats } from "./stats/statModifiers.js";
|
|
35
|
+
export { applyStatPoolDelta, changeStatPool, createStatPool, patchStatPool, } from "./stats/statPool.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ChangeSignal } from "../store/changeSignal.js";
|
|
2
|
+
import { type AngleUnit, type DevtoolsControlKind, type ResolvedAxisBounds, type TunableChoice, type TunableOptions } from "./tunableSchema.js";
|
|
3
|
+
import type { Devtools, Tunable } from "./types.js";
|
|
4
|
+
/** Internal mutable record backing a registered control: schema metadata, current value, and change listeners. */
|
|
5
|
+
export interface ControlRecord {
|
|
6
|
+
name: string;
|
|
7
|
+
kind: DevtoolsControlKind;
|
|
8
|
+
label: string;
|
|
9
|
+
group: string;
|
|
10
|
+
initial: unknown;
|
|
11
|
+
value: unknown;
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
step?: number;
|
|
15
|
+
options?: readonly unknown[];
|
|
16
|
+
choices?: readonly TunableChoice[];
|
|
17
|
+
unit?: AngleUnit;
|
|
18
|
+
displayUnit?: AngleUnit;
|
|
19
|
+
wrap?: boolean;
|
|
20
|
+
integer?: boolean;
|
|
21
|
+
axisBounds?: ResolvedAxisBounds;
|
|
22
|
+
hasAlpha?: boolean;
|
|
23
|
+
schemaVersion: number;
|
|
24
|
+
listeners: Set<(value: unknown) => void>;
|
|
25
|
+
}
|
|
26
|
+
/** @internal */
|
|
27
|
+
export declare function isStructuralKind(kind: DevtoolsControlKind): boolean;
|
|
28
|
+
/** Controls subsystem: the public controls facade plus internal register/write helpers and the record map. */
|
|
29
|
+
export interface ControlsModule {
|
|
30
|
+
controls: Devtools["controls"];
|
|
31
|
+
register: <T>(name: string, initial: T, options?: TunableOptions<T>) => Tunable<T>;
|
|
32
|
+
writeControl: (record: ControlRecord, raw: unknown) => boolean;
|
|
33
|
+
controlRecords: Map<string, ControlRecord>;
|
|
34
|
+
}
|
|
35
|
+
/** Create the controls subsystem that registers, validates, and mutates tunable controls, notifying on change. */
|
|
36
|
+
export declare const createControlsModule: (deps: {
|
|
37
|
+
signal: ChangeSignal;
|
|
38
|
+
}) => ControlsModule;
|