@jgengine/core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat/effects.d.ts +11 -4
  9. package/dist/combat/effects.js +24 -17
  10. package/dist/combat/magazine.d.ts +13 -0
  11. package/dist/combat/magazine.js +27 -0
  12. package/dist/combat/vfxPresets.d.ts +176 -0
  13. package/dist/combat/vfxPresets.js +74 -0
  14. package/dist/combat/weaponFire.d.ts +114 -0
  15. package/dist/combat/weaponFire.js +96 -0
  16. package/dist/combat.d.ts +5 -2
  17. package/dist/combat.js +4 -1
  18. package/dist/devtools/controls.d.ts +38 -0
  19. package/dist/devtools/controls.js +181 -0
  20. package/dist/devtools/devtools.d.ts +5 -197
  21. package/dist/devtools/devtools.js +38 -774
  22. package/dist/devtools/discover.d.ts +32 -0
  23. package/dist/devtools/discover.js +240 -0
  24. package/dist/devtools/fallbackSeams.d.ts +50 -0
  25. package/dist/devtools/fallbackSeams.js +111 -0
  26. package/dist/devtools/frame.d.ts +15 -0
  27. package/dist/devtools/frame.js +247 -0
  28. package/dist/devtools/logs.d.ts +15 -0
  29. package/dist/devtools/logs.js +86 -0
  30. package/dist/devtools/overrides.d.ts +16 -0
  31. package/dist/devtools/overrides.js +66 -0
  32. package/dist/devtools/types.d.ts +212 -0
  33. package/dist/devtools/types.js +1 -0
  34. package/dist/devtools/urlFlags.d.ts +22 -0
  35. package/dist/devtools/urlFlags.js +62 -0
  36. package/dist/economy/auctionBook.d.ts +122 -0
  37. package/dist/economy/auctionBook.js +213 -0
  38. package/dist/economy/priceHistory.d.ts +44 -0
  39. package/dist/economy/priceHistory.js +89 -0
  40. package/dist/economy/unlockPoints.d.ts +92 -0
  41. package/dist/economy/unlockPoints.js +103 -0
  42. package/dist/editor/commandApply.d.ts +5 -0
  43. package/dist/editor/commandApply.js +755 -0
  44. package/dist/editor/commands.d.ts +48 -3
  45. package/dist/editor/commands.js +40 -754
  46. package/dist/editor/directives.d.ts +94 -0
  47. package/dist/editor/directives.js +206 -0
  48. package/dist/editor/document.d.ts +37 -2
  49. package/dist/editor/document.js +422 -6
  50. package/dist/editor/entityCatalog.d.ts +42 -0
  51. package/dist/editor/entityCatalog.js +101 -0
  52. package/dist/editor/environment.d.ts +74 -0
  53. package/dist/editor/environment.js +169 -0
  54. package/dist/editor/index.d.ts +26 -1
  55. package/dist/editor/index.js +8 -1
  56. package/dist/editor/minimap.d.ts +31 -0
  57. package/dist/editor/minimap.js +73 -0
  58. package/dist/editor/streamer.d.ts +66 -0
  59. package/dist/editor/streamer.js +68 -0
  60. package/dist/editor/types.d.ts +143 -0
  61. package/dist/editor/world.d.ts +136 -0
  62. package/dist/editor/world.js +303 -0
  63. package/dist/game/breeding.d.ts +161 -0
  64. package/dist/game/breeding.js +181 -0
  65. package/dist/game/cameraConfig.d.ts +451 -0
  66. package/dist/game/cameraConfig.js +1 -0
  67. package/dist/game/clipRoles.d.ts +51 -0
  68. package/dist/game/clipRoles.js +142 -0
  69. package/dist/game/defineGame.d.ts +16 -9
  70. package/dist/game/defineGame.js +14 -6
  71. package/dist/game/defineSystem.d.ts +2 -0
  72. package/dist/game/defineSystem.js +2 -0
  73. package/dist/game/events.d.ts +28 -0
  74. package/dist/game/partAnimation.d.ts +78 -0
  75. package/dist/game/partAnimation.js +147 -0
  76. package/dist/game/playableGame.d.ts +72 -440
  77. package/dist/game/playableGame.js +1 -1
  78. package/dist/game/progression.d.ts +35 -0
  79. package/dist/game/progression.js +22 -1
  80. package/dist/game/quest.js +1 -3
  81. package/dist/game/tribe.d.ts +184 -0
  82. package/dist/game/tribe.js +432 -0
  83. package/dist/gameplay.d.ts +3 -1
  84. package/dist/gameplay.js +3 -1
  85. package/dist/inventory/encumbrance.d.ts +69 -0
  86. package/dist/inventory/encumbrance.js +68 -0
  87. package/dist/inventory/inventoryModel.d.ts +24 -8
  88. package/dist/inventory/inventoryModel.js +47 -8
  89. package/dist/item/durability.js +1 -3
  90. package/dist/meta/changelog.d.ts +1 -1
  91. package/dist/meta/changelog.js +48 -1
  92. package/dist/movement/movementModel.d.ts +15 -1
  93. package/dist/movement/movementModel.js +33 -15
  94. package/dist/movement/playerMovement.js +102 -2
  95. package/dist/multiplayer/combatSnapshot.js +13 -8
  96. package/dist/multiplayer/presenceModel.d.ts +2 -0
  97. package/dist/multiplayer/presenceModel.js +8 -0
  98. package/dist/physics/drivableVehicle.d.ts +2 -0
  99. package/dist/physics/drivableVehicle.js +1 -1
  100. package/dist/physics/flightDynamics.d.ts +82 -0
  101. package/dist/physics/flightDynamics.js +165 -0
  102. package/dist/physics/gravityField.d.ts +25 -0
  103. package/dist/physics/gravityField.js +41 -0
  104. package/dist/physics/kinematicVehicle.d.ts +87 -0
  105. package/dist/physics/kinematicVehicle.js +193 -11
  106. package/dist/physics/physicsWorld.d.ts +26 -0
  107. package/dist/physics/physicsWorld.js +109 -117
  108. package/dist/physics/vehicleObstacles.d.ts +44 -0
  109. package/dist/physics/vehicleObstacles.js +115 -0
  110. package/dist/random/rng.d.ts +4 -1
  111. package/dist/random/rng.js +4 -1
  112. package/dist/runtime/adapter.d.ts +13 -0
  113. package/dist/runtime/adapter.js +13 -0
  114. package/dist/runtime/context/combat.d.ts +51 -0
  115. package/dist/runtime/context/combat.js +131 -0
  116. package/dist/runtime/context/combatFx.js +15 -5
  117. package/dist/runtime/context/deathLoot.d.ts +30 -0
  118. package/dist/runtime/context/deathLoot.js +39 -0
  119. package/dist/runtime/context/player.d.ts +48 -0
  120. package/dist/runtime/context/player.js +125 -0
  121. package/dist/runtime/context/scene.d.ts +60 -0
  122. package/dist/runtime/context/scene.js +329 -0
  123. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  124. package/dist/runtime/context/snapshotCodecs.js +26 -0
  125. package/dist/runtime/descriptors/baseline.js +26 -3
  126. package/dist/runtime/descriptors/features.d.ts +5 -0
  127. package/dist/runtime/descriptors/features.js +20 -2
  128. package/dist/runtime/gameContext.d.ts +2 -428
  129. package/dist/runtime/gameContext.js +133 -500
  130. package/dist/runtime/gameContextTypes.d.ts +500 -0
  131. package/dist/runtime/gameContextTypes.js +1 -0
  132. package/dist/runtime/headlessRunner.d.ts +17 -2
  133. package/dist/runtime/headlessRunner.js +15 -0
  134. package/dist/runtime/hostPolicy.d.ts +51 -0
  135. package/dist/runtime/hostPolicy.js +68 -0
  136. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  137. package/dist/runtime/hostedGameRunner.js +1 -0
  138. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  139. package/dist/runtime/hostedWorldSession.js +1 -0
  140. package/dist/runtime/worldSnapshot.d.ts +13 -1
  141. package/dist/runtime/worldSnapshot.js +15 -3
  142. package/dist/scene/assetCatalog.d.ts +7 -0
  143. package/dist/scene/assetSpace.js +1 -3
  144. package/dist/scene/authoredTriggers.d.ts +50 -0
  145. package/dist/scene/authoredTriggers.js +114 -0
  146. package/dist/scene/builtinSceneKinds.js +8 -3
  147. package/dist/scene/colliders.d.ts +79 -0
  148. package/dist/scene/colliders.js +170 -0
  149. package/dist/scene/collisionMesh.d.ts +94 -0
  150. package/dist/scene/collisionMesh.js +699 -0
  151. package/dist/scene/companion.d.ts +147 -0
  152. package/dist/scene/companion.js +193 -0
  153. package/dist/scene/entityStats.d.ts +7 -9
  154. package/dist/scene/entityStats.js +19 -15
  155. package/dist/scene/sceneKinds.d.ts +16 -0
  156. package/dist/scene/sceneKinds.js +4 -0
  157. package/dist/scene/sceneRaycast.js +15 -0
  158. package/dist/stats/spawnLevelStats.d.ts +110 -0
  159. package/dist/stats/spawnLevelStats.js +140 -0
  160. package/dist/stats/statModifiers.d.ts +16 -0
  161. package/dist/stats/statModifiers.js +30 -1
  162. package/dist/stats/statPool.d.ts +70 -0
  163. package/dist/stats/statPool.js +62 -0
  164. package/dist/store/changeSignal.d.ts +3 -0
  165. package/dist/store/changeSignal.js +2 -0
  166. package/dist/store/observableKeyedStore.d.ts +2 -0
  167. package/dist/store/observableKeyedStore.js +1 -0
  168. package/dist/survival/decayMeter.js +1 -3
  169. package/dist/survival/regionHealth.js +1 -3
  170. package/dist/ui/hudLayout.d.ts +5 -0
  171. package/dist/ui/hudLayout.js +17 -1
  172. package/dist/ui/panelModel.d.ts +120 -0
  173. package/dist/ui/panelModel.js +168 -0
  174. package/dist/ui.d.ts +1 -0
  175. package/dist/ui.js +1 -0
  176. package/dist/world/authoredEntities.d.ts +29 -0
  177. package/dist/world/authoredEntities.js +29 -0
  178. package/dist/world/authoredObjects.d.ts +15 -1
  179. package/dist/world/authoredObjects.js +13 -1
  180. package/dist/world/authoredSpawn.d.ts +39 -0
  181. package/dist/world/authoredSpawn.js +37 -0
  182. package/dist/world/buildSockets.d.ts +152 -0
  183. package/dist/world/buildSockets.js +188 -0
  184. package/dist/world/buildingLots.d.ts +84 -0
  185. package/dist/world/buildingLots.js +119 -0
  186. package/dist/world/buildings.d.ts +6 -0
  187. package/dist/world/cityBlocks.d.ts +152 -0
  188. package/dist/world/cityBlocks.js +670 -0
  189. package/dist/world/cityContent.d.ts +77 -0
  190. package/dist/world/cityContent.js +261 -0
  191. package/dist/world/cityGenerator.d.ts +41 -0
  192. package/dist/world/cityGenerator.js +58 -0
  193. package/dist/world/cityGeometry.d.ts +114 -0
  194. package/dist/world/cityGeometry.js +573 -0
  195. package/dist/world/cityKind.d.ts +314 -0
  196. package/dist/world/cityKind.js +1578 -0
  197. package/dist/world/environmentSummary.js +57 -1
  198. package/dist/world/features.d.ts +53 -5
  199. package/dist/world/features.js +15 -3
  200. package/dist/world/grassKind.js +2 -2
  201. package/dist/world/markers.d.ts +50 -4
  202. package/dist/world/markers.js +49 -0
  203. package/dist/world/massing.js +1 -1
  204. package/dist/world/minimapBake.d.ts +78 -0
  205. package/dist/world/minimapBake.js +208 -0
  206. package/dist/world/minimapTrack.d.ts +17 -0
  207. package/dist/world/minimapTrack.js +22 -0
  208. package/dist/world/place.d.ts +166 -0
  209. package/dist/world/place.js +96 -0
  210. package/dist/world/placeAsset.js +3 -1
  211. package/dist/world/poleLineKind.d.ts +40 -0
  212. package/dist/world/poleLineKind.js +97 -0
  213. package/dist/world/polyline.js +9 -2
  214. package/dist/world/resourceNode.d.ts +109 -0
  215. package/dist/world/resourceNode.js +140 -0
  216. package/dist/world/roads.d.ts +19 -2
  217. package/dist/world/roads.js +60 -3
  218. package/dist/world/scatter.js +2 -20
  219. package/dist/world/streetGenerator.d.ts +159 -0
  220. package/dist/world/streetGenerator.js +684 -0
  221. package/dist/world/streets.d.ts +103 -1
  222. package/dist/world/streets.js +273 -0
  223. package/dist/world/terraform.js +58 -88
  224. package/dist/world/terrain.d.ts +65 -1
  225. package/dist/world/terrain.js +119 -2
  226. package/dist/world.d.ts +14 -5
  227. package/dist/world.js +12 -3
  228. package/package.json +1 -1
  229. package/skills/game-design/SKILL.md +28 -0
  230. package/skills/game-design/agents/openai.yaml +4 -0
  231. package/skills/game-design/references/application-playbook.md +170 -0
  232. package/skills/game-design/references/field-guide.md +288 -0
  233. package/skills/jgengine/SKILL.md +8 -4
  234. package/skills/jgengine/api.md +182 -81
  235. package/skills/jgengine/capabilities.md +104 -2
  236. package/skills/jgengine/recipes/minimal-game.md +88 -0
  237. package/skills/jgengine-combat/SKILL.md +19 -1
  238. package/skills/jgengine-combat/api.md +75 -2
  239. package/skills/jgengine-combat/capabilities.md +61 -1
  240. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  241. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  242. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  243. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  244. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  245. package/skills/jgengine-combat/reference.md +33 -4
  246. package/skills/jgengine-gameplay/SKILL.md +8 -0
  247. package/skills/jgengine-gameplay/api.md +182 -26
  248. package/skills/jgengine-gameplay/capabilities.md +96 -2
  249. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  250. package/skills/jgengine-gameplay/reference-systems.md +10 -5
  251. package/skills/jgengine-world/SKILL.md +13 -0
  252. package/skills/jgengine-world/api.md +453 -21
  253. package/skills/jgengine-world/capabilities.md +112 -2
  254. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  255. package/skills/jgengine-world/reference.md +43 -4
  256. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,147 @@
1
+ import type { MobVec3 } from "../ai/mobBrain.js";
2
+ import type { StatCatalog, StatValue, StatValueMap } from "./entityStats.js";
3
+ /**
4
+ * A tamed companion's standing order. Genre-agnostic aggression + posture preset:
5
+ * - `follow` — heel to the owner and assist whatever the owner is fighting.
6
+ * - `stay` — hold at `home` and assist the owner's target from there.
7
+ * - `passive` — heel to the owner and never engage (a pacifist escort).
8
+ * - `neutral` — heel to the owner and only fight back nearby threats (defensive).
9
+ * - `aggressive` — heel to the owner and proactively engage nearby threats and the owner's target.
10
+ */
11
+ export type CompanionCommand = "follow" | "stay" | "passive" | "neutral" | "aggressive";
12
+ /**
13
+ * Serializable per-companion state: who owns it, its standing order, where it holds, its
14
+ * leash radius, and its levelled stat pool. Plain data — JSON round-trips without loss.
15
+ */
16
+ export interface CompanionRecord {
17
+ id: string;
18
+ /** Owning user/entity id — the roster ownership tie. */
19
+ ownerId: string;
20
+ /** The {@link RosterEntry.id} this companion was tamed from, or `null` when free-standing. */
21
+ sourceId: string | null;
22
+ command: CompanionCommand;
23
+ /** Hold/return anchor used by `stay`; `null` until the owner parks it somewhere. */
24
+ home: MobVec3 | null;
25
+ /** Radius the game keeps the companion within of its resolved anchor (feeds a brain's leash distance). */
26
+ leash: number;
27
+ level: number;
28
+ /** Stat points earned from levels and not yet spent via {@link CompanionRoster.spend}. */
29
+ unspentPoints: number;
30
+ /** Levelled stat pool, composed over `scene/entityStats`. */
31
+ stats: StatValueMap;
32
+ }
33
+ /** Per-tick world context the game supplies so a command resolves into a concrete intent. */
34
+ export interface CompanionIntentContext {
35
+ /** Owner's current world position — the anchor for every non-`stay` mode; `null` when absent. */
36
+ ownerPosition?: MobVec3 | null;
37
+ /** The owner's current attack target — the companion assists/aggros it per its command. */
38
+ ownerTargetId?: string | null;
39
+ /** Threat ids near the companion the game offers as engage candidates (already proximity-filtered upstream). */
40
+ threats?: readonly string[];
41
+ }
42
+ /**
43
+ * The companion's decision for one tick, shaped for `ai/mobBrain`: feed `candidates` into
44
+ * `MobBrainDeps.candidates()`, treat `leashTo` as the brain's `home`, and pass `leashDistance`
45
+ * as `leashDistance`. `kind` summarises the posture for HUD/animation.
46
+ */
47
+ export interface CompanionIntent {
48
+ /** `follow`/`hold` when idle at the anchor; `engage` a fresh threat; `assist` the owner's target. */
49
+ kind: "follow" | "hold" | "engage" | "assist";
50
+ /** Whether the companion leashes to the owner (follow modes) or its `home` (stay). */
51
+ anchor: "owner" | "home";
52
+ /** Resolved anchor world point — owner position or `home`; `null` when that point is unknown. */
53
+ leashTo: MobVec3 | null;
54
+ /** Leash radius (copied from the record) the game passes to the brain / clamps movement against. */
55
+ leashDistance: number;
56
+ /** Threat ids to expose as `MobBrainDeps.candidates()`; empty when the command forbids engaging. */
57
+ candidates: readonly string[];
58
+ /** The primary threat to engage/assist this tick — `candidates[0]`; `null` while following/holding. */
59
+ targetId: string | null;
60
+ }
61
+ /** Rule for turning one spent level-point into a stat gain. Absent from config ⇒ the stat can't be upgraded. */
62
+ export interface CompanionStatUpgrade {
63
+ /** Amount a single point adds to the stat's `max` (and heals into `current`). */
64
+ increment: number;
65
+ /** Hard ceiling for the stat's `max`; spends that would exceed it are rejected. */
66
+ maxCap?: number;
67
+ }
68
+ /** Outcome of {@link CompanionRoster.spend}: the updated record + stat, or a rejection reason. */
69
+ export type CompanionSpendResult = {
70
+ status: "ok";
71
+ record: CompanionRecord;
72
+ stat: StatValue;
73
+ } | {
74
+ status: "rejected";
75
+ reason: string;
76
+ };
77
+ /** Per-companion configuration shared by every companion the roster mints. */
78
+ export interface CompanionConfig {
79
+ /** Stat catalog seeded into each new companion's pool (via `entityStats.seedStatValues`). */
80
+ stats?: StatCatalog;
81
+ /** Per-stat upgrade rules consulted by {@link CompanionRoster.spend}. */
82
+ upgrades?: Record<string, CompanionStatUpgrade>;
83
+ /** Points granted on reaching a given level; default `1` per level. Inject `game/progression` curves here. */
84
+ pointsPerLevel?: (level: number) => number;
85
+ /** Level ceiling; `levelUp` past it is a no-op. Default `Infinity`. */
86
+ maxLevel?: number;
87
+ /** Level every companion starts at; default `1`. */
88
+ startLevel?: number;
89
+ /** Default leash radius for new companions; default `12`. */
90
+ leash?: number;
91
+ }
92
+ /** Options for {@link CompanionRoster.adopt}. */
93
+ export interface CompanionAdoptOptions {
94
+ id?: string;
95
+ /** The {@link RosterEntry.id} this companion springs from — the ownership/catalog tie. */
96
+ sourceId?: string;
97
+ command?: CompanionCommand;
98
+ home?: MobVec3 | null;
99
+ leash?: number;
100
+ level?: number;
101
+ /** Override the config stat catalog for this one companion. */
102
+ stats?: StatCatalog;
103
+ }
104
+ /**
105
+ * A set of tamed companions keyed by companion id, scoped to owners. Wraps allegiance/command
106
+ * state, per-companion levelled stats, and intent resolution behind create/command/level/
107
+ * snapshot/hydrate — the counterpart of `scene/roster` for creatures that fight beside you.
108
+ */
109
+ export interface CompanionRoster {
110
+ /** Mint a companion for an owner (optionally tied to a {@link RosterEntry}). */
111
+ adopt(ownerId: string, options?: CompanionAdoptOptions): CompanionRecord;
112
+ release(companionId: string): boolean;
113
+ get(companionId: string): CompanionRecord | null;
114
+ ownerOf(companionId: string): string | null;
115
+ list(ownerId: string): readonly CompanionRecord[];
116
+ /** Set a companion's standing order; `null` if the id is unknown. */
117
+ command(companionId: string, command: CompanionCommand): CompanionRecord | null;
118
+ /** Park/clear a companion's `stay` anchor. */
119
+ setHome(companionId: string, home: MobVec3 | null): CompanionRecord | null;
120
+ setLeash(companionId: string, leash: number): CompanionRecord | null;
121
+ /** Gain `levels` levels (bounded by `maxLevel`), accruing points from `pointsPerLevel`. */
122
+ levelUp(companionId: string, levels?: number): CompanionRecord | null;
123
+ /** Spend one earned point into a stat per its {@link CompanionStatUpgrade} rule. */
124
+ spend(companionId: string, statId: string): CompanionSpendResult;
125
+ /** Resolve the companion's per-tick {@link CompanionIntent}; `null` if the id is unknown. */
126
+ resolveIntent(companionId: string, context: CompanionIntentContext): CompanionIntent | null;
127
+ /** Whole-store serializable copy — the save/replication seam. */
128
+ snapshot(): Record<string, CompanionRecord>;
129
+ hydrate(data: Record<string, CompanionRecord>): void;
130
+ }
131
+ /**
132
+ * Pure command → intent resolver. Maps a companion's standing order plus this tick's owner
133
+ * position, owner target, and nearby threats into the {@link CompanionIntent} a game feeds into
134
+ * `ai/mobBrain` (or executes directly). Deterministic and allocation-light — no import of the
135
+ * `@internal` brain factory; the two interoperate purely by shape.
136
+ *
137
+ * @capability companion-intent resolve a companion's command + threat context into a per-tick follow/hold/engage/assist intent
138
+ */
139
+ export declare function resolveCompanionIntent(record: CompanionRecord, context: CompanionIntentContext): CompanionIntent;
140
+ /**
141
+ * Build a {@link CompanionRoster}. Composes `scene/roster` ownership (via `ownerId`/`sourceId`),
142
+ * `scene/entityStats` for the per-companion stat pool, and {@link resolveCompanionIntent} for
143
+ * `ai/mobBrain`-shaped behavior — turning a captured creature into a loyal, levelling ally.
144
+ *
145
+ * @capability companion-roster manage tamed companions — allegiance, commands, per-companion stats/leveling, and mobBrain-shaped intent
146
+ */
147
+ export declare function createCompanionRoster(config?: CompanionConfig): CompanionRoster;
@@ -0,0 +1,193 @@
1
+ import { seedStatValues, setStatValue } from "./entityStats.js";
2
+ function dedupePush(out, id) {
3
+ if (!out.includes(id))
4
+ out.push(id);
5
+ }
6
+ function candidatesFor(command, ownerTargetId, threats) {
7
+ switch (command) {
8
+ case "passive":
9
+ return [];
10
+ case "follow":
11
+ case "stay":
12
+ return ownerTargetId === null ? [] : [ownerTargetId];
13
+ case "neutral":
14
+ return [...threats];
15
+ case "aggressive": {
16
+ const out = [];
17
+ for (const threat of threats)
18
+ dedupePush(out, threat);
19
+ if (ownerTargetId !== null)
20
+ dedupePush(out, ownerTargetId);
21
+ return out;
22
+ }
23
+ }
24
+ }
25
+ /**
26
+ * Pure command → intent resolver. Maps a companion's standing order plus this tick's owner
27
+ * position, owner target, and nearby threats into the {@link CompanionIntent} a game feeds into
28
+ * `ai/mobBrain` (or executes directly). Deterministic and allocation-light — no import of the
29
+ * `@internal` brain factory; the two interoperate purely by shape.
30
+ *
31
+ * @capability companion-intent resolve a companion's command + threat context into a per-tick follow/hold/engage/assist intent
32
+ */
33
+ export function resolveCompanionIntent(record, context) {
34
+ const ownerTargetId = context.ownerTargetId ?? null;
35
+ const threats = context.threats ?? [];
36
+ const anchor = record.command === "stay" ? "home" : "owner";
37
+ const leashTo = anchor === "home" ? record.home : (context.ownerPosition ?? null);
38
+ const candidates = candidatesFor(record.command, ownerTargetId, threats);
39
+ const targetId = candidates.length > 0 ? candidates[0] : null;
40
+ let kind;
41
+ if (targetId === null) {
42
+ kind = anchor === "home" ? "hold" : "follow";
43
+ }
44
+ else if (targetId === ownerTargetId) {
45
+ kind = "assist";
46
+ }
47
+ else {
48
+ kind = "engage";
49
+ }
50
+ return { kind, anchor, leashTo, leashDistance: record.leash, candidates, targetId };
51
+ }
52
+ function cloneStats(stats) {
53
+ const out = {};
54
+ for (const [statId, value] of Object.entries(stats))
55
+ out[statId] = { ...value };
56
+ return out;
57
+ }
58
+ function cloneRecord(record) {
59
+ return {
60
+ id: record.id,
61
+ ownerId: record.ownerId,
62
+ sourceId: record.sourceId,
63
+ command: record.command,
64
+ home: record.home === null ? null : [record.home[0], record.home[1], record.home[2]],
65
+ leash: record.leash,
66
+ level: record.level,
67
+ unspentPoints: record.unspentPoints,
68
+ stats: cloneStats(record.stats),
69
+ };
70
+ }
71
+ /**
72
+ * Build a {@link CompanionRoster}. Composes `scene/roster` ownership (via `ownerId`/`sourceId`),
73
+ * `scene/entityStats` for the per-companion stat pool, and {@link resolveCompanionIntent} for
74
+ * `ai/mobBrain`-shaped behavior — turning a captured creature into a loyal, levelling ally.
75
+ *
76
+ * @capability companion-roster manage tamed companions — allegiance, commands, per-companion stats/leveling, and mobBrain-shaped intent
77
+ */
78
+ export function createCompanionRoster(config = {}) {
79
+ const records = new Map();
80
+ const pointsPerLevel = config.pointsPerLevel ?? (() => 1);
81
+ const maxLevel = config.maxLevel ?? Number.POSITIVE_INFINITY;
82
+ const startLevel = config.startLevel ?? 1;
83
+ const defaultLeash = config.leash ?? 12;
84
+ let counter = 0;
85
+ const write = (record) => {
86
+ records.set(record.id, record);
87
+ return record;
88
+ };
89
+ const patch = (companionId, fn) => {
90
+ const record = records.get(companionId);
91
+ if (record === undefined)
92
+ return null;
93
+ return write(fn(record));
94
+ };
95
+ return {
96
+ adopt(ownerId, options = {}) {
97
+ counter += 1;
98
+ const id = options.id ?? `companion_${counter}`;
99
+ const seed = options.stats ?? config.stats ?? {};
100
+ const record = {
101
+ id,
102
+ ownerId,
103
+ sourceId: options.sourceId ?? null,
104
+ command: options.command ?? "follow",
105
+ home: options.home ?? null,
106
+ leash: options.leash ?? defaultLeash,
107
+ level: options.level ?? startLevel,
108
+ unspentPoints: 0,
109
+ stats: seedStatValues(seed),
110
+ };
111
+ return write(record);
112
+ },
113
+ release(companionId) {
114
+ return records.delete(companionId);
115
+ },
116
+ get(companionId) {
117
+ return records.get(companionId) ?? null;
118
+ },
119
+ ownerOf(companionId) {
120
+ return records.get(companionId)?.ownerId ?? null;
121
+ },
122
+ list(ownerId) {
123
+ const out = [];
124
+ for (const record of records.values())
125
+ if (record.ownerId === ownerId)
126
+ out.push(record);
127
+ return out;
128
+ },
129
+ command(companionId, command) {
130
+ return patch(companionId, (record) => ({ ...record, command }));
131
+ },
132
+ setHome(companionId, home) {
133
+ return patch(companionId, (record) => ({
134
+ ...record,
135
+ home: home === null ? null : [home[0], home[1], home[2]],
136
+ }));
137
+ },
138
+ setLeash(companionId, leash) {
139
+ return patch(companionId, (record) => ({ ...record, leash }));
140
+ },
141
+ levelUp(companionId, levels = 1) {
142
+ return patch(companionId, (record) => {
143
+ let level = record.level;
144
+ let points = record.unspentPoints;
145
+ for (let i = 0; i < levels && level < maxLevel; i += 1) {
146
+ level += 1;
147
+ points += pointsPerLevel(level);
148
+ }
149
+ return { ...record, level, unspentPoints: points };
150
+ });
151
+ },
152
+ spend(companionId, statId) {
153
+ const record = records.get(companionId);
154
+ if (record === undefined)
155
+ return { status: "rejected", reason: `unknown companion "${companionId}"` };
156
+ if (record.unspentPoints <= 0)
157
+ return { status: "rejected", reason: "no unspent points" };
158
+ const upgrade = config.upgrades?.[statId];
159
+ if (upgrade === undefined)
160
+ return { status: "rejected", reason: `stat "${statId}" is not upgradable` };
161
+ const existing = record.stats[statId];
162
+ if (existing === undefined)
163
+ return { status: "rejected", reason: `unknown stat "${statId}"` };
164
+ const nextMax = existing.max + upgrade.increment;
165
+ if (upgrade.maxCap !== undefined && nextMax > upgrade.maxCap) {
166
+ return { status: "rejected", reason: `stat "${statId}" is at its cap` };
167
+ }
168
+ const stats = setStatValue(record.stats, statId, {
169
+ max: nextMax,
170
+ current: existing.current + upgrade.increment,
171
+ });
172
+ const updated = write({ ...record, unspentPoints: record.unspentPoints - 1, stats });
173
+ return { status: "ok", record: updated, stat: updated.stats[statId] };
174
+ },
175
+ resolveIntent(companionId, context) {
176
+ const record = records.get(companionId);
177
+ if (record === undefined)
178
+ return null;
179
+ return resolveCompanionIntent(record, context);
180
+ },
181
+ snapshot() {
182
+ const out = {};
183
+ for (const [id, record] of records)
184
+ out[id] = cloneRecord(record);
185
+ return out;
186
+ },
187
+ hydrate(data) {
188
+ records.clear();
189
+ for (const [id, record] of Object.entries(data))
190
+ records.set(id, cloneRecord(record));
191
+ },
192
+ };
193
+ }
@@ -1,12 +1,9 @@
1
- export interface StatValue {
2
- current: number;
3
- max: number;
4
- min: number;
1
+ import { type StatPool, type StatPoolAccess, type StatPoolPatch } from "../stats/statPool.js";
2
+ /** Native entity-stat name retained as a compatibility bridge to the portable pool model. */
3
+ export interface StatValue extends StatPool {
5
4
  }
6
- export interface StatValuePatch {
7
- current?: number;
8
- max?: number;
9
- min?: number;
5
+ /** Native entity-stat patch retained as a compatibility bridge. */
6
+ export interface StatValuePatch extends StatPoolPatch {
10
7
  }
11
8
  export type StatValueMap = Record<string, StatValue>;
12
9
  export type StatCatalog = Record<string, {
@@ -40,7 +37,8 @@ export declare function snapshotEntityStats(store: ReadonlyMap<string, StatValue
40
37
  * @internal
41
38
  */
42
39
  export declare function hydrateEntityStats(store: Map<string, StatValueMap>, data: Record<string, StatValueMap>): void;
43
- export interface EntityStatsApi {
40
+ /** Native entity-stat adapter over the portable {@link StatPoolAccess} contract. */
41
+ export interface EntityStatsApi extends StatPoolAccess {
44
42
  get(instanceId: string, statId: string): StatValue | null;
45
43
  set(instanceId: string, statId: string, patch: StatValuePatch): boolean;
46
44
  delta(instanceId: string, statId: string, amount: number): null | {
@@ -1,6 +1,4 @@
1
- function clamp(value, min, max) {
2
- return Math.min(max, Math.max(min, value));
3
- }
1
+ import { changeStatPool, createStatPool, patchStatPool, } from "../stats/statPool.js";
4
2
  /** @internal */
5
3
  export function getStatValue(map, statId) {
6
4
  return map[statId] ?? null;
@@ -8,10 +6,10 @@ export function getStatValue(map, statId) {
8
6
  /** @internal */
9
7
  export function setStatValue(map, statId, patch) {
10
8
  const existing = map[statId];
11
- const max = patch.max ?? existing?.max ?? 0;
12
- const min = patch.min ?? existing?.min ?? 0;
13
- const current = clamp(patch.current ?? existing?.current ?? max, min, max);
14
- return { ...map, [statId]: { current, max, min } };
9
+ const next = existing === undefined
10
+ ? createStatPool({ max: patch.max ?? 0, min: patch.min, current: patch.current })
11
+ : patchStatPool(existing, patch);
12
+ return { ...map, [statId]: next };
15
13
  }
16
14
  /** @internal */
17
15
  export function applyPoolDelta(map, statId, amount) {
@@ -19,23 +17,26 @@ export function applyPoolDelta(map, statId, amount) {
19
17
  if (existing === undefined) {
20
18
  return { status: "rejected", reason: `unknown stat "${statId}"` };
21
19
  }
22
- const current = clamp(existing.current + amount, existing.min, existing.max);
23
- const stat = { ...existing, current };
20
+ const change = changeStatPool(existing, amount);
21
+ const stat = change.pool;
22
+ // No-op change: `changeStatPool` handed back the same pool reference, so keep
23
+ // the existing map too — callers can guard on reference identity.
24
+ if (stat === existing) {
25
+ return { status: "ok", map, stat, hitMin: change.hitMin, hitMax: change.hitMax };
26
+ }
24
27
  return {
25
28
  status: "ok",
26
29
  map: { ...map, [statId]: stat },
27
30
  stat,
28
- hitMin: current === existing.min,
29
- hitMax: current === existing.max,
31
+ hitMin: change.hitMin,
32
+ hitMax: change.hitMax,
30
33
  };
31
34
  }
32
35
  /** @internal */
33
36
  export function seedStatValues(catalogStats) {
34
37
  const map = {};
35
38
  for (const [statId, declaration] of Object.entries(catalogStats)) {
36
- const min = declaration.min ?? 0;
37
- const current = clamp(declaration.current ?? declaration.max, min, declaration.max);
38
- map[statId] = { current, max: declaration.max, min };
39
+ map[statId] = createStatPool(declaration);
39
40
  }
40
41
  return map;
41
42
  }
@@ -85,10 +86,13 @@ export function createEntityStatsApi(resolve) {
85
86
  const map = resolve(instanceId);
86
87
  if (map === undefined)
87
88
  return { reason: `unknown instance "${instanceId}"` };
89
+ const existing = map[statId];
88
90
  const result = applyPoolDelta(map, statId, amount);
89
91
  if (result.status === "rejected")
90
92
  return { reason: result.reason };
91
- map[statId] = result.stat;
93
+ // Skip the store write when nothing moved so reference guards stay stable.
94
+ if (result.stat !== existing)
95
+ map[statId] = result.stat;
92
96
  return null;
93
97
  },
94
98
  };
@@ -121,12 +121,28 @@ export interface ParamGroup {
121
121
  /** Start collapsed. Default expanded. */
122
122
  collapsed?: boolean;
123
123
  }
124
+ /**
125
+ * A named slider/weight bundle for a kind — a saved set of field values the inspector (or the
126
+ * `apply_preset` verb) writes into an object's `meta` in one patch, ready to tweak afterwards.
127
+ * Presets are plain data on the schema, so kinds ship archetypes and games can override the
128
+ * registration with their own.
129
+ */
130
+ export interface ParamPreset {
131
+ id: string;
132
+ label?: string;
133
+ /** Field values the preset writes; fields not listed keep their current values. */
134
+ values: Readonly<Record<string, unknown>>;
135
+ }
124
136
  /** A kind's full parameter surface: an ordered list of fields the inspector renders top-to-bottom. */
125
137
  export interface ParamSchema {
126
138
  fields: readonly ParamField[];
127
139
  /** Optional named sections; fields reference one by `group`. Ungrouped fields render first, headerless. */
128
140
  groups?: readonly ParamGroup[];
141
+ /** Optional named field-value bundles the inspector offers as one-click applies. */
142
+ presets?: readonly ParamPreset[];
129
143
  }
144
+ /** Look up a schema preset by id, or undefined — the seam `apply_preset` and games resolve bundles through. */
145
+ export declare function findSchemaPreset(schema: ParamSchema, id: string): ParamPreset | undefined;
130
146
  /** Parsed params after `parseParams`: every schema field present with a validated, defaulted value. */
131
147
  export type ParsedParams = Record<string, number | boolean | string | WeightedParamEntry[]>;
132
148
  /**
@@ -5,6 +5,10 @@
5
5
  * party ship a new authorable "studio" (pole line, water, bookcase) — schema + resolver here, a
6
6
  * matching renderer in `shell` — without editing editor or engine files. Scatter is the proof adopter.
7
7
  */
8
+ /** Look up a schema preset by id, or undefined — the seam `apply_preset` and games resolve bundles through. */
9
+ export function findSchemaPreset(schema, id) {
10
+ return schema.presets?.find((preset) => preset.id === id);
11
+ }
8
12
  function num(value, fallback, min, max) {
9
13
  let n = typeof value === "number" && Number.isFinite(value) ? value : fallback;
10
14
  if (min !== undefined)
@@ -1,4 +1,5 @@
1
1
  import { colliderBounds, colliderWorldCenter, defaultEntityColliders, defaultObjectColliders, resolveColliders, } from "./colliders.js";
2
+ import { raycastCollisionMesh } from "./collisionMesh.js";
2
3
  import { intersectAabb, normalizeDirection } from "./objectQuery.js";
3
4
  function excluded(exclude, id) {
4
5
  if (exclude === undefined)
@@ -54,6 +55,20 @@ function rayHitsCollider(origin, direction, maxDistance, collider, position, rot
54
55
  ],
55
56
  };
56
57
  }
58
+ if (collider.shape.kind === "mesh") {
59
+ const hit = raycastCollisionMesh(collider.shape.mesh, origin, direction, maxDistance, position, rotationY, collider.shape.meshScale, collider.shape.meshTranslate);
60
+ if (hit === null)
61
+ return null;
62
+ return {
63
+ distance: hit.distance,
64
+ normal: hit.normal,
65
+ point: [
66
+ origin[0] + direction[0] * hit.distance,
67
+ origin[1] + direction[1] * hit.distance,
68
+ origin[2] + direction[2] * hit.distance,
69
+ ],
70
+ };
71
+ }
57
72
  const bounds = colliderBounds(collider, position, rotationY);
58
73
  const hit = intersectAabb(origin, direction, bounds.min, bounds.max, maxDistance);
59
74
  if (hit === null)
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Per-stat growth definition for one species. `base` is the level-1 value of the stat; `growth`
3
+ * is how much a single distributed point adds, expressed as a fraction of `base` — so a point on a
4
+ * `growth: 0.1` stat is worth +10% of base. The resolved value is `base * (1 + points * growth)`.
5
+ */
6
+ export interface SpawnStatDef {
7
+ base: number;
8
+ growth: number;
9
+ }
10
+ /**
11
+ * A species' stat table: every stat the creature owns keyed to its {@link SpawnStatDef}. The key
12
+ * order is the deterministic order random points are distributed over, so a serialized species
13
+ * definition reproduces the same rolls under the same rng sequence.
14
+ */
15
+ export type SpawnSpeciesDef<TStat extends string> = Record<TStat, SpawnStatDef>;
16
+ /**
17
+ * A single creature's serializable stat record — a reference to its species (`speciesId`), the
18
+ * current `level`, the per-stat count of distributed points, and how many post-capture domestic
19
+ * levels have been spent. Plain data: round-trips through JSON, save/load, and multiplayer sync.
20
+ * Resolve display values with {@link resolveSpawnStats}.
21
+ */
22
+ export interface CreatureStatInstance<TStat extends string> {
23
+ speciesId: string;
24
+ level: number;
25
+ points: Record<TStat, number>;
26
+ domesticLevels: number;
27
+ }
28
+ /**
29
+ * Outcome of {@link applyCaptureEffectiveness}: the granted `bonusLevels`, the `tamedLevel`
30
+ * (wild level plus bonus levels), and the post-capture {@link CreatureStatInstance}.
31
+ * @internal
32
+ */
33
+ export interface CaptureResult<TStat extends string> {
34
+ bonusLevels: number;
35
+ tamedLevel: number;
36
+ instance: CreatureStatInstance<TStat>;
37
+ }
38
+ /**
39
+ * Options for {@link spendDomesticLevel}. `maxDomesticLevels` caps how many post-capture levels a
40
+ * creature may spend; once reached, further spends are refused.
41
+ * @internal
42
+ */
43
+ export interface SpendLevelOptions {
44
+ maxDomesticLevels?: number;
45
+ }
46
+ /**
47
+ * Result of {@link spendDomesticLevel}: the resulting instance and whether the level was applied
48
+ * (`spent: false` when the domestic-level cap blocked it, leaving the instance untouched).
49
+ * @internal
50
+ */
51
+ export interface SpendLevelResult<TStat extends string> {
52
+ instance: CreatureStatInstance<TStat>;
53
+ spent: boolean;
54
+ }
55
+ /**
56
+ * Roll a fresh wild creature at `level`: distribute `(level - 1)` points randomly across the
57
+ * species' stats using the injected rng and return a serializable {@link CreatureStatInstance}.
58
+ * Deterministic — the same rng sequence yields the same point spread. Levels below 1 clamp to 1
59
+ * (no points). Use `seededRng` from `../random/rng` for a persisted, reproducible stream.
60
+ *
61
+ * @capability spawn-level-roll distribute a wild creature's spawn level into random per-stat points
62
+ */
63
+ export declare function rollSpawnInstance<TStat extends string>(speciesId: string, species: SpawnSpeciesDef<TStat>, level: number, rng: () => number): CreatureStatInstance<TStat>;
64
+ /**
65
+ * Resolve an instance's live stat values from its distributed points:
66
+ * `value = base * (1 + points * growth)` per stat. Stats with zero points return their base.
67
+ *
68
+ * @capability spawn-stat-values resolve a creature's per-stat values from its distributed points
69
+ */
70
+ export declare function resolveSpawnStats<TStat extends string>(species: SpawnSpeciesDef<TStat>, instance: CreatureStatInstance<TStat>): Record<TStat, number>;
71
+ /**
72
+ * Bonus levels granted by a capture at the given effectiveness:
73
+ * `floor(level * clamp01(effectiveness) / 2)`. A perfect (1.0) capture grants half the wild
74
+ * level; a botched (0) capture grants none. Effectiveness outside `[0,1]` clamps.
75
+ *
76
+ * @capability capture-bonus-levels convert wild level and capture effectiveness into bonus levels
77
+ */
78
+ export declare function captureBonusLevels(level: number, effectiveness: number): number;
79
+ /**
80
+ * Apply a capture at the given effectiveness: compute bonus levels via {@link captureBonusLevels},
81
+ * distribute those extra points randomly with the injected rng, and return the post-capture
82
+ * instance (level raised to the tamed total) alongside the granted bonus levels and tamed level.
83
+ * The input instance is not mutated.
84
+ *
85
+ * @capability capture-effectiveness-tame apply capture effectiveness to grant and distribute bonus levels
86
+ */
87
+ export declare function applyCaptureEffectiveness<TStat extends string>(species: SpawnSpeciesDef<TStat>, instance: CreatureStatInstance<TStat>, effectiveness: number, rng: () => number): CaptureResult<TStat>;
88
+ /**
89
+ * Spend one earned post-capture level into `stat`, adding a single growth increment (one point)
90
+ * and raising both `level` and `domesticLevels`. When `maxDomesticLevels` is set and already
91
+ * reached, the spend is refused (`spent: false`) and the instance is returned untouched. Pure:
92
+ * returns a new instance rather than mutating the input.
93
+ *
94
+ * @capability domestic-level-spend spend an earned post-capture level into a chosen stat under a cap
95
+ */
96
+ export declare function spendDomesticLevel<TStat extends string>(instance: CreatureStatInstance<TStat>, stat: TStat, options?: SpendLevelOptions): SpendLevelResult<TStat>;
97
+ /**
98
+ * Deep-copy an instance into a detached, serializable record — the transport/save counterpart of
99
+ * {@link restoreSpawnInstance}. Copies the points map so later edits don't leak into the snapshot.
100
+ *
101
+ * @capability spawn-instance-snapshot deep-copy a creature stat instance into a serializable record
102
+ */
103
+ export declare function snapshotSpawnInstance<TStat extends string>(instance: CreatureStatInstance<TStat>): CreatureStatInstance<TStat>;
104
+ /**
105
+ * Rehydrate a creature stat instance from decoded data (e.g. `JSON.parse` output), returning a
106
+ * fresh deep copy the caller owns. Inverse of {@link snapshotSpawnInstance}.
107
+ *
108
+ * @capability spawn-instance-restore rehydrate a creature stat instance from decoded snapshot data
109
+ */
110
+ export declare function restoreSpawnInstance<TStat extends string>(data: CreatureStatInstance<TStat>): CreatureStatInstance<TStat>;