@jgengine/core 0.6.0 → 0.7.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 (222) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +2 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +281 -4
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +35 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +64 -2
  128. package/dist/runtime/gameContext.js +154 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -0,0 +1,130 @@
1
+ export function productionBuilding(config) {
2
+ return {
3
+ id: config.id,
4
+ inputs: config.inputs ?? [],
5
+ outputs: config.outputs,
6
+ rate: config.rate,
7
+ power: config.power,
8
+ bufferMultiplier: config.bufferMultiplier,
9
+ };
10
+ }
11
+ export function createProductionState() {
12
+ return { buffer: {}, output: {}, progress: 0, active: false };
13
+ }
14
+ function addCount(counts, itemId, amount) {
15
+ const next = { ...counts };
16
+ const total = (next[itemId] ?? 0) + amount;
17
+ if (total <= 0)
18
+ delete next[itemId];
19
+ else
20
+ next[itemId] = total;
21
+ return next;
22
+ }
23
+ function bufferCapacity(def, required) {
24
+ const multiplier = def.bufferMultiplier !== undefined && def.bufferMultiplier > 0 ? def.bufferMultiplier : 2;
25
+ return Math.max(required, Math.ceil(required * multiplier));
26
+ }
27
+ export function acceptsInput(def, state, itemId) {
28
+ const input = def.inputs.find((i) => i.itemId === itemId);
29
+ if (input === undefined)
30
+ return false;
31
+ const held = state.buffer[itemId] ?? 0;
32
+ return held < bufferCapacity(def, input.count);
33
+ }
34
+ export function feedProduction(def, state, itemId, count) {
35
+ const input = def.inputs.find((i) => i.itemId === itemId);
36
+ if (input === undefined || count <= 0)
37
+ return { state, accepted: 0 };
38
+ const capacity = bufferCapacity(def, input.count);
39
+ const held = state.buffer[itemId] ?? 0;
40
+ const accepted = Math.min(count, Math.max(0, capacity - held));
41
+ if (accepted === 0)
42
+ return { state, accepted: 0 };
43
+ return { state: { ...state, buffer: addCount(state.buffer, itemId, accepted) }, accepted };
44
+ }
45
+ export function drainOutput(state, itemId, count) {
46
+ const held = state.output[itemId] ?? 0;
47
+ const taken = count === undefined ? held : Math.min(count, held);
48
+ if (taken <= 0)
49
+ return { state, taken: 0 };
50
+ return { state: { ...state, output: addCount(state.output, itemId, -taken) }, taken };
51
+ }
52
+ function hasInputs(def, buffer) {
53
+ return def.inputs.every((i) => (buffer[i.itemId] ?? 0) >= i.count);
54
+ }
55
+ function consume(def, buffer) {
56
+ let next = buffer;
57
+ for (const input of def.inputs)
58
+ next = addCount(next, input.itemId, -input.count);
59
+ return next;
60
+ }
61
+ function emit(def, output) {
62
+ let next = output;
63
+ for (const out of def.outputs)
64
+ next = addCount(next, out.itemId, out.count);
65
+ return next;
66
+ }
67
+ export function tickProduction(def, state, input) {
68
+ const powerNeeded = def.power !== undefined && def.power > 0;
69
+ if (powerNeeded && input.powered !== true)
70
+ return state;
71
+ if (!(def.rate > 0) || input.dt <= 0)
72
+ return state;
73
+ const secondsPerCycle = 1 / def.rate;
74
+ let buffer = state.buffer;
75
+ let output = state.output;
76
+ let progress = state.progress;
77
+ let active = state.active;
78
+ if (!active) {
79
+ if (!hasInputs(def, buffer))
80
+ return state;
81
+ buffer = consume(def, buffer);
82
+ active = true;
83
+ progress = 0;
84
+ }
85
+ progress += input.dt;
86
+ let guard = 0;
87
+ while (active && progress >= secondsPerCycle) {
88
+ progress -= secondsPerCycle;
89
+ output = emit(def, output);
90
+ if (hasInputs(def, buffer)) {
91
+ buffer = consume(def, buffer);
92
+ }
93
+ else {
94
+ active = false;
95
+ progress = 0;
96
+ }
97
+ guard += 1;
98
+ if (guard >= 10_000)
99
+ break;
100
+ }
101
+ return { buffer, output, progress, active };
102
+ }
103
+ export function advanceTransport(path, items, dt) {
104
+ if (dt <= 0 || !(path.speed > 0))
105
+ return { items: items.slice(), delivered: [] };
106
+ const moving = [];
107
+ const delivered = [];
108
+ const step = path.speed * dt;
109
+ for (const item of items) {
110
+ const position = item.position + step;
111
+ if (position >= path.length)
112
+ delivered.push({ ...item, position: path.length });
113
+ else
114
+ moving.push({ ...item, position });
115
+ }
116
+ return { items: moving, delivered };
117
+ }
118
+ export function resolvePowerGrid(supply, consumers) {
119
+ const powered = new Set();
120
+ let used = 0;
121
+ let demand = 0;
122
+ for (const consumer of consumers) {
123
+ demand += Math.max(0, consumer.demand);
124
+ if (used + consumer.demand <= supply) {
125
+ used += consumer.demand;
126
+ powered.add(consumer.id);
127
+ }
128
+ }
129
+ return { powered, supply, demand, deficit: Math.max(0, demand - supply) };
130
+ }
@@ -0,0 +1,62 @@
1
+ import type { Vec2 } from "../world/geometry.js";
2
+ import type { InventoryLayout, InventoryState, ItemTraits } from "../inventory/inventoryModel.js";
3
+ export interface RecipeItem {
4
+ itemId: string;
5
+ count: number;
6
+ }
7
+ export interface RecipeDef {
8
+ id: string;
9
+ inputs: readonly RecipeItem[];
10
+ outputs: readonly RecipeItem[];
11
+ seconds?: number;
12
+ station?: string;
13
+ stationRange?: number;
14
+ requires?: readonly string[];
15
+ category?: string;
16
+ }
17
+ export interface StationInstance {
18
+ catalogId: string;
19
+ position: Vec2;
20
+ }
21
+ export interface CraftContext {
22
+ origin?: Vec2;
23
+ stations?: readonly StationInstance[];
24
+ unlocked?: (id: string) => boolean;
25
+ }
26
+ export type CraftRejection = {
27
+ reason: "missing-inputs";
28
+ missing: readonly RecipeItem[];
29
+ } | {
30
+ reason: "no-station";
31
+ station: string;
32
+ } | {
33
+ reason: "locked";
34
+ requires: readonly string[];
35
+ } | {
36
+ reason: "no-output-space";
37
+ };
38
+ export type CraftCheck = {
39
+ ok: true;
40
+ } | ({
41
+ ok: false;
42
+ } & CraftRejection);
43
+ export type CraftResult = {
44
+ status: "ok";
45
+ state: InventoryState;
46
+ } | ({
47
+ status: "rejected";
48
+ } & CraftRejection);
49
+ export declare function craftSeconds(recipe: RecipeDef): number;
50
+ export declare function missingInputs(state: InventoryState, recipe: RecipeDef): RecipeItem[];
51
+ export declare function hasRecipeInputs(state: InventoryState, recipe: RecipeDef): boolean;
52
+ export declare function stationSatisfied(recipe: RecipeDef, context: CraftContext): boolean;
53
+ export declare function canCraft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context?: CraftContext): CraftCheck;
54
+ export declare function craft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context?: CraftContext): CraftResult;
55
+ export interface RecipeGraph {
56
+ all(): readonly RecipeDef[];
57
+ get(id: string): RecipeDef | null;
58
+ producing(itemId: string): RecipeDef[];
59
+ using(itemId: string): RecipeDef[];
60
+ category(categoryId: string): RecipeDef[];
61
+ }
62
+ export declare function createRecipeGraph(defs?: readonly RecipeDef[]): RecipeGraph;
@@ -0,0 +1,101 @@
1
+ import { countItem, putItem, takeItem } from "../inventory/inventoryModel.js";
2
+ export function craftSeconds(recipe) {
3
+ return recipe.seconds !== undefined && recipe.seconds > 0 ? recipe.seconds : 0;
4
+ }
5
+ export function missingInputs(state, recipe) {
6
+ const missing = [];
7
+ for (const input of recipe.inputs) {
8
+ const have = countItem(state, input.itemId);
9
+ if (have < input.count)
10
+ missing.push({ itemId: input.itemId, count: input.count - have });
11
+ }
12
+ return missing;
13
+ }
14
+ export function hasRecipeInputs(state, recipe) {
15
+ return missingInputs(state, recipe).length === 0;
16
+ }
17
+ export function stationSatisfied(recipe, context) {
18
+ if (recipe.station === undefined)
19
+ return true;
20
+ const range = recipe.stationRange !== undefined && recipe.stationRange > 0 ? recipe.stationRange : Infinity;
21
+ const origin = context.origin;
22
+ const stations = context.stations ?? [];
23
+ for (const station of stations) {
24
+ if (station.catalogId !== recipe.station)
25
+ continue;
26
+ if (range === Infinity || origin === undefined)
27
+ return true;
28
+ const dx = station.position[0] - origin[0];
29
+ const dz = station.position[1] - origin[1];
30
+ if (dx * dx + dz * dz <= range * range)
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ function lockedRequirements(recipe, context) {
36
+ if (recipe.requires === undefined || recipe.requires.length === 0)
37
+ return [];
38
+ const unlocked = context.unlocked;
39
+ if (unlocked === undefined)
40
+ return [];
41
+ return recipe.requires.filter((id) => !unlocked(id));
42
+ }
43
+ function outputsFit(state, layout, traits, recipe) {
44
+ let projected = state;
45
+ for (const output of recipe.outputs) {
46
+ const put = putItem(projected, layout, traits, output.itemId, output.count);
47
+ if (put.status !== "ok")
48
+ return false;
49
+ projected = put.state;
50
+ }
51
+ return true;
52
+ }
53
+ function consumedState(state, recipe) {
54
+ let projected = state;
55
+ for (const input of recipe.inputs) {
56
+ const take = takeItem(projected, input.itemId, input.count);
57
+ if (take.status === "ok")
58
+ projected = take.state;
59
+ }
60
+ return projected;
61
+ }
62
+ export function canCraft(state, layout, traits, recipe, context = {}) {
63
+ const locked = lockedRequirements(recipe, context);
64
+ if (locked.length > 0)
65
+ return { ok: false, reason: "locked", requires: locked };
66
+ if (!stationSatisfied(recipe, context))
67
+ return { ok: false, reason: "no-station", station: recipe.station };
68
+ const missing = missingInputs(state, recipe);
69
+ if (missing.length > 0)
70
+ return { ok: false, reason: "missing-inputs", missing };
71
+ if (!outputsFit(consumedState(state, recipe), layout, traits, recipe))
72
+ return { ok: false, reason: "no-output-space" };
73
+ return { ok: true };
74
+ }
75
+ export function craft(state, layout, traits, recipe, context = {}) {
76
+ const check = canCraft(state, layout, traits, recipe, context);
77
+ if (!check.ok) {
78
+ const { ok: _ok, ...rejection } = check;
79
+ return { status: "rejected", ...rejection };
80
+ }
81
+ let projected = consumedState(state, recipe);
82
+ for (const output of recipe.outputs) {
83
+ const put = putItem(projected, layout, traits, output.itemId, output.count);
84
+ if (put.status !== "ok")
85
+ return { status: "rejected", reason: "no-output-space" };
86
+ projected = put.state;
87
+ }
88
+ return { status: "ok", state: projected };
89
+ }
90
+ export function createRecipeGraph(defs = []) {
91
+ const byId = new Map();
92
+ for (const def of defs)
93
+ byId.set(def.id, def);
94
+ return {
95
+ all: () => defs,
96
+ get: (id) => byId.get(id) ?? null,
97
+ producing: (itemId) => defs.filter((r) => r.outputs.some((o) => o.itemId === itemId)),
98
+ using: (itemId) => defs.filter((r) => r.inputs.some((i) => i.itemId === itemId)),
99
+ category: (categoryId) => defs.filter((r) => r.category === categoryId),
100
+ };
101
+ }
@@ -0,0 +1,28 @@
1
+ import { type WalletState } from "./wallet.js";
2
+ export type WalletScope = {
3
+ kind: "user";
4
+ userId: string;
5
+ } | {
6
+ kind: "group";
7
+ groupId: string;
8
+ };
9
+ export declare function scopeKey(scope: WalletScope): string;
10
+ export declare function userScope(userId: string): WalletScope;
11
+ export declare function groupScope(groupId: string): WalletScope;
12
+ export interface WalletBook {
13
+ scopes: Readonly<Record<string, WalletState>>;
14
+ contributions: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, number>>>>>>;
15
+ }
16
+ export type BookChargeResult = {
17
+ status: "ok";
18
+ book: WalletBook;
19
+ } | {
20
+ status: "rejected";
21
+ reason: "insufficient-funds";
22
+ };
23
+ export declare function createWalletBook(): WalletBook;
24
+ export declare function balanceIn(book: WalletBook, scope: WalletScope, currency: string): number;
25
+ export declare function grantTo(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): WalletBook;
26
+ export declare function chargeFrom(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): BookChargeResult;
27
+ export declare function contributionOf(book: WalletBook, groupId: string, userId: string): Readonly<Record<string, number>>;
28
+ export declare function contributorsOf(book: WalletBook, groupId: string): string[];
@@ -0,0 +1,62 @@
1
+ import { balance, charge, createEmptyWallet, grant } from "./wallet.js";
2
+ export function scopeKey(scope) {
3
+ return scope.kind === "user" ? `user:${scope.userId}` : `group:${scope.groupId}`;
4
+ }
5
+ export function userScope(userId) {
6
+ return { kind: "user", userId };
7
+ }
8
+ export function groupScope(groupId) {
9
+ return { kind: "group", groupId };
10
+ }
11
+ export function createWalletBook() {
12
+ return { scopes: {}, contributions: {} };
13
+ }
14
+ function scopeState(book, key) {
15
+ return book.scopes[key] ?? createEmptyWallet();
16
+ }
17
+ export function balanceIn(book, scope, currency) {
18
+ return balance(scopeState(book, scopeKey(scope)), currency);
19
+ }
20
+ function withScope(book, key, state) {
21
+ return { ...book, scopes: { ...book.scopes, [key]: state } };
22
+ }
23
+ function recordContribution(book, groupKey, userId, currency, delta) {
24
+ const group = book.contributions[groupKey] ?? {};
25
+ const perUser = group[userId] ?? {};
26
+ const next = (perUser[currency] ?? 0) + delta;
27
+ return {
28
+ ...book,
29
+ contributions: {
30
+ ...book.contributions,
31
+ [groupKey]: {
32
+ ...group,
33
+ [userId]: { ...perUser, [currency]: next },
34
+ },
35
+ },
36
+ };
37
+ }
38
+ export function grantTo(book, scope, currency, amount, by) {
39
+ const key = scopeKey(scope);
40
+ let next = withScope(book, key, grant(scopeState(book, key), currency, amount));
41
+ if (scope.kind === "group" && by !== undefined) {
42
+ next = recordContribution(next, key, by, currency, amount);
43
+ }
44
+ return next;
45
+ }
46
+ export function chargeFrom(book, scope, currency, amount, by) {
47
+ const key = scopeKey(scope);
48
+ const result = charge(scopeState(book, key), currency, amount);
49
+ if (result.status === "rejected")
50
+ return result;
51
+ let next = withScope(book, key, result.state);
52
+ if (scope.kind === "group" && by !== undefined) {
53
+ next = recordContribution(next, key, by, currency, -amount);
54
+ }
55
+ return { status: "ok", book: next };
56
+ }
57
+ export function contributionOf(book, groupId, userId) {
58
+ return book.contributions[scopeKey(groupScope(groupId))]?.[userId] ?? {};
59
+ }
60
+ export function contributorsOf(book, groupId) {
61
+ return Object.keys(book.contributions[scopeKey(groupScope(groupId))] ?? {});
62
+ }
@@ -0,0 +1,40 @@
1
+ import type { UnlockDef, UnlockState } from "../game/unlocks.js";
2
+ export interface TechNodeDef extends UnlockDef {
3
+ requires?: readonly string[];
4
+ cost?: Readonly<Record<string, number>>;
5
+ grants?: readonly string[];
6
+ recipe?: string;
7
+ }
8
+ export type TechState = UnlockState;
9
+ export type TechRejection = {
10
+ reason: "unknown-node";
11
+ } | {
12
+ reason: "already-unlocked";
13
+ } | {
14
+ reason: "missing-prerequisites";
15
+ missing: readonly string[];
16
+ };
17
+ export type TechCheck = {
18
+ ok: true;
19
+ } | ({
20
+ ok: false;
21
+ } & TechRejection);
22
+ export declare function techPrerequisitesMet(state: TechState, node: TechNodeDef): boolean;
23
+ export declare function missingPrerequisites(state: TechState, node: TechNodeDef): string[];
24
+ export declare function canUnlockTech(defs: readonly TechNodeDef[], state: TechState, id: string): TechCheck;
25
+ export declare function grantTech(state: TechState, node: TechNodeDef): string[];
26
+ export declare function availableTech(defs: readonly TechNodeDef[], state: TechState): TechNodeDef[];
27
+ export declare function unlockedRecipes(defs: readonly TechNodeDef[], state: TechState): string[];
28
+ export interface TechTree {
29
+ has(userId: string, id: string): boolean;
30
+ canUnlock(userId: string, id: string): TechCheck;
31
+ unlock(userId: string, id: string): TechCheck;
32
+ available(userId: string): TechNodeDef[];
33
+ recipes(userId: string): string[];
34
+ list(userId: string): string[];
35
+ tree(categoryId: string): UnlockDef[];
36
+ node(id: string): TechNodeDef | null;
37
+ snapshot(userId: string): string[];
38
+ hydrate(userId: string, ids: string[]): void;
39
+ }
40
+ export declare function createTechTree(defs?: readonly TechNodeDef[]): TechTree;
@@ -0,0 +1,66 @@
1
+ import { createUnlocks, grantUnlock, hasUnlock, unlockTree } from "../game/unlocks.js";
2
+ export function techPrerequisitesMet(state, node) {
3
+ const requires = node.requires ?? [];
4
+ return requires.every((id) => hasUnlock(state, id));
5
+ }
6
+ export function missingPrerequisites(state, node) {
7
+ return (node.requires ?? []).filter((id) => !hasUnlock(state, id));
8
+ }
9
+ export function canUnlockTech(defs, state, id) {
10
+ const node = defs.find((n) => n.id === id);
11
+ if (node === undefined)
12
+ return { ok: false, reason: "unknown-node" };
13
+ if (hasUnlock(state, id))
14
+ return { ok: false, reason: "already-unlocked" };
15
+ const missing = missingPrerequisites(state, node);
16
+ if (missing.length > 0)
17
+ return { ok: false, reason: "missing-prerequisites", missing };
18
+ return { ok: true };
19
+ }
20
+ export function grantTech(state, node) {
21
+ let next = grantUnlock(state, node.id);
22
+ for (const extra of node.grants ?? [])
23
+ next = grantUnlock(next, extra);
24
+ return next;
25
+ }
26
+ export function availableTech(defs, state) {
27
+ return defs.filter((node) => !hasUnlock(state, node.id) && techPrerequisitesMet(state, node));
28
+ }
29
+ export function unlockedRecipes(defs, state) {
30
+ const recipes = [];
31
+ for (const node of defs) {
32
+ if (node.recipe !== undefined && hasUnlock(state, node.id))
33
+ recipes.push(node.recipe);
34
+ }
35
+ return recipes;
36
+ }
37
+ export function createTechTree(defs = []) {
38
+ const nodes = new Map();
39
+ for (const def of defs)
40
+ nodes.set(def.id, def);
41
+ const unlocks = createUnlocks(defs.map((d) => ({ id: d.id, category: d.category })));
42
+ function stateOf(userId) {
43
+ return unlocks.list(userId);
44
+ }
45
+ return {
46
+ has: (userId, id) => unlocks.has(userId, id),
47
+ canUnlock: (userId, id) => canUnlockTech(defs, stateOf(userId), id),
48
+ unlock(userId, id) {
49
+ const check = canUnlockTech(defs, stateOf(userId), id);
50
+ if (!check.ok)
51
+ return check;
52
+ const node = nodes.get(id);
53
+ unlocks.grant(userId, node.id);
54
+ for (const extra of node.grants ?? [])
55
+ unlocks.grant(userId, extra);
56
+ return { ok: true };
57
+ },
58
+ available: (userId) => availableTech(defs, stateOf(userId)),
59
+ recipes: (userId) => unlockedRecipes(defs, stateOf(userId)),
60
+ list: (userId) => unlocks.list(userId),
61
+ tree: (categoryId) => unlockTree(defs, categoryId),
62
+ node: (id) => nodes.get(id) ?? null,
63
+ snapshot: (userId) => unlocks.snapshot(userId),
64
+ hydrate: (userId, ids) => unlocks.hydrate(userId, ids),
65
+ };
66
+ }
@@ -0,0 +1,25 @@
1
+ export interface CosmeticLoadoutDef {
2
+ slots: Record<string, string>;
3
+ }
4
+ export interface CosmeticsChangedEvent {
5
+ userId: string;
6
+ slots: Record<string, string>;
7
+ }
8
+ export interface CosmeticsEvents {
9
+ emit(name: "cosmetics.changed", payload: CosmeticsChangedEvent): void;
10
+ }
11
+ export interface CosmeticsDeps {
12
+ events?: CosmeticsEvents;
13
+ }
14
+ export interface Cosmetics {
15
+ register(defs: Record<string, CosmeticLoadoutDef>): void;
16
+ has(loadoutId: string): boolean;
17
+ apply(userId: string, loadoutId: string): {
18
+ reason: string;
19
+ } | null;
20
+ equip(userId: string, slot: string, cosmeticId: string | null): void;
21
+ get(userId: string): Readonly<Record<string, string>>;
22
+ snapshot(userId: string): Record<string, string>;
23
+ hydrate(userId: string, state: Record<string, string>): void;
24
+ }
25
+ export declare function createCosmetics(deps?: CosmeticsDeps): Cosmetics;
@@ -0,0 +1,51 @@
1
+ const EMPTY_SLOTS = Object.freeze({});
2
+ export function createCosmetics(deps = {}) {
3
+ const definitions = new Map();
4
+ const equipped = new Map();
5
+ function slotsOf(userId) {
6
+ let slots = equipped.get(userId);
7
+ if (slots === undefined) {
8
+ slots = {};
9
+ equipped.set(userId, slots);
10
+ }
11
+ return slots;
12
+ }
13
+ function notify(userId) {
14
+ deps.events?.emit("cosmetics.changed", { userId, slots: { ...slotsOf(userId) } });
15
+ }
16
+ return {
17
+ register(defs) {
18
+ for (const [loadoutId, def] of Object.entries(defs))
19
+ definitions.set(loadoutId, def);
20
+ },
21
+ has(loadoutId) {
22
+ return definitions.has(loadoutId);
23
+ },
24
+ apply(userId, loadoutId) {
25
+ const def = definitions.get(loadoutId);
26
+ if (def === undefined)
27
+ return { reason: `unknown cosmetic loadout "${loadoutId}"` };
28
+ const slots = slotsOf(userId);
29
+ Object.assign(slots, def.slots);
30
+ notify(userId);
31
+ return null;
32
+ },
33
+ equip(userId, slot, cosmeticId) {
34
+ const slots = slotsOf(userId);
35
+ if (cosmeticId === null)
36
+ delete slots[slot];
37
+ else
38
+ slots[slot] = cosmeticId;
39
+ notify(userId);
40
+ },
41
+ get(userId) {
42
+ return equipped.get(userId) ?? EMPTY_SLOTS;
43
+ },
44
+ snapshot(userId) {
45
+ return { ...slotsOf(userId) };
46
+ },
47
+ hydrate(userId, state) {
48
+ equipped.set(userId, { ...state });
49
+ },
50
+ };
51
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ActionCodesMap } from "../input/actionBindings.js";
2
2
  import type { ItemTraits } from "../inventory/inventoryModel.js";
3
+ import type { StorageTier } from "../inventory/storageTier.js";
3
4
  import type { SaveConfig } from "../runtime/save.js";
4
5
  import type { AssetCatalog, ModelAssetRef } from "../scene/assetCatalog.js";
5
6
  import { type EntityStore } from "../scene/entityStore.js";
@@ -14,6 +15,7 @@ export interface InventoryDeclaration {
14
15
  accepts?: string | readonly string[];
15
16
  traits?: ItemTraits;
16
17
  applyModifiers?: boolean;
18
+ tier?: StorageTier;
17
19
  }
18
20
  export type GameServerConfig = "persistent" | {
19
21
  mode: string;
@@ -1,3 +1,5 @@
1
+ import type { TelegraphShape } from "../combat/telegraph.js";
2
+ import type { CameraShake } from "../combat/hitReaction.js";
1
3
  export type DeathReason = {
2
4
  kind: "player_kill";
3
5
  killerUserId: string;
@@ -72,6 +74,24 @@ export interface EntityFloatTextEvent {
72
74
  text: string;
73
75
  kind: string;
74
76
  amount?: number;
77
+ hitType?: string;
78
+ element?: string;
79
+ crit?: boolean;
80
+ scale?: number;
81
+ }
82
+ export interface CombatTelegraphEvent {
83
+ id: number;
84
+ shape: TelegraphShape;
85
+ position: [number, number, number];
86
+ dir?: number;
87
+ windupMs: number;
88
+ kind: string;
89
+ }
90
+ export interface CombatHitReactionEvent {
91
+ instanceId?: string;
92
+ position: [number, number, number];
93
+ hitstopMs: number;
94
+ shake?: CameraShake;
75
95
  }
76
96
  export interface ProjectileSettledEvent {
77
97
  from: string;
@@ -80,9 +100,45 @@ export interface ProjectileSettledEvent {
80
100
  effect: string;
81
101
  hit: boolean;
82
102
  }
103
+ export interface WorldItemDroppedEvent {
104
+ instanceId: string;
105
+ itemId: string;
106
+ rarity: string;
107
+ count: number;
108
+ position: [number, number, number];
109
+ source?: string;
110
+ }
111
+ export interface WorldItemPickedUpEvent {
112
+ instanceId: string;
113
+ userId: string;
114
+ itemId: string;
115
+ rarity: string;
116
+ count: number;
117
+ }
118
+ export interface CosmeticsChangedEvent {
119
+ userId: string;
120
+ slots: Record<string, string>;
121
+ }
122
+ export interface EmotePlayedEvent {
123
+ from: string;
124
+ emoteId: string;
125
+ at: readonly [number, number, number];
126
+ recipients: readonly string[];
127
+ }
128
+ export interface PossessionSwappedEvent {
129
+ userId: string;
130
+ entityId: string;
131
+ previousEntityId: string;
132
+ }
133
+ export interface FormChangedEvent {
134
+ instanceId: string;
135
+ formId: string | null;
136
+ }
83
137
  export interface GameEventMap {
84
138
  "entity.died": EntityDiedEvent;
85
139
  "entity.floatText": EntityFloatTextEvent;
140
+ "combat.telegraph": CombatTelegraphEvent;
141
+ "combat.hitReaction": CombatHitReactionEvent;
86
142
  "loot.granted": LootGrantedEvent;
87
143
  "inventory.added": InventoryAddedEvent;
88
144
  "quest.accepted": QuestAcceptedEvent;
@@ -93,6 +149,12 @@ export interface GameEventMap {
93
149
  "social.party.left": SocialPartyLeftEvent;
94
150
  "stat.levelUp": StatLevelUpEvent;
95
151
  "projectile.settled": ProjectileSettledEvent;
152
+ "worldItem.dropped": WorldItemDroppedEvent;
153
+ "worldItem.picked_up": WorldItemPickedUpEvent;
154
+ "cosmetics.changed": CosmeticsChangedEvent;
155
+ "emote.played": EmotePlayedEvent;
156
+ "possession.swapped": PossessionSwappedEvent;
157
+ "form.changed": FormChangedEvent;
96
158
  }
97
159
  export type GameEventHandler<TPayload> = (payload: TPayload) => void;
98
160
  export interface GameEvents<TMap extends GameEventMap = GameEventMap> {