@jgengine/core 0.14.0 → 0.15.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 (214) hide show
  1. package/CHANGELOG.md +500 -1
  2. package/dist/ai/difficulty.d.ts +154 -0
  3. package/dist/ai/difficulty.js +194 -0
  4. package/dist/ai/driver.d.ts +107 -0
  5. package/dist/ai/driver.js +175 -0
  6. package/dist/ai/flock.js +13 -23
  7. package/dist/ai/laneSelect.js +2 -1
  8. package/dist/ai/mobBrain.js +2 -1
  9. package/dist/ai/waveRunner.d.ts +81 -0
  10. package/dist/ai/waveRunner.js +104 -0
  11. package/dist/audio/musicState.d.ts +47 -0
  12. package/dist/audio/musicState.js +38 -0
  13. package/dist/audio/stepCadence.d.ts +47 -0
  14. package/dist/audio/stepCadence.js +29 -0
  15. package/dist/audio/synth.d.ts +12 -0
  16. package/dist/authoring.d.ts +6 -4
  17. package/dist/authoring.js +6 -4
  18. package/dist/combat/castRunner.d.ts +13 -1
  19. package/dist/combat/castRunner.js +5 -1
  20. package/dist/combat/comboMeter.d.ts +140 -0
  21. package/dist/combat/comboMeter.js +215 -0
  22. package/dist/combat/comboPoints.d.ts +11 -0
  23. package/dist/combat/comboPoints.js +11 -2
  24. package/dist/combat/damageResolution.d.ts +1 -1
  25. package/dist/combat/defensiveWindow.d.ts +5 -0
  26. package/dist/combat/effects.d.ts +24 -0
  27. package/dist/combat/effects.js +6 -1
  28. package/dist/combat/projectiles.d.ts +6 -0
  29. package/dist/combat/projectiles.js +2 -2
  30. package/dist/combat/regenShield.d.ts +40 -2
  31. package/dist/combat/regenShield.js +66 -18
  32. package/dist/combat/resourcePool.d.ts +19 -0
  33. package/dist/combat/resourcePool.js +20 -2
  34. package/dist/combat/shotOrigin.d.ts +5 -0
  35. package/dist/combat/statusApplication.d.ts +1 -1
  36. package/dist/combat/statusApplication.js +2 -1
  37. package/dist/combat/statusEffectView.d.ts +56 -0
  38. package/dist/combat/statusEffectView.js +52 -0
  39. package/dist/combat.d.ts +4 -2
  40. package/dist/combat.js +2 -0
  41. package/dist/devtools/fallbackSeams.d.ts +3 -2
  42. package/dist/devtools/fallbackSeams.js +1 -1
  43. package/dist/devtools/textureErrors.d.ts +40 -0
  44. package/dist/devtools/textureErrors.js +68 -0
  45. package/dist/economy/shopStock.d.ts +110 -0
  46. package/dist/economy/shopStock.js +157 -0
  47. package/dist/economy/wallet.d.ts +6 -0
  48. package/dist/economy/wallet.js +1 -0
  49. package/dist/game/achievements.d.ts +80 -0
  50. package/dist/game/achievements.js +124 -0
  51. package/dist/game/cameraConfig.d.ts +7 -0
  52. package/dist/game/codex.d.ts +60 -0
  53. package/dist/game/codex.js +89 -0
  54. package/dist/game/credits.d.ts +36 -0
  55. package/dist/game/credits.js +0 -0
  56. package/dist/game/defineGame.d.ts +28 -3
  57. package/dist/game/defineGame.js +5 -0
  58. package/dist/game/dialogueGraph.d.ts +116 -0
  59. package/dist/game/dialogueGraph.js +103 -0
  60. package/dist/game/eventTicker.d.ts +95 -0
  61. package/dist/game/eventTicker.js +91 -0
  62. package/dist/game/events.d.ts +24 -5
  63. package/dist/game/events.js +18 -0
  64. package/dist/game/gamePhase.d.ts +1 -1
  65. package/dist/game/gamePhase.js +1 -1
  66. package/dist/game/leaderboardRank.d.ts +71 -0
  67. package/dist/game/leaderboardRank.js +76 -0
  68. package/dist/game/lootTable.d.ts +10 -1
  69. package/dist/game/lootTable.js +4 -2
  70. package/dist/game/notifications.d.ts +57 -0
  71. package/dist/game/notifications.js +107 -0
  72. package/dist/game/playableGame.d.ts +39 -2
  73. package/dist/game/quest.d.ts +26 -0
  74. package/dist/game/quest.js +31 -0
  75. package/dist/game/runDraft.js +2 -1
  76. package/dist/game/saveSlots.d.ts +102 -0
  77. package/dist/game/saveSlots.js +136 -0
  78. package/dist/game/talentTreeView.d.ts +106 -0
  79. package/dist/game/talentTreeView.js +114 -0
  80. package/dist/game/worldItem.js +2 -1
  81. package/dist/gameplay.d.ts +15 -4
  82. package/dist/gameplay.js +13 -2
  83. package/dist/i18n/i18n.d.ts +51 -0
  84. package/dist/i18n/i18n.js +82 -0
  85. package/dist/input/axisInput.d.ts +1 -1
  86. package/dist/input/axisInput.js +18 -2
  87. package/dist/input/rebindSession.d.ts +141 -0
  88. package/dist/input/rebindSession.js +235 -0
  89. package/dist/input/touchControlsMode.d.ts +16 -0
  90. package/dist/input/touchControlsMode.js +23 -0
  91. package/dist/input/touchScheme.d.ts +30 -1
  92. package/dist/input/touchScheme.js +31 -1
  93. package/dist/interaction/promptRegistry.d.ts +78 -0
  94. package/dist/interaction/promptRegistry.js +130 -0
  95. package/dist/inventory/storageTier.js +3 -2
  96. package/dist/meta/changelog.d.ts +1 -1
  97. package/dist/meta/changelog.js +145 -1
  98. package/dist/movement/movementModel.d.ts +26 -3
  99. package/dist/movement/movementModel.js +121 -15
  100. package/dist/movement/playerMovement.js +161 -25
  101. package/dist/physics/drivableVehicle.js +3 -1
  102. package/dist/physics/kinematicVehicle.d.ts +56 -1
  103. package/dist/physics/kinematicVehicle.js +99 -10
  104. package/dist/physics/vehicleObstacles.js +19 -0
  105. package/dist/random/resolveRng.d.ts +14 -0
  106. package/dist/random/resolveRng.js +31 -0
  107. package/dist/render/postProcessing.d.ts +12 -1
  108. package/dist/runtime/cameraDirector.d.ts +2 -2
  109. package/dist/runtime/context/combat.js +8 -0
  110. package/dist/runtime/gameContext.js +6 -1
  111. package/dist/runtime/gameContextTypes.d.ts +18 -0
  112. package/dist/runtime/inputSnapshot.d.ts +10 -1
  113. package/dist/runtime/inputSnapshot.js +13 -1
  114. package/dist/runtime/runtimeSave.d.ts +3 -3
  115. package/dist/runtime/runtimeSave.js +11 -2
  116. package/dist/scene/assetDiagnostics.js +2 -2
  117. package/dist/scene/autoTarget.js +2 -1
  118. package/dist/scene/captureCheck.js +3 -2
  119. package/dist/scene/entityStore.d.ts +4 -1
  120. package/dist/scene/entityStore.js +3 -0
  121. package/dist/scene/objectStore.d.ts +10 -0
  122. package/dist/scene/objectStore.js +29 -6
  123. package/dist/scene/sceneKinds.d.ts +18 -0
  124. package/dist/scene/sceneKinds.js +19 -0
  125. package/dist/scene/sequenceDirector.d.ts +110 -0
  126. package/dist/scene/sequenceDirector.js +146 -0
  127. package/dist/session/roles.js +3 -2
  128. package/dist/settings/settingsModel.d.ts +1 -0
  129. package/dist/settings/settingsModel.js +1 -0
  130. package/dist/stats/rollCheck.js +4 -2
  131. package/dist/time/dayNightCycle.d.ts +114 -0
  132. package/dist/time/dayNightCycle.js +208 -0
  133. package/dist/time/timerSet.d.ts +132 -0
  134. package/dist/time/timerSet.js +243 -0
  135. package/dist/ui/accessibility.d.ts +51 -0
  136. package/dist/ui/accessibility.js +83 -0
  137. package/dist/ui/coachMarks.d.ts +99 -0
  138. package/dist/ui/coachMarks.js +126 -0
  139. package/dist/ui/floatingText.d.ts +114 -0
  140. package/dist/ui/floatingText.js +203 -0
  141. package/dist/ui/modalStack.d.ts +143 -0
  142. package/dist/ui/modalStack.js +160 -0
  143. package/dist/ui/objectiveBanner.d.ts +123 -0
  144. package/dist/ui/objectiveBanner.js +162 -0
  145. package/dist/ui/photoMode.d.ts +32 -0
  146. package/dist/ui/photoMode.js +51 -0
  147. package/dist/ui/radialMenu.d.ts +47 -0
  148. package/dist/ui/radialMenu.js +74 -0
  149. package/dist/ui/screenMarkers.d.ts +128 -0
  150. package/dist/ui/screenMarkers.js +118 -0
  151. package/dist/ui.d.ts +8 -0
  152. package/dist/ui.js +8 -0
  153. package/dist/vfx/cameraShake.d.ts +97 -0
  154. package/dist/vfx/cameraShake.js +136 -0
  155. package/dist/vfx/damageDirection.d.ts +102 -0
  156. package/dist/vfx/damageDirection.js +188 -0
  157. package/dist/vfx/particles.d.ts +103 -0
  158. package/dist/vfx/particles.js +256 -0
  159. package/dist/vfx/screenEffects.d.ts +149 -0
  160. package/dist/vfx/screenEffects.js +191 -0
  161. package/dist/world/authoredObjects.d.ts +18 -0
  162. package/dist/world/authoredObjects.js +21 -0
  163. package/dist/world/authoredSpawn.js +19 -0
  164. package/dist/world/buildingLots.d.ts +54 -11
  165. package/dist/world/buildingLots.js +116 -30
  166. package/dist/world/cityBlocks.d.ts +45 -0
  167. package/dist/world/cityBlocks.js +159 -0
  168. package/dist/world/cityContent.d.ts +46 -2
  169. package/dist/world/cityContent.js +160 -2
  170. package/dist/world/cityGenerator.d.ts +261 -23
  171. package/dist/world/cityGenerator.js +643 -33
  172. package/dist/world/cityGeometry.d.ts +20 -0
  173. package/dist/world/cityGeometry.js +91 -0
  174. package/dist/world/cityKind.js +1 -44
  175. package/dist/world/fastTravel.d.ts +64 -0
  176. package/dist/world/fastTravel.js +108 -0
  177. package/dist/world/features.d.ts +16 -0
  178. package/dist/world/features.js +2 -0
  179. package/dist/world/grounded.d.ts +11 -0
  180. package/dist/world/grounded.js +12 -0
  181. package/dist/world/mapAnnotations.d.ts +75 -0
  182. package/dist/world/mapAnnotations.js +139 -0
  183. package/dist/world/markers.js +1 -0
  184. package/dist/world/resourceNode.d.ts +1 -1
  185. package/dist/world/resourceNode.js +2 -1
  186. package/dist/world/roads.d.ts +294 -5
  187. package/dist/world/roads.js +1262 -21
  188. package/dist/world/sharedAnnotations.d.ts +84 -0
  189. package/dist/world/sharedAnnotations.js +76 -0
  190. package/dist/world/spawnOverride.d.ts +37 -0
  191. package/dist/world/spawnOverride.js +50 -0
  192. package/dist/world/streetGenerator.d.ts +60 -11
  193. package/dist/world/streetGenerator.js +1786 -129
  194. package/dist/world/terrain.d.ts +1 -0
  195. package/dist/world/terrain.js +2 -0
  196. package/dist/world/waypoints.d.ts +63 -0
  197. package/dist/world/waypoints.js +137 -0
  198. package/dist/world.d.ts +17 -2
  199. package/dist/world.js +17 -2
  200. package/package.json +1 -1
  201. package/skills/ce-handoff/SKILL.md +149 -0
  202. package/skills/game-design/SKILL.md +2 -0
  203. package/skills/jgengine/SKILL.md +3 -1
  204. package/skills/jgengine/api.md +17 -7
  205. package/skills/jgengine/capabilities.md +12 -0
  206. package/skills/jgengine/recipes/minimal-game.md +7 -4
  207. package/skills/jgengine-combat/api.md +40 -4
  208. package/skills/jgengine-combat/capabilities.md +12 -0
  209. package/skills/jgengine-combat/reference.md +2 -0
  210. package/skills/jgengine-gameplay/api.md +212 -11
  211. package/skills/jgengine-gameplay/capabilities.md +66 -1
  212. package/skills/jgengine-world/api.md +397 -11
  213. package/skills/jgengine-world/capabilities.md +123 -2
  214. package/skills/jgengine-world/reference.md +15 -1
@@ -0,0 +1,106 @@
1
+ import type { TalentNodeDef, TalentTree } from "./talents.js";
2
+ /**
3
+ * Render state of a talent node, derived from its rank and prerequisite satisfaction.
4
+ * - `locked` — rank 0 and at least one prerequisite unmet; cannot be trained yet.
5
+ * - `available` — rank 0, every prerequisite met; ready to take a first point.
6
+ * - `learned` — at least one rank invested, but below `maxRank`.
7
+ * - `maxed` — fully invested (`rank === maxRank`).
8
+ */
9
+ export type TalentNodeState = "locked" | "available" | "learned" | "maxed";
10
+ /** One prerequisite edge into a node — the source node, the rank it demands, and whether that is met. */
11
+ export interface TalentEdgeView {
12
+ /** Prerequisite node id (edge source). */
13
+ from: string;
14
+ /** Rank the prerequisite must reach for this edge to be satisfied. */
15
+ rank: number;
16
+ /** Whether the prerequisite currently sits at or above the required rank. */
17
+ met: boolean;
18
+ }
19
+ /** A per-node view for rendering: grid placement (tier/branch), rank, state, and inbound prerequisite edges. */
20
+ export interface TalentNodeView {
21
+ /** Stable node id (matches the {@link TalentNodeDef}). */
22
+ id: string;
23
+ /** Branch key the node belongs to (`""` when the def has none) — a free string the game styles/columns by. */
24
+ branch: string;
25
+ /** 0-based prerequisite depth: `0` for a root, else `1 + max(tier of prerequisites)`. Drives row/tier layout. */
26
+ tier: number;
27
+ /** Points currently invested in the node. */
28
+ rank: number;
29
+ /** Maximum points the node accepts. */
30
+ maxRank: number;
31
+ /** Derived render state (see {@link TalentNodeState}). */
32
+ state: TalentNodeState;
33
+ /** Whether a point can be spent on the node right now (mirrors `tree.canAllocate(id).ok`). */
34
+ allocatable: boolean;
35
+ /** Inbound prerequisite edges, in definition order. */
36
+ requires: readonly TalentEdgeView[];
37
+ }
38
+ /** A whole-tree render view: placed nodes plus branch/tier extents and point totals. */
39
+ export interface TalentTreeView {
40
+ /** Every node, in definition order, with its render placement and state. */
41
+ nodes: readonly TalentNodeView[];
42
+ /** Distinct branch keys, in first-seen (definition) order — the natural column order. */
43
+ branches: readonly string[];
44
+ /** Number of tiers, i.e. `max(tier) + 1` (0 when the tree is empty). */
45
+ tiers: number;
46
+ /** Points the player still has to spend. */
47
+ pointsAvailable: number;
48
+ /** Points already invested across the tree. */
49
+ pointsSpent: number;
50
+ }
51
+ /**
52
+ * Per-node status a game supplies to build a view from *any* unlock rule — not just point-spend.
53
+ * A game computes these from whatever it wants (a currency threshold, a level, a quest flag, a
54
+ * skill-point pool, nothing at all) so the same widget renders a buy-with-points talent tree, a
55
+ * money-gated upgrade tree, or a condition-unlocked ability web without changing the renderer.
56
+ */
57
+ export interface TalentNodeStatus {
58
+ /** Points/levels invested in the node — `0` means "not taken yet"; a binary unlock uses `0`/`1`. */
59
+ rank: number;
60
+ /**
61
+ * Whether the node can be taken right now under the game's own rule (enough points, enough cash,
62
+ * a level reached, a flag set, …). Drives the `available` state and whether the widget fires
63
+ * `onLearn` for it. The view never interprets *why* — that stays the game's decision.
64
+ */
65
+ allocatable: boolean;
66
+ }
67
+ /** Optional overall point totals for the view header; omit (or 0) for trees that spend no point currency. */
68
+ export interface TalentPointTotals {
69
+ /** Points the player still has to spend. Default `0`. */
70
+ pointsAvailable?: number;
71
+ /** Points already invested across the tree. Default `0`. */
72
+ pointsSpent?: number;
73
+ }
74
+ /**
75
+ * Build a flat, serializable render view of a node graph from a caller-supplied per-node status,
76
+ * decoupled from any particular progression model. For each node the game returns its `rank` and
77
+ * whether it is `allocatable` *right now* under whatever rule it likes — a skill-point pool, a cash
78
+ * threshold, a level gate, a quest flag, or nothing. The builder places every node by branch +
79
+ * prerequisite-depth tier, tags it learned/available/locked/maxed (`available` when rank 0 and every
80
+ * prerequisite is met — see below), and resolves inbound prerequisite edges (each `met` when the
81
+ * source node's supplied rank meets the demand). This is the general escape hatch behind
82
+ * {@link talentTreeView}: it lets the same widget render a buy-with-points talent tree, a money-gated
83
+ * upgrade tree, or a condition-unlocked ability web without the renderer changing. It never interprets
84
+ * what a node *means* — ids, branches, and ranks stay opaque game data.
85
+ *
86
+ * `state` is derived purely from the supplied `rank` vs `maxRank` and prerequisite satisfaction, so a
87
+ * node the game considers takeable-by-condition surfaces as `available` (rank 0, prerequisites met)
88
+ * and the widget only fires `onLearn` for nodes whose `allocatable` the game returned `true`.
89
+ *
90
+ * @capability talent-tree-view-from build a placed talent/upgrade-tree render view from any per-node unlock rule (points, currency, level, flag) — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
91
+ */
92
+ export declare function talentTreeViewFrom<TStat extends string = string>(nodes: readonly TalentNodeDef<TStat>[], status: (node: TalentNodeDef<TStat>) => TalentNodeStatus, totals?: TalentPointTotals): TalentTreeView;
93
+ /**
94
+ * Project the existing talent model (`createTalentTree`) into a flat, serializable render view: every
95
+ * node placed by branch + prerequisite-depth tier, tagged learned/available/locked/maxed, with its
96
+ * inbound prerequisite edges (and whether each is met) and whether a point can be spent right now. It is
97
+ * a pure read over the node defs plus a live {@link TalentTree}, so a React/canvas widget can lay out
98
+ * nodes, draw edges, and gate clicks without re-deriving topology or re-interpreting eligibility. The
99
+ * model still owns allocation, requirements, and point rules — this only reshapes them for drawing, and
100
+ * never interprets what a node *means* (ids, branches, and ranks stay opaque game data). This is the
101
+ * point-spend adapter over {@link talentTreeViewFrom}; pass a custom status to that builder instead to
102
+ * drive the tree from a currency, level, or any other unlock rule.
103
+ *
104
+ * @capability talent-tree-view flatten a talent tree into a placed, per-node render view — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
105
+ */
106
+ export declare function talentTreeView<TStat extends string = string>(nodes: readonly TalentNodeDef<TStat>[], tree: TalentTree<TStat>): TalentTreeView;
@@ -0,0 +1,114 @@
1
+ const DEFAULT_BRANCH = "";
2
+ function requirementNode(requirement) {
3
+ return typeof requirement === "string" ? requirement : requirement.nodeId;
4
+ }
5
+ function requirementRank(requirement) {
6
+ return typeof requirement === "string" ? 1 : requirement.rank;
7
+ }
8
+ /**
9
+ * Compute each node's tier as its longest prerequisite chain depth, memoized. Cycles and
10
+ * dangling prerequisite ids are treated as tier `0` contributions so the pass always terminates.
11
+ */
12
+ function computeTiers(byId) {
13
+ const tiers = new Map();
14
+ const visiting = new Set();
15
+ function tierOf(id) {
16
+ const cached = tiers.get(id);
17
+ if (cached !== undefined)
18
+ return cached;
19
+ const node = byId.get(id);
20
+ if (node === undefined || visiting.has(id))
21
+ return 0;
22
+ visiting.add(id);
23
+ let depth = 0;
24
+ for (const requirement of node.requires ?? []) {
25
+ const reqId = requirementNode(requirement);
26
+ if (byId.has(reqId))
27
+ depth = Math.max(depth, tierOf(reqId) + 1);
28
+ }
29
+ visiting.delete(id);
30
+ tiers.set(id, depth);
31
+ return depth;
32
+ }
33
+ for (const id of byId.keys())
34
+ tierOf(id);
35
+ return tiers;
36
+ }
37
+ /**
38
+ * Build a flat, serializable render view of a node graph from a caller-supplied per-node status,
39
+ * decoupled from any particular progression model. For each node the game returns its `rank` and
40
+ * whether it is `allocatable` *right now* under whatever rule it likes — a skill-point pool, a cash
41
+ * threshold, a level gate, a quest flag, or nothing. The builder places every node by branch +
42
+ * prerequisite-depth tier, tags it learned/available/locked/maxed (`available` when rank 0 and every
43
+ * prerequisite is met — see below), and resolves inbound prerequisite edges (each `met` when the
44
+ * source node's supplied rank meets the demand). This is the general escape hatch behind
45
+ * {@link talentTreeView}: it lets the same widget render a buy-with-points talent tree, a money-gated
46
+ * upgrade tree, or a condition-unlocked ability web without the renderer changing. It never interprets
47
+ * what a node *means* — ids, branches, and ranks stay opaque game data.
48
+ *
49
+ * `state` is derived purely from the supplied `rank` vs `maxRank` and prerequisite satisfaction, so a
50
+ * node the game considers takeable-by-condition surfaces as `available` (rank 0, prerequisites met)
51
+ * and the widget only fires `onLearn` for nodes whose `allocatable` the game returned `true`.
52
+ *
53
+ * @capability talent-tree-view-from build a placed talent/upgrade-tree render view from any per-node unlock rule (points, currency, level, flag) — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
54
+ */
55
+ export function talentTreeViewFrom(nodes, status, totals) {
56
+ const byId = new Map();
57
+ for (const node of nodes)
58
+ byId.set(node.id, node);
59
+ const tiers = computeTiers(byId);
60
+ // Resolve every node's status once up front so prerequisite `met` checks can read a sibling's rank.
61
+ const statusById = new Map();
62
+ for (const node of nodes)
63
+ statusById.set(node.id, status(node));
64
+ const branches = [];
65
+ const seenBranches = new Set();
66
+ let maxTier = -1;
67
+ const viewNodes = nodes.map((node) => {
68
+ const branch = node.branch ?? DEFAULT_BRANCH;
69
+ if (!seenBranches.has(branch)) {
70
+ seenBranches.add(branch);
71
+ branches.push(branch);
72
+ }
73
+ const tier = tiers.get(node.id) ?? 0;
74
+ if (tier > maxTier)
75
+ maxTier = tier;
76
+ const { rank, allocatable } = statusById.get(node.id);
77
+ const requires = (node.requires ?? []).map((requirement) => {
78
+ const from = requirementNode(requirement);
79
+ const required = requirementRank(requirement);
80
+ return { from, rank: required, met: (statusById.get(from)?.rank ?? 0) >= required };
81
+ });
82
+ let state;
83
+ if (rank >= node.maxRank)
84
+ state = "maxed";
85
+ else if (rank > 0)
86
+ state = "learned";
87
+ else
88
+ state = requires.every((edge) => edge.met) ? "available" : "locked";
89
+ return { id: node.id, branch, tier, rank, maxRank: node.maxRank, state, allocatable, requires };
90
+ });
91
+ return {
92
+ nodes: viewNodes,
93
+ branches,
94
+ tiers: maxTier + 1,
95
+ pointsAvailable: totals?.pointsAvailable ?? 0,
96
+ pointsSpent: totals?.pointsSpent ?? 0,
97
+ };
98
+ }
99
+ /**
100
+ * Project the existing talent model (`createTalentTree`) into a flat, serializable render view: every
101
+ * node placed by branch + prerequisite-depth tier, tagged learned/available/locked/maxed, with its
102
+ * inbound prerequisite edges (and whether each is met) and whether a point can be spent right now. It is
103
+ * a pure read over the node defs plus a live {@link TalentTree}, so a React/canvas widget can lay out
104
+ * nodes, draw edges, and gate clicks without re-deriving topology or re-interpreting eligibility. The
105
+ * model still owns allocation, requirements, and point rules — this only reshapes them for drawing, and
106
+ * never interprets what a node *means* (ids, branches, and ranks stay opaque game data). This is the
107
+ * point-spend adapter over {@link talentTreeViewFrom}; pass a custom status to that builder instead to
108
+ * drive the tree from a currency, level, or any other unlock rule.
109
+ *
110
+ * @capability talent-tree-view flatten a talent tree into a placed, per-node render view — branch/tier layout, learned/available/locked/maxed state, prerequisite edges
111
+ */
112
+ export function talentTreeView(nodes, tree) {
113
+ return talentTreeViewFrom(nodes, (node) => ({ rank: tree.rank(node.id), allocatable: tree.canAllocate(node.id).ok }), { pointsAvailable: tree.pointsAvailable(), pointsSpent: tree.pointsSpent() });
114
+ }
@@ -1,5 +1,6 @@
1
1
  import { distanceBetween } from "../scene/spatial.js";
2
2
  import { evaluateLootFilter } from "./lootFilter.js";
3
+ import { resolveRng } from "../random/resolveRng.js";
3
4
  /** Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). */
4
5
  export const WORLD_ITEM_ENTITY_NAME = "world_item";
5
6
  export const DEFAULT_RARITY = "common";
@@ -105,7 +106,7 @@ export function createWorldItemStore(deps) {
105
106
  export function resolveDeathDrops(drops, options) {
106
107
  if (options.mode !== "world")
107
108
  return { worldSpawns: [], grants: [...drops] };
108
- const rng = options.rng ?? Math.random;
109
+ const rng = resolveRng(options.rng, "worldItem");
109
110
  const worldSpawns = [];
110
111
  const grants = [];
111
112
  for (const drop of drops) {
@@ -9,40 +9,49 @@ export { createAuctionBook, type Auction, type AuctionSettlement } from "./econo
9
9
  export { createListingBook, type Listing } from "./economy/listingBook.js";
10
10
  export { createPriceHistory, type PriceStats, type SaleRecord } from "./economy/priceHistory.js";
11
11
  export { addScheduledRule, advanceLedger, annotate, balanceOf, cancelRule, capAmount, createResourceLedger, curveScale, pauseRule, redirect, rejectWhen, resumeRule, taxFraction, thresholdScale, type AdvanceOptions, type AdvanceResult, type AppliedTransaction, type CatchUpPolicy, type LedgerEvent, type LedgerEventKind, type PolicyContext, type PolicyRead, type Precision, type ResourceLedger, type ResourcePolicy, type ResourceTransaction, type RuleCursor, type ScheduledRule, type ThresholdBand, } from "./economy/resourceLedger.js";
12
+ export { createShopStock, type ShopBuyRejection, type ShopBuyResult, type ShopPrice, type ShopSellRejection, type ShopSellResult, type ShopStock, type ShopStockConfig, type ShopStockEntry, type ShopStockSnapshot, } from "./economy/shopStock.js";
12
13
  export { type TechNodeDef } from "./economy/techTree.js";
13
- export { balance, charge, chargeAll, createEmptyWallet, grant, isOverdrawn } from "./economy/wallet.js";
14
+ export { balance, canAfford, charge, chargeAll, createEmptyWallet, grant, isOverdrawn, type ChargeOptions, type ChargeResult, type Overdraft, } from "./economy/wallet.js";
15
+ export { createAchievementTracker, type AchievementDef, type AchievementSnapshot, type AchievementTracker, type AchievementTrackerOptions, type AchievementUnlock, type AchievementView, } from "./game/achievements.js";
14
16
  export { DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, createChatRateLimiter, type Chat, type ChatMessage, type ChatRateLimit, type ChatSendResult, } from "./game/chat.js";
17
+ export { createCodex, type Codex, type CodexEntryDef, type CodexEntryView, type CodexOptions, type CodexSnapshot, } from "./game/codex.js";
15
18
  export { playControlsActive } from "./game/controlGate.js";
16
19
  export { createCosmetics } from "./game/cosmetics.js";
17
- export { type GameDefinition, type GameDefinitionConfig, type GameLoop, type InventoryDeclaration, type LifecycleConfig, type PhysicsConfig, } from "./game/defineGame.js";
20
+ export { type GameDefinition, type GameDefinitionConfig, type GameLifecycle, type GameLoop, type InventoryDeclaration, type LifecycleConfig, type PhysicsConfig, } from "./game/defineGame.js";
18
21
  export { defineSystem, type SystemDefinition, type SystemEventHandlers, type SystemTick } from "./game/defineSystem.js";
19
22
  export { createGameDialogue, dialogueSlot } from "./game/dialogue.js";
23
+ export { createDialogueRun, selectDialogueView, type DialogueGraph, type DialogueGraphChoice, type DialogueGraphNode, type DialogueGraphSnapshot, type DialogueGraphView, type DialogueRun, type DialogueRunOptions, } from "./game/dialogueGraph.js";
24
+ export { createEventTicker, type EventTicker, type EventTickerEntry, type EventTickerInput, type EventTickerOptions, type EventTickerSnapshot, type EventTickerView, } from "./game/eventTicker.js";
20
25
  export { createGameEvents, type CombatTelegraphEvent, type CombatVfxEvent, type EntityDiedEvent, type EntityFloatTextEvent, type GameEventMap, type GameEvents, type RetainedVfxKind, type StatLevelUpEvent, type VfxKind, type VfxRef, } from "./game/events.js";
21
26
  export { appendFeed, createGameFeed, pruneFeed, type FeedEntry, type FeedWindow, type TimedFeedEntry, } from "./game/feed.js";
22
27
  export { gamePhase, setGamePhase, type GamePhase } from "./game/gamePhase.js";
23
28
  export { createKeyValueStore, type KeyValueStorage } from "./game/keyValueStore.js";
24
29
  export { createLeaderboard, type LeaderboardRow, type LeaderboardScope } from "./game/leaderboard.js";
30
+ export { medalFor, rankLeaderboard, type RankLeaderboardOptions, type RankableRow, type RankedEntry, type TieMode, } from "./game/leaderboardRank.js";
25
31
  export { createLevelSequence, type LevelSequence } from "./game/levelSequence.js";
26
32
  export { createLoadouts, type LoadoutDef } from "./game/loadout.js";
27
33
  export { evaluateLootFilter, lootFilter, type LootFilterRule } from "./game/lootFilter.js";
28
34
  export { createLootPipeline, defineLootPipeline, type LootDropProvenance, type LootModifier, type LootPipeline, type LootPipelineDef, type LootPipelineDeps, type LootPlanEntry, type LootResolution, type LootResolveContext, type LootRollPlan, type LootStage, type LootStageKind, type LootStageStatus, type LootStageTrace, } from "./game/lootPipeline.js";
29
35
  export { createLootRegistry, lootTable, type Drop, type LootTableDef } from "./game/lootTable.js";
30
36
  export { resolveOneShotClip } from "./game/modelAnimation.js";
37
+ export { createNotificationCenter, type NotificationCenterOptions, type NotificationEntry, type NotificationFilter, type NotificationInput, type NotificationStore, } from "./game/notifications.js";
31
38
  export { evaluateObjective } from "./game/objectives.js";
32
39
  export { createLayerRegistry, diffParams, orderLayers, resolveParams, resolveSelection, validateLayers, type LayerConflict, type LayerOps, type LayerRegistry, type LayerSelection, type ParamContribution, type ParamDelta, type ParamLayer, type ParamOp, type ParamSnapshot, } from "./game/paramLayers.js";
33
40
  export { DEFAULT_PING_CATEGORIES, PING_FEED_ACTION, createPingSystem, type PingCategory, type PingSystem, } from "./game/ping.js";
34
41
  export { CAMERA_FRUSTUM_DEFAULTS, worldHealthBarAllowsRole, type BackdropConfig, type CameraKeyframe, type CameraRigKind, type ChaseCameraConfig, type CinematicCameraConfig, type DirectionalLightingConfig, type EntitySpriteConfig, type FirstPersonCameraConfig, type GameCameraConfig, type InspectionCameraConfig, type InspectionZoomAnchor, type LightingConfig, type LockOnCameraConfig, type ModelConfig, type ModelMaterialMaps, type ModelMaterialOverride, type ObjectStyle, type ObserverCameraConfig, type PlayableGame, type PointerConfig, type RtsCameraConfig, type ShoulderCameraConfig, type SideScrollCameraConfig, type TopDownCameraConfig, type WorldItemRenderConfig, type WorldOverlayProps, } from "./game/playableGame.js";
35
42
  export { curve, evalCurve, leveling, type Curve, type LevelProgress, type LevelingConfig, type LevelingTrack, } from "./game/progression.js";
36
- export { createQuestJournal, type QuestDef, type QuestInstance, type QuestRewards } from "./game/quest.js";
43
+ export { createQuestJournal, defaultObjectiveLabel, describeTrackedQuest, type QuestDef, type QuestInstance, type QuestRewards, type TrackedObjectiveView, type TrackedQuestView, } from "./game/quest.js";
37
44
  export { RaceState, createLapTimer, createRaceState, finishRaceSession, firstPastPost, idleRaceSession, lapDurations, parDelta, placementOf, raceOutcomeOf, racePlacements, raceTrack, splitSegments, startRaceCountdown, tickRaceSession, } from "./game/race.js";
38
45
  export { createRecordBook } from "./game/recordBook.js";
39
46
  export { createRuleRegistry, rerollRules, selectRules, type RuleDef, type RuleRegistry, type RuleSelection, type RuleSelectionConfig, } from "./game/ruleSelection.js";
40
47
  export { createRunDraft, type RunDraft, type RunModifierOffer } from "./game/runDraft.js";
48
+ export { createSaveSlots, type SaveSlotMeta, type SaveSlotWrite, type SaveSlots, type SaveSlotsConfig, type SaveSlotsSnapshot, } from "./game/saveSlots.js";
41
49
  export { createSaveStore, localSaveBackend, memorySaveBackend, remoteSaveBackend, type SaveBackend, type SaveStatus, type SaveStore, } from "./game/saveStore.js";
42
50
  export { createSocial, type FriendEntry, type FriendRequestEntry, type Friends, type Party, type PartyInviteEntry, type PartyMemberEntry, type PresenceInfo, type Social, type SocialDeps, type WorldInvite, type WorldInviteTarget, } from "./game/social.js";
43
51
  export { createSpawnPoints } from "./game/spawnPoints.js";
44
52
  export { composeGameLoop } from "./game/systemRuntime.js";
45
53
  export { DEFAULT_FIXED_STAGES, DEFAULT_FRAME_STAGES, compileSystemSchedule, type CompiledSystemSchedule, } from "./game/systemSchedule.js";
54
+ export { talentTreeView, talentTreeViewFrom, type TalentEdgeView, type TalentNodeState, type TalentNodeStatus, type TalentNodeView, type TalentPointTotals, type TalentTreeView, } from "./game/talentTreeView.js";
46
55
  export { createTalentTree, type TalentNodeDef, type TalentTree } from "./game/talents.js";
47
56
  export { appendToast, createToastQueue, pruneToasts, type Toast } from "./game/toasts.js";
48
57
  export { createUnlockCatalog, createUnlocks, type UnlockDef } from "./game/unlocks.js";
@@ -54,8 +63,10 @@ export { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, save
54
63
  export { createGestureSurfaceTracker } from "./input/gestureSurface.js";
55
64
  export { aimToPoint, type PointerHit, type PointerVec3 } from "./input/pointer.js";
56
65
  export { normalizePointerToAxis, type PointerAxisState } from "./input/pointerAxis.js";
66
+ export { createRebindSession, type RebindActionConfig, type RebindConflict, type RebindRow, type RebindSession, type RebindSessionActionsConfig, type RebindSessionConfig, type RebindSessionMapConfig, type RebindSessionSnapshot, } from "./input/rebindSession.js";
67
+ export { activeTouchControlsMode, setTouchControlsMode } from "./input/touchControlsMode.js";
57
68
  export { createTouchGestureTracker } from "./input/touchGestures.js";
58
- export { DEFAULT_TOUCH_STYLE, TOUCH_STYLES, TOUCH_STYLE_OPTIONS, deriveTouchScheme, touchButtonShape, touchCode, withTouchCodes, type TouchAnchor, type TouchButton, type TouchButtonShape, type TouchJoystick, type TouchScheme, type TouchStyle, } from "./input/touchScheme.js";
69
+ export { DEFAULT_TOUCH_STYLE, TOUCH_STYLES, TOUCH_STYLE_OPTIONS, deriveTouchScheme, touchButtonShape, touchCode, withTouchCodes, type TouchAnchor, type TouchButton, type TouchButtonShape, type TouchControlsConfig, type TouchControlsModeConfig, type TouchJoystick, type TouchScheme, type TouchStyle, } from "./input/touchScheme.js";
59
70
  export { type InventorySlot, type InventoryState } from "./inventory/inventoryModel.js";
60
71
  export { type Cell, type Rotation } from "./inventory/shapedGrid.js";
61
72
  export { type SlotGrid } from "./inventory/slotModel.js";
package/dist/gameplay.js CHANGED
@@ -9,40 +9,49 @@ export { createAuctionBook } from "./economy/auctionBook.js";
9
9
  export { createListingBook } from "./economy/listingBook.js";
10
10
  export { createPriceHistory } from "./economy/priceHistory.js";
11
11
  export { addScheduledRule, advanceLedger, annotate, balanceOf, cancelRule, capAmount, createResourceLedger, curveScale, pauseRule, redirect, rejectWhen, resumeRule, taxFraction, thresholdScale, } from "./economy/resourceLedger.js";
12
+ export { createShopStock, } from "./economy/shopStock.js";
12
13
  export {} from "./economy/techTree.js";
13
- export { balance, charge, chargeAll, createEmptyWallet, grant, isOverdrawn } from "./economy/wallet.js";
14
+ export { balance, canAfford, charge, chargeAll, createEmptyWallet, grant, isOverdrawn, } from "./economy/wallet.js";
15
+ export { createAchievementTracker, } from "./game/achievements.js";
14
16
  export { DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, createChatRateLimiter, } from "./game/chat.js";
17
+ export { createCodex, } from "./game/codex.js";
15
18
  export { playControlsActive } from "./game/controlGate.js";
16
19
  export { createCosmetics } from "./game/cosmetics.js";
17
20
  export {} from "./game/defineGame.js";
18
21
  export { defineSystem } from "./game/defineSystem.js";
19
22
  export { createGameDialogue, dialogueSlot } from "./game/dialogue.js";
23
+ export { createDialogueRun, selectDialogueView, } from "./game/dialogueGraph.js";
24
+ export { createEventTicker, } from "./game/eventTicker.js";
20
25
  export { createGameEvents, } from "./game/events.js";
21
26
  export { appendFeed, createGameFeed, pruneFeed, } from "./game/feed.js";
22
27
  export { gamePhase, setGamePhase } from "./game/gamePhase.js";
23
28
  export { createKeyValueStore } from "./game/keyValueStore.js";
24
29
  export { createLeaderboard } from "./game/leaderboard.js";
30
+ export { medalFor, rankLeaderboard, } from "./game/leaderboardRank.js";
25
31
  export { createLevelSequence } from "./game/levelSequence.js";
26
32
  export { createLoadouts } from "./game/loadout.js";
27
33
  export { evaluateLootFilter, lootFilter } from "./game/lootFilter.js";
28
34
  export { createLootPipeline, defineLootPipeline, } from "./game/lootPipeline.js";
29
35
  export { createLootRegistry, lootTable } from "./game/lootTable.js";
30
36
  export { resolveOneShotClip } from "./game/modelAnimation.js";
37
+ export { createNotificationCenter, } from "./game/notifications.js";
31
38
  export { evaluateObjective } from "./game/objectives.js";
32
39
  export { createLayerRegistry, diffParams, orderLayers, resolveParams, resolveSelection, validateLayers, } from "./game/paramLayers.js";
33
40
  export { DEFAULT_PING_CATEGORIES, PING_FEED_ACTION, createPingSystem, } from "./game/ping.js";
34
41
  export { CAMERA_FRUSTUM_DEFAULTS, worldHealthBarAllowsRole, } from "./game/playableGame.js";
35
42
  export { curve, evalCurve, leveling, } from "./game/progression.js";
36
- export { createQuestJournal } from "./game/quest.js";
43
+ export { createQuestJournal, defaultObjectiveLabel, describeTrackedQuest, } from "./game/quest.js";
37
44
  export { RaceState, createLapTimer, createRaceState, finishRaceSession, firstPastPost, idleRaceSession, lapDurations, parDelta, placementOf, raceOutcomeOf, racePlacements, raceTrack, splitSegments, startRaceCountdown, tickRaceSession, } from "./game/race.js";
38
45
  export { createRecordBook } from "./game/recordBook.js";
39
46
  export { createRuleRegistry, rerollRules, selectRules, } from "./game/ruleSelection.js";
40
47
  export { createRunDraft } from "./game/runDraft.js";
48
+ export { createSaveSlots, } from "./game/saveSlots.js";
41
49
  export { createSaveStore, localSaveBackend, memorySaveBackend, remoteSaveBackend, } from "./game/saveStore.js";
42
50
  export { createSocial, } from "./game/social.js";
43
51
  export { createSpawnPoints } from "./game/spawnPoints.js";
44
52
  export { composeGameLoop } from "./game/systemRuntime.js";
45
53
  export { DEFAULT_FIXED_STAGES, DEFAULT_FRAME_STAGES, compileSystemSchedule, } from "./game/systemSchedule.js";
54
+ export { talentTreeView, talentTreeViewFrom, } from "./game/talentTreeView.js";
46
55
  export { createTalentTree } from "./game/talents.js";
47
56
  export { appendToast, createToastQueue, pruneToasts } from "./game/toasts.js";
48
57
  export { createUnlockCatalog, createUnlocks } from "./game/unlocks.js";
@@ -54,6 +63,8 @@ export { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, save
54
63
  export { createGestureSurfaceTracker } from "./input/gestureSurface.js";
55
64
  export { aimToPoint } from "./input/pointer.js";
56
65
  export { normalizePointerToAxis } from "./input/pointerAxis.js";
66
+ export { createRebindSession, } from "./input/rebindSession.js";
67
+ export { activeTouchControlsMode, setTouchControlsMode } from "./input/touchControlsMode.js";
57
68
  export { createTouchGestureTracker } from "./input/touchGestures.js";
58
69
  export { DEFAULT_TOUCH_STYLE, TOUCH_STYLES, TOUCH_STYLE_OPTIONS, deriveTouchScheme, touchButtonShape, touchCode, withTouchCodes, } from "./input/touchScheme.js";
59
70
  export {} from "./inventory/inventoryModel.js";
@@ -0,0 +1,51 @@
1
+ /** A locale tag (e.g. `"en"`, `"es"`, `"ja"`) — a key into a {@link Catalog}. */
2
+ export type Locale = string;
3
+ /** Flat message table for one locale: key → template string with `{param}` slots. */
4
+ export type Messages = Readonly<Record<string, string>>;
5
+ /** All locales' message tables, keyed by locale. */
6
+ export type Catalog = Readonly<Record<Locale, Messages>>;
7
+ /** Interpolation values for a message template. */
8
+ export type TParams = Readonly<Record<string, string | number>>;
9
+ /**
10
+ * Replace `{name}` placeholders in `template` with `params[name]`. Unknown
11
+ * placeholders are left intact. Pure and locale-independent.
12
+ */
13
+ export declare function interpolate(template: string, params?: TParams): string;
14
+ /** Configuration for {@link createI18n}. */
15
+ export interface I18nOptions {
16
+ catalog: Catalog;
17
+ /** Active locale. Must be a key of `catalog` for lookups to resolve there. */
18
+ locale: Locale;
19
+ /** Locale consulted when a key is missing in the active locale. */
20
+ fallbackLocale?: Locale;
21
+ /** Invoked when a key resolves nowhere (dev warnings/telemetry); `t` still returns the key. */
22
+ onMissing?: (key: string, locale: Locale) => void;
23
+ }
24
+ /** Runtime translator over a message {@link Catalog} — active locale, lookup with fallback, interpolation, and pluralization. */
25
+ export interface I18n {
26
+ locale(): Locale;
27
+ /** Switch the active locale; notifies subscribers when it actually changes. */
28
+ setLocale(locale: Locale): void;
29
+ /** Locales present in the catalog. */
30
+ locales(): readonly Locale[];
31
+ /** True when `key` resolves in the active or fallback locale. */
32
+ has(key: string): boolean;
33
+ /** Translate `key`, interpolating `{param}` slots; returns `key` itself if it resolves nowhere. */
34
+ t(key: string, params?: TParams): string;
35
+ /**
36
+ * Plural-aware translate: picks `${key}.${category}` for the active locale's
37
+ * Intl plural category (falling back to `${key}.other`, then `key`), with
38
+ * `count` available as the `{count}` interpolation param.
39
+ */
40
+ plural(key: string, count: number, params?: TParams): string;
41
+ subscribe(listener: () => void): () => void;
42
+ }
43
+ /**
44
+ * Create a translator over a message catalog: active-locale lookup with a
45
+ * fallback-locale chain, `{param}` interpolation, and `Intl.PluralRules`-based
46
+ * pluralization. Observable (`subscribe`) so React re-renders on `setLocale`;
47
+ * the catalog is caller-owned static data, the locale is the only state.
48
+ *
49
+ * @capability i18n translate a message catalog with fallback-locale lookup, `{param}` interpolation, and Intl pluralization; observable active-locale switching
50
+ */
51
+ export declare function createI18n(options: I18nOptions): I18n;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Replace `{name}` placeholders in `template` with `params[name]`. Unknown
3
+ * placeholders are left intact. Pure and locale-independent.
4
+ */
5
+ export function interpolate(template, params) {
6
+ if (params === undefined)
7
+ return template;
8
+ return template.replace(/\{(\w+)\}/g, (whole, name) => {
9
+ const value = params[name];
10
+ return value === undefined ? whole : String(value);
11
+ });
12
+ }
13
+ function pluralCategory(locale, count) {
14
+ try {
15
+ return new Intl.PluralRules(locale).select(count);
16
+ }
17
+ catch {
18
+ return count === 1 ? "one" : "other";
19
+ }
20
+ }
21
+ /**
22
+ * Create a translator over a message catalog: active-locale lookup with a
23
+ * fallback-locale chain, `{param}` interpolation, and `Intl.PluralRules`-based
24
+ * pluralization. Observable (`subscribe`) so React re-renders on `setLocale`;
25
+ * the catalog is caller-owned static data, the locale is the only state.
26
+ *
27
+ * @capability i18n translate a message catalog with fallback-locale lookup, `{param}` interpolation, and Intl pluralization; observable active-locale switching
28
+ */
29
+ export function createI18n(options) {
30
+ const { catalog, fallbackLocale, onMissing } = options;
31
+ let active = options.locale;
32
+ const listeners = new Set();
33
+ function resolve(key) {
34
+ const inActive = catalog[active]?.[key];
35
+ if (inActive !== undefined)
36
+ return inActive;
37
+ if (fallbackLocale !== undefined && fallbackLocale !== active)
38
+ return catalog[fallbackLocale]?.[key];
39
+ return undefined;
40
+ }
41
+ const i18n = {
42
+ locale() {
43
+ return active;
44
+ },
45
+ setLocale(locale) {
46
+ if (locale === active)
47
+ return;
48
+ active = locale;
49
+ for (const listener of listeners)
50
+ listener();
51
+ },
52
+ locales() {
53
+ return Object.keys(catalog);
54
+ },
55
+ has(key) {
56
+ return resolve(key) !== undefined;
57
+ },
58
+ t(key, params) {
59
+ const template = resolve(key);
60
+ if (template === undefined) {
61
+ onMissing?.(key, active);
62
+ return key;
63
+ }
64
+ return interpolate(template, params);
65
+ },
66
+ plural(key, count, params) {
67
+ const category = pluralCategory(active, count);
68
+ const template = resolve(`${key}.${category}`) ?? resolve(`${key}.other`) ?? resolve(key);
69
+ const merged = { count, ...(params ?? {}) };
70
+ if (template === undefined) {
71
+ onMissing?.(`${key}.${category}`, active);
72
+ return key;
73
+ }
74
+ return interpolate(template, merged);
75
+ },
76
+ subscribe(listener) {
77
+ listeners.add(listener);
78
+ return () => listeners.delete(listener);
79
+ },
80
+ };
81
+ return i18n;
82
+ }
@@ -73,6 +73,6 @@ export interface GenericAxisChannel<TAxes extends string> {
73
73
  * is active), clamped to the axis range; axes without a listed range default to bipolar `[-1, 1]`.
74
74
  * @internal
75
75
  */
76
- export declare function sampleAxisBindings<TAxes extends string>(bindings: Record<TAxes, AxisBinding>, isDown: (code: string) => boolean, pointer?: PointerAxisState | null, ranges?: Partial<Record<TAxes, AxisRange>>): Record<TAxes, number>;
76
+ export declare function sampleAxisBindings<TAxes extends string>(bindings: Record<TAxes, AxisBinding>, isDown: (code: string) => boolean, pointer?: PointerAxisState | null, ranges?: Partial<Record<TAxes, AxisRange>>, value?: (code: string) => number): Record<TAxes, number>;
77
77
  /** @internal */
78
78
  export declare function createAxisChannel<TAxes extends string>(config: GenericAxisConfig<TAxes>): GenericAxisChannel<TAxes>;
@@ -25,6 +25,21 @@ function digitalTarget(binding, isDown) {
25
25
  const negative = binding.negative !== undefined && binding.negative.some(isDown) ? 1 : 0;
26
26
  return positive - negative;
27
27
  }
28
+ /** Strongest magnitude across a side's codes — with a `value` source an analog stick contributes its actual deflection; without one this degrades to {@link digitalTarget}'s 0/1. */
29
+ function sideValue(codes, value) {
30
+ if (codes === undefined)
31
+ return 0;
32
+ let strongest = 0;
33
+ for (const code of codes) {
34
+ const magnitude = value(code);
35
+ if (magnitude > strongest)
36
+ strongest = magnitude;
37
+ }
38
+ return strongest;
39
+ }
40
+ function analogTarget(binding, value) {
41
+ return sideValue(binding.positive, value) - sideValue(binding.negative, value);
42
+ }
28
43
  /**
29
44
  * Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
30
45
  * are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
@@ -85,13 +100,14 @@ const BIPOLAR_RANGE = { min: -1, max: 1 };
85
100
  * is active), clamped to the axis range; axes without a listed range default to bipolar `[-1, 1]`.
86
101
  * @internal
87
102
  */
88
- export function sampleAxisBindings(bindings, isDown, pointer, ranges) {
103
+ export function sampleAxisBindings(bindings, isDown, pointer, ranges, value) {
89
104
  const out = {};
90
105
  for (const axis of Object.keys(bindings)) {
91
106
  const binding = bindings[axis];
92
107
  const range = ranges?.[axis] ?? BIPOLAR_RANGE;
93
108
  const fromPointer = binding.pointer === undefined ? null : pointerAxisValue(binding.pointer, pointer ?? null);
94
- out[axis] = clampAxis(fromPointer ?? digitalTarget(binding, isDown), range);
109
+ const fromBindings = value === undefined ? digitalTarget(binding, isDown) : analogTarget(binding, value);
110
+ out[axis] = clampAxis(fromPointer ?? fromBindings, range);
95
111
  }
96
112
  return out;
97
113
  }