@jgengine/core 0.5.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 (244) hide show
  1. package/CHANGELOG.md +191 -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 +5 -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 +291 -5
  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 +53 -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 +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  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 +66 -2
  128. package/dist/runtime/gameContext.js +157 -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/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -0,0 +1,54 @@
1
+ export type AffixOp = "add" | "mul";
2
+ export interface AffixDef {
3
+ id: string;
4
+ stat: string;
5
+ op?: AffixOp;
6
+ roll: number | [number, number];
7
+ weight: number;
8
+ namePart?: {
9
+ position: "prefix" | "suffix";
10
+ text: string;
11
+ };
12
+ }
13
+ export interface AffixPool {
14
+ id: string;
15
+ affixes: readonly AffixDef[];
16
+ }
17
+ export interface RarityTier {
18
+ id: string;
19
+ weight: number;
20
+ affixCount: number | [number, number];
21
+ statScale?: number;
22
+ namePart?: string;
23
+ pools?: readonly string[];
24
+ }
25
+ export interface ItemBaseDef {
26
+ id: string;
27
+ name: string;
28
+ baseStats: Record<string, number>;
29
+ pools: readonly string[];
30
+ }
31
+ export interface RollerConfig {
32
+ pools: readonly AffixPool[];
33
+ rarities: readonly RarityTier[];
34
+ }
35
+ export interface RolledAffix {
36
+ id: string;
37
+ stat: string;
38
+ op: AffixOp;
39
+ value: number;
40
+ }
41
+ export interface RolledItem {
42
+ baseId: string;
43
+ rarity: string;
44
+ name: string;
45
+ affixes: readonly RolledAffix[];
46
+ stats: Record<string, number>;
47
+ }
48
+ export declare function seededRng(seed: string | number): () => number;
49
+ export interface AffixRoller {
50
+ rollRarity(rng: () => number): RarityTier;
51
+ roll(base: ItemBaseDef, rarityId: string, rng: () => number): RolledItem;
52
+ rollRandom(base: ItemBaseDef, rng: () => number): RolledItem;
53
+ }
54
+ export declare function createAffixRoller(config: RollerConfig): AffixRoller;
@@ -0,0 +1,133 @@
1
+ import { pickWeighted } from "../world/scatterItems.js";
2
+ function hashSeed(seed) {
3
+ const text = typeof seed === "number" ? seed.toString() : seed;
4
+ let h = 2166136261 >>> 0;
5
+ for (let i = 0; i < text.length; i++) {
6
+ h ^= text.charCodeAt(i);
7
+ h = Math.imul(h, 16777619);
8
+ }
9
+ return h >>> 0;
10
+ }
11
+ export function seededRng(seed) {
12
+ let a = hashSeed(seed) >>> 0;
13
+ return () => {
14
+ a = (a + 0x6d2b79f5) >>> 0;
15
+ let t = a;
16
+ t = Math.imul(t ^ (t >>> 15), t | 1);
17
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
18
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
19
+ };
20
+ }
21
+ function resolveRoll(roll, rng) {
22
+ if (typeof roll === "number")
23
+ return roll;
24
+ const [min, max] = roll;
25
+ return min + rng() * (max - min);
26
+ }
27
+ function resolveCount(count, rng) {
28
+ if (typeof count === "number")
29
+ return count;
30
+ const [min, max] = count;
31
+ return min + Math.floor(rng() * (max - min + 1));
32
+ }
33
+ function assertConfig(config) {
34
+ if (config.rarities.length === 0)
35
+ throw new Error("affix roller must have at least one rarity tier");
36
+ for (const tier of config.rarities) {
37
+ if (!(tier.weight > 0))
38
+ throw new Error(`rarity "${tier.id}" weight must be positive`);
39
+ }
40
+ }
41
+ export function createAffixRoller(config) {
42
+ assertConfig(config);
43
+ const poolById = new Map(config.pools.map((p) => [p.id, p]));
44
+ const rarityById = new Map(config.rarities.map((r) => [r.id, r]));
45
+ const rarityEntries = config.rarities.map((value) => ({ value, weight: value.weight }));
46
+ function candidatesFor(base, rarity) {
47
+ const allowed = rarity.pools;
48
+ const out = [];
49
+ for (const poolId of base.pools) {
50
+ if (allowed !== undefined && !allowed.includes(poolId))
51
+ continue;
52
+ const pool = poolById.get(poolId);
53
+ if (pool === undefined)
54
+ continue;
55
+ for (const affix of pool.affixes)
56
+ out.push(affix);
57
+ }
58
+ return out;
59
+ }
60
+ function drawAffixes(candidates, count, rng) {
61
+ const remaining = candidates.slice();
62
+ const drawn = [];
63
+ const target = Math.min(count, remaining.length);
64
+ for (let i = 0; i < target; i++) {
65
+ const picked = pickWeighted(remaining.map((value) => ({ value, weight: value.weight })), rng());
66
+ if (picked === null)
67
+ break;
68
+ remaining.splice(remaining.indexOf(picked), 1);
69
+ drawn.push({ id: picked.id, stat: picked.stat, op: picked.op ?? "add", value: resolveRoll(picked.roll, rng) });
70
+ }
71
+ return drawn;
72
+ }
73
+ function computeStats(base, rarity, affixes) {
74
+ const scale = rarity.statScale ?? 1;
75
+ const stats = {};
76
+ for (const [key, value] of Object.entries(base.baseStats))
77
+ stats[key] = value * scale;
78
+ for (const affix of affixes) {
79
+ if (affix.op === "add")
80
+ stats[affix.stat] = (stats[affix.stat] ?? 0) + affix.value;
81
+ }
82
+ for (const affix of affixes) {
83
+ if (affix.op === "mul")
84
+ stats[affix.stat] = (stats[affix.stat] ?? 0) * affix.value;
85
+ }
86
+ return stats;
87
+ }
88
+ function composeName(base, rarity, affixes) {
89
+ const prefix = affixes.find((a) => affixNameFor(a)?.position === "prefix");
90
+ const suffix = affixes.find((a) => affixNameFor(a)?.position === "suffix");
91
+ const parts = [];
92
+ if (rarity.namePart !== undefined)
93
+ parts.push(rarity.namePart);
94
+ if (prefix !== undefined)
95
+ parts.push(affixNameFor(prefix).text);
96
+ parts.push(base.name);
97
+ if (suffix !== undefined)
98
+ parts.push(affixNameFor(suffix).text);
99
+ return parts.join(" ");
100
+ }
101
+ const nameByAffixId = new Map();
102
+ for (const pool of config.pools) {
103
+ for (const affix of pool.affixes)
104
+ if (affix.namePart !== undefined)
105
+ nameByAffixId.set(affix.id, affix.namePart);
106
+ }
107
+ function affixNameFor(affix) {
108
+ return nameByAffixId.get(affix.id);
109
+ }
110
+ return {
111
+ rollRarity(rng) {
112
+ const picked = pickWeighted(rarityEntries, rng());
113
+ return picked ?? config.rarities[config.rarities.length - 1];
114
+ },
115
+ roll(base, rarityId, rng) {
116
+ const rarity = rarityById.get(rarityId);
117
+ if (rarity === undefined)
118
+ throw new Error(`unknown rarity tier: ${rarityId}`);
119
+ const count = resolveCount(rarity.affixCount, rng);
120
+ const affixes = drawAffixes(candidatesFor(base, rarity), count, rng);
121
+ return {
122
+ baseId: base.id,
123
+ rarity: rarity.id,
124
+ name: composeName(base, rarity, affixes),
125
+ affixes,
126
+ stats: computeStats(base, rarity, affixes),
127
+ };
128
+ },
129
+ rollRandom(base, rng) {
130
+ return this.roll(base, this.rollRarity(rng).id, rng);
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,51 @@
1
+ export interface RepairMaterial {
2
+ item: string;
3
+ perPoint: number;
4
+ }
5
+ export interface RepairSpec {
6
+ materials: readonly RepairMaterial[];
7
+ station?: string;
8
+ qualityLossPerRepair?: number;
9
+ }
10
+ export interface DurabilitySpec {
11
+ max: number;
12
+ wearPerUse?: number;
13
+ wearPerHit?: number;
14
+ disableAtZero?: boolean;
15
+ repair?: RepairSpec;
16
+ }
17
+ export interface DurabilityState {
18
+ current: number;
19
+ max: number;
20
+ }
21
+ export type WearKind = "use" | "hit";
22
+ export interface RepairCost {
23
+ item: string;
24
+ count: number;
25
+ }
26
+ export interface RepairQuote {
27
+ materials: readonly RepairCost[];
28
+ restored: number;
29
+ state: DurabilityState;
30
+ }
31
+ export declare function createDurability(spec: DurabilitySpec): DurabilityState;
32
+ export declare function durabilityFraction(state: DurabilityState): number;
33
+ export declare function isBroken(state: DurabilityState): boolean;
34
+ export declare function isDisabled(spec: DurabilitySpec, state: DurabilityState): boolean;
35
+ export declare function wearAmount(spec: DurabilitySpec, kind: WearKind): number;
36
+ export declare function applyWear(state: DurabilityState, amount: number): DurabilityState;
37
+ export declare function wear(spec: DurabilitySpec, state: DurabilityState, kind: WearKind, times?: number): DurabilityState;
38
+ export declare function canRepairAt(spec: DurabilitySpec, stationId?: string): boolean;
39
+ export declare function repairQuote(spec: DurabilitySpec, state: DurabilityState, options?: {
40
+ to?: number;
41
+ station?: string;
42
+ }): RepairQuote | null;
43
+ export interface DurabilityTracker {
44
+ init(instanceId: string, spec: DurabilitySpec): DurabilityState;
45
+ get(instanceId: string): DurabilityState | null;
46
+ set(instanceId: string, state: DurabilityState): void;
47
+ wear(instanceId: string, spec: DurabilitySpec, kind: WearKind, times?: number): DurabilityState | null;
48
+ isDisabled(instanceId: string, spec: DurabilitySpec): boolean;
49
+ remove(instanceId: string): void;
50
+ }
51
+ export declare function createDurabilityTracker(): DurabilityTracker;
@@ -0,0 +1,78 @@
1
+ function clamp(value, min, max) {
2
+ return value < min ? min : value > max ? max : value;
3
+ }
4
+ export function createDurability(spec) {
5
+ const max = Math.max(0, spec.max);
6
+ return { current: max, max };
7
+ }
8
+ export function durabilityFraction(state) {
9
+ return state.max <= 0 ? 0 : clamp(state.current / state.max, 0, 1);
10
+ }
11
+ export function isBroken(state) {
12
+ return state.current <= 0;
13
+ }
14
+ export function isDisabled(spec, state) {
15
+ return isBroken(state) && spec.disableAtZero !== false;
16
+ }
17
+ export function wearAmount(spec, kind) {
18
+ return kind === "use" ? spec.wearPerUse ?? 0 : spec.wearPerHit ?? 0;
19
+ }
20
+ export function applyWear(state, amount) {
21
+ if (amount <= 0)
22
+ return state;
23
+ return { current: clamp(state.current - amount, 0, state.max), max: state.max };
24
+ }
25
+ export function wear(spec, state, kind, times = 1) {
26
+ return applyWear(state, wearAmount(spec, kind) * Math.max(0, times));
27
+ }
28
+ export function canRepairAt(spec, stationId) {
29
+ const repair = spec.repair;
30
+ if (repair === undefined)
31
+ return false;
32
+ return repair.station === undefined || repair.station === stationId;
33
+ }
34
+ export function repairQuote(spec, state, options) {
35
+ const repair = spec.repair;
36
+ if (repair === undefined)
37
+ return null;
38
+ if (repair.station !== undefined && repair.station !== options?.station)
39
+ return null;
40
+ const loss = repair.qualityLossPerRepair ?? 0;
41
+ const newMax = Math.max(1, state.max - loss);
42
+ const capped = Math.min(state.current, newMax);
43
+ const target = clamp(options?.to ?? newMax, capped, newMax);
44
+ const restored = Math.max(0, target - capped);
45
+ const materials = repair.materials.map((m) => ({ item: m.item, count: Math.ceil(m.perPoint * restored) }));
46
+ return { materials, restored, state: { current: target, max: newMax } };
47
+ }
48
+ export function createDurabilityTracker() {
49
+ const states = new Map();
50
+ return {
51
+ init(instanceId, spec) {
52
+ const state = createDurability(spec);
53
+ states.set(instanceId, state);
54
+ return state;
55
+ },
56
+ get(instanceId) {
57
+ return states.get(instanceId) ?? null;
58
+ },
59
+ set(instanceId, state) {
60
+ states.set(instanceId, state);
61
+ },
62
+ wear(instanceId, spec, kind, times) {
63
+ const current = states.get(instanceId);
64
+ if (current === undefined)
65
+ return null;
66
+ const next = wear(spec, current, kind, times);
67
+ states.set(instanceId, next);
68
+ return next;
69
+ },
70
+ isDisabled(instanceId, spec) {
71
+ const state = states.get(instanceId);
72
+ return state !== undefined && isDisabled(spec, state);
73
+ },
74
+ remove(instanceId) {
75
+ states.delete(instanceId);
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,46 @@
1
+ export interface MountSlotDef {
2
+ id: string;
3
+ accepts: string | readonly string[];
4
+ required?: boolean;
5
+ }
6
+ export interface ModularItemDef {
7
+ id: string;
8
+ baseStats: Record<string, number>;
9
+ slots: readonly MountSlotDef[];
10
+ }
11
+ export interface PartDef {
12
+ id: string;
13
+ category: string;
14
+ stats?: Record<string, number>;
15
+ multipliers?: Record<string, number>;
16
+ }
17
+ export interface InstalledPart {
18
+ slotId: string;
19
+ part: PartDef;
20
+ }
21
+ export type InstallResult = {
22
+ status: "ok";
23
+ installed: readonly InstalledPart[];
24
+ } | {
25
+ status: "rejected";
26
+ reason: "unknown-slot" | "wrong-category" | "slot-occupied";
27
+ };
28
+ export declare function slotById(def: ModularItemDef, slotId: string): MountSlotDef | null;
29
+ export declare function slotAccepts(slot: MountSlotDef, category: string): boolean;
30
+ export declare function install(def: ModularItemDef, installed: readonly InstalledPart[], slotId: string, part: PartDef): InstallResult;
31
+ export declare function uninstall(installed: readonly InstalledPart[], slotId: string): readonly InstalledPart[];
32
+ export declare function partInSlot(installed: readonly InstalledPart[], slotId: string): PartDef | null;
33
+ export declare function computeEffectiveStats(def: ModularItemDef, installed: readonly InstalledPart[]): Record<string, number>;
34
+ export declare function missingRequiredSlots(def: ModularItemDef, installed: readonly InstalledPart[]): string[];
35
+ export declare function isComplete(def: ModularItemDef, installed: readonly InstalledPart[]): boolean;
36
+ export interface ModularItem {
37
+ readonly def: ModularItemDef;
38
+ parts(): readonly InstalledPart[];
39
+ install(slotId: string, part: PartDef): InstallResult;
40
+ uninstall(slotId: string): readonly InstalledPart[];
41
+ partInSlot(slotId: string): PartDef | null;
42
+ effectiveStats(): Record<string, number>;
43
+ missingRequired(): string[];
44
+ isComplete(): boolean;
45
+ }
46
+ export declare function createModularItem(def: ModularItemDef, initial?: readonly InstalledPart[]): ModularItem;
@@ -0,0 +1,75 @@
1
+ export function slotById(def, slotId) {
2
+ return def.slots.find((s) => s.id === slotId) ?? null;
3
+ }
4
+ export function slotAccepts(slot, category) {
5
+ return typeof slot.accepts === "string" ? slot.accepts === category : slot.accepts.includes(category);
6
+ }
7
+ export function install(def, installed, slotId, part) {
8
+ const slot = slotById(def, slotId);
9
+ if (slot === null)
10
+ return { status: "rejected", reason: "unknown-slot" };
11
+ if (!slotAccepts(slot, part.category))
12
+ return { status: "rejected", reason: "wrong-category" };
13
+ if (installed.some((i) => i.slotId === slotId))
14
+ return { status: "rejected", reason: "slot-occupied" };
15
+ return { status: "ok", installed: [...installed, { slotId, part }] };
16
+ }
17
+ export function uninstall(installed, slotId) {
18
+ return installed.filter((i) => i.slotId !== slotId);
19
+ }
20
+ export function partInSlot(installed, slotId) {
21
+ return installed.find((i) => i.slotId === slotId)?.part ?? null;
22
+ }
23
+ export function computeEffectiveStats(def, installed) {
24
+ const stats = { ...def.baseStats };
25
+ for (const { part } of installed) {
26
+ if (part.stats === undefined)
27
+ continue;
28
+ for (const [key, value] of Object.entries(part.stats))
29
+ stats[key] = (stats[key] ?? 0) + value;
30
+ }
31
+ for (const { part } of installed) {
32
+ if (part.multipliers === undefined)
33
+ continue;
34
+ for (const [key, factor] of Object.entries(part.multipliers))
35
+ stats[key] = (stats[key] ?? 0) * factor;
36
+ }
37
+ return stats;
38
+ }
39
+ export function missingRequiredSlots(def, installed) {
40
+ return def.slots.filter((s) => s.required === true && !installed.some((i) => i.slotId === s.id)).map((s) => s.id);
41
+ }
42
+ export function isComplete(def, installed) {
43
+ return missingRequiredSlots(def, installed).length === 0;
44
+ }
45
+ export function createModularItem(def, initial = []) {
46
+ let installed = initial;
47
+ return {
48
+ def,
49
+ parts() {
50
+ return installed;
51
+ },
52
+ install(slotId, part) {
53
+ const result = install(def, installed, slotId, part);
54
+ if (result.status === "ok")
55
+ installed = result.installed;
56
+ return result;
57
+ },
58
+ uninstall(slotId) {
59
+ installed = uninstall(installed, slotId);
60
+ return installed;
61
+ },
62
+ partInSlot(slotId) {
63
+ return partInSlot(installed, slotId);
64
+ },
65
+ effectiveStats() {
66
+ return computeEffectiveStats(def, installed);
67
+ },
68
+ missingRequired() {
69
+ return missingRequiredSlots(def, installed);
70
+ },
71
+ isComplete() {
72
+ return isComplete(def, installed);
73
+ },
74
+ };
75
+ }
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.7.0";
2
2
  export interface ChangelogEntry {
3
3
  migrate: readonly string[];
4
4
  added: readonly string[];
@@ -1,5 +1,57 @@
1
- export const VERSION = "0.5.0";
1
+ export const VERSION = "0.7.0";
2
2
  export const CHANGELOG = {
3
+ "0.7.0": {
4
+ migrate: [
5
+ "Bump every @jgengine/* dependency to ^0.7.0 (the eight packages version in lockstep).",
6
+ "0.7.0 is additive — every 0.6.0 API is unchanged, so no code change is required to upgrade; existing games keep the orbit/first-person camera, single-player-entity control, and every existing primitive exactly as before.",
7
+ "Opt into any new system by importing it directly: a camera rig via camera.rig; a sensor/* probe with @jgengine/shell/vision renderers; an ai/* director over the nav/ navmesh; turn/* + tactics/* for turn-based games; cards/* + board/* for deckbuilders; crafting/* for recipes/production/farming; survival/* + world/{envField,weather,realm} for survival; combat/{abilityKit,animationState,defensiveWindow} for action feel; physics/{vehicleBody,traversal,structure,ragdoll} for vehicles/destruction; session/* for contested/round/downed/ring/extraction machines; and multiplayer/* for lag-comp/hidden-commit/matchmaking.",
8
+ "entityStore.update() now also accepts name, so possession/form can retarget an instance's catalog id without despawn/respawn.",
9
+ ],
10
+ added: [
11
+ "Navigation & pointer input — nav/navGrid (A* walkable grid), nav/pathFollow, input/pointer (PointerHit), scene/selection, interaction/contextMenu, and PlayableGame.pointer (worldHit, marquee select, right-click verbs, move/aim commands) in @jgengine/shell.",
12
+ "Camera rig library — eight rigs on PlayableGame.camera (topDown/iso, rts, shoulder, lockOn, chase + cockpit/hood/rear, orbit, first), a cinematic keyframe path, rig cross-fade, followEntityId: null, and a shared camera-shake/trauma channel (cameraShake / useCameraShake).",
13
+ "Physics constraints & actors — PhysicsWorld joints (hinge/fixed/distance/spring), onCollision gameplay-event hook, physics/ragdoll, physics/carryable, physics/forceVolume + platformCarry, and physics/spatialGrid broad-phase.",
14
+ "Vehicles, mounts & racing — physics/vehicleBody (suspension + grip curve), input/axisInput, physics/buoyancy boat, scene/mount multi-seat rideable, physics/damageZones crash stages, and game/race (raceTrack + raceState).",
15
+ "Traversal & destruction — physics/traversal (grapple/glide), world/carve (voxel carve/deposit + terrain write-back), and physics/structure (integrity graph → debris collapse).",
16
+ "AI director/behavior/crowds — ai/spawnDirector, ai/threat, ai/jobBoard, ai/crowd flow field, and a scene/behaviors patrol descriptor over the navmesh.",
17
+ "Abilities, resources & cooldowns — combat/abilityKit (four-state slots), stats/accumulatorMeter + stats/eventMeter (ult/streak), scene/autoTarget, combat/resistance matrix, game/runDraft, and @jgengine/react useAbilitySlots / useEventMeter.",
18
+ "Character combat feel — combat/animationState, combat/attackTags, combat/defensiveWindow, combat/comboString, combat/breakMeters (stagger/buildup), movement/dash, combat/hitReaction (hitstop + shake), combat/telegraph, and typed float-text styling.",
19
+ "Item & gear — item/durability, item/affix roller, item/modularItem, and inventory/storageTier (carried vs banked + delivery/insurance/consolation).",
20
+ "Objective/mode/session machines — session/contestedChannel, session/roundState, combat/downed, session/ring, session/extraction, and runtime/persistenceScope (run-vs-meta split with HostPersistence.resetScenario).",
21
+ "Turn-based & tactics — turn/turnLoop (+ action-economy pools), turn/commit (immediate/simultaneous/rewind), tactics/tacticalGrid, tactics/predictiveQuery, tactics/snapshot, tactics/surface, and shared combat/effects resolveAreaTargets.",
22
+ "Card & board stack — cards/cardPile, cards/modifierPipeline, board/laneBoard, board/timelineBoard, inventory/shapedGrid, and @jgengine/react dragLayer.",
23
+ "Crafting, tech & production — crafting/recipe graph, economy/techTree (prereq-gated unlocks), crafting/production buildings + conveyor/power, and crafting/crop farming state machine.",
24
+ "Survival & environment — survival/decayMeter, survival/moodle, survival/regionHealth, world/envField, world/weather (+ coarse fire grid), world/realm composition, and @jgengine/shell FireSpreadLayer.",
25
+ "World items & loot — game/worldItem (third scene bucket), game/lootFilter, onDeath.dropMode world scatter, PlayableGame.worldItem render binding, and @jgengine/react useWorldItems / useNearestWorldItem.",
26
+ "Placement, building & terraform — world/placementController, world/connectors, world/support, world/walls, world/placedStructureStore, world/terraform (editable terrain write-back), world/buildPermissions, and @jgengine/shell ghost/tint/brush renderers.",
27
+ "Map, HUD & ping — world/markers, world/fog, world/minimap, game/ping, @jgengine/react Minimap/Compass/WorldMap + useMarkers/useFog, and @jgengine/shell bakeTerrainMap + MapMarkerBeacons.",
28
+ "Audio & voice — audio/audioFalloff contract, time/beatClock, @jgengine/shell positional emitters (PlayableGame.audio + entitySounds/objectSounds), and @jgengine/ws voiceChannel router.",
29
+ "Interaction verbs & minigames — interaction/skillCheck, interaction/qte, scene/captureCheck + scene/roster, stats/rollCheck, dialogue skill-check gates, and @jgengine/react SkillCheckBar/QteTrack/CaptureOdds + useRoster.",
30
+ "Sensors, vision & observer — sensor/revealQuery, sensor/hiddenStateProbe, sensor/frustumSensor, sensor/recordingBuffer, the observer camera rig, and @jgengine/shell vision/replay renderers.",
31
+ "Player embodiment & expression — scene/possession, scene/form (shapeshift), game/cosmetics loadouts, and social emote broadcast over the presence layer.",
32
+ "Multiplayer depth — multiplayer/lagCompensation (position history + rewind), multiplayer/simultaneousCommit, multiplayer/combatSnapshot replay, scene/stationClaim shared-vehicle facets, economy/sharedWallet, and multiplayer/matchmaking (browse/filter/join-by-code) with @jgengine/node host support.",
33
+ ],
34
+ changed: [],
35
+ removed: [],
36
+ },
37
+ "0.6.0": {
38
+ migrate: [
39
+ "Bump every @jgengine/* dependency to ^0.6.0 (the eight packages version in lockstep).",
40
+ "0.6.0 is additive — every 0.5.0 API is unchanged, so no code change is required to upgrade.",
41
+ "Optional: mount an outdoor scene by handing environment({ terrain, weather, vegetation, water, structures }) from @jgengine/core/world/features to the shell — @jgengine/shell EnvironmentScene renders terrain, rain/snow, grass, ocean, and buildings from it, while gameplay reads the same world through the renderer-free query primitives (resolveTerrainField, windField, waterSurface, scatter, buildingIndex).",
42
+ ],
43
+ added: [
44
+ "@jgengine/core/world query primitives — renderer-free world sampling so gameplay reads the same world the shell renders: world/terrain (TerrainField, noiseField, resolveTerrainField, resolveGroundStep), world/wind (windField), world/water (waterSurface, waterSurfaceFromDescriptor, CPU Gerstner), world/scatter (scatter — seeded overlap-aware point distribution), world/buildings (generateBuilding, generateBuildingDistrict) and world/buildingIndex (buildingIndex — at/within/nearest/isInside/blockers).",
45
+ "@jgengine/core/world/features gained composable outdoor descriptors: environment(), terrain(), rain(), snow(), grass(), ocean(), building().",
46
+ "@jgengine/core/world/regions (createRegionField — blend content-agnostic biomes by nearest selector, extends TerrainField so it ground-snaps) and world/scatterItems (scatterItems — region-driven, grounded, above-water/slope-aware content scatter with pickWeighted).",
47
+ "@jgengine/core/physics/physicsWorld — standalone fixed-capacity rigid-body sim (SoA buffers, spatial-hash broadphase, sleeping) for many colliding dynamic bodies; independent of the defineGame physics character-controller config.",
48
+ "ctx.time simulation clock (@jgengine/core/time/simClock) — dt in onTick is game time, so decay/regen/AI written as rate*dt obey pause and fast-forward for free. Configure via defineGame({ time: { scale?, speeds?, dayLength?, start?, startPaused? } }); pause/play/setSpeed/calendar controls, and useGameClock() in React.",
49
+ "@jgengine/shell environment/terrain/water/weather/structures renderers — EnvironmentScene mounts an environment() descriptor; GrassField, ProceduralGround, Ocean, RainField, SnowField, LightningStrike, GeneratedBuilding, and world/InstancedBodies (renders PhysicsWorld bodies).",
50
+ "@jgengine/react/liveBind — useFrameBind drives a DOM/SVG element from a per-frame value without re-rendering React.",
51
+ ],
52
+ changed: [],
53
+ removed: [],
54
+ },
3
55
  "0.5.0": {
4
56
  migrate: [
5
57
  "Bump every @jgengine/* dependency to ^0.5.0 (the eight packages version in lockstep).",
@@ -0,0 +1,37 @@
1
+ export interface DashConfig {
2
+ distance: number;
3
+ durationMs: number;
4
+ iframes: {
5
+ fromMs: number;
6
+ toMs: number;
7
+ };
8
+ staminaCost: number;
9
+ staminaMax: number;
10
+ staminaRegenPerSecond: number;
11
+ cooldownMs: number;
12
+ }
13
+ export interface DashDirection {
14
+ x: number;
15
+ z: number;
16
+ }
17
+ export type DashRejection = {
18
+ reason: "no-stamina" | "cooldown" | "dashing";
19
+ };
20
+ export interface DashBurst {
21
+ direction: DashDirection;
22
+ durationMs: number;
23
+ distance: number;
24
+ }
25
+ export declare function iframeActive(config: DashConfig, elapsedMs: number): boolean;
26
+ export declare function dashEase(t: number): number;
27
+ export declare function dashOffset(config: DashConfig, dir: DashDirection, elapsedMs: number): [number, number, number];
28
+ export interface DashState {
29
+ tryDash(direction: DashDirection, nowMs: number): DashRejection | DashBurst;
30
+ tick(dtSeconds: number, nowMs: number): void;
31
+ stamina(): number;
32
+ staminaFraction(): number;
33
+ isDashing(nowMs: number): boolean;
34
+ isInvulnerable(nowMs: number): boolean;
35
+ offset(nowMs: number): [number, number, number];
36
+ }
37
+ export declare function createDashState(config: DashConfig): DashState;
@@ -0,0 +1,65 @@
1
+ export function iframeActive(config, elapsedMs) {
2
+ return elapsedMs >= config.iframes.fromMs && elapsedMs < config.iframes.toMs;
3
+ }
4
+ export function dashEase(t) {
5
+ const clamped = Math.max(0, Math.min(1, t));
6
+ return 1 - (1 - clamped) * (1 - clamped);
7
+ }
8
+ export function dashOffset(config, dir, elapsedMs) {
9
+ const traveled = dashEase(elapsedMs / config.durationMs) * config.distance;
10
+ const len = Math.hypot(dir.x, dir.z);
11
+ if (len === 0)
12
+ return [0, 0, 0];
13
+ return [(dir.x / len) * traveled, 0, (dir.z / len) * traveled];
14
+ }
15
+ export function createDashState(config) {
16
+ let stamina = config.staminaMax;
17
+ let dashStartMs = null;
18
+ let dashDir = { x: 0, z: 0 };
19
+ let cooldownUntil = 0;
20
+ function elapsed(nowMs) {
21
+ if (dashStartMs === null)
22
+ return null;
23
+ const dt = nowMs - dashStartMs;
24
+ return dt >= 0 && dt < config.durationMs ? dt : null;
25
+ }
26
+ return {
27
+ tryDash(direction, nowMs) {
28
+ if (elapsed(nowMs) !== null)
29
+ return { reason: "dashing" };
30
+ if (nowMs < cooldownUntil)
31
+ return { reason: "cooldown" };
32
+ if (stamina < config.staminaCost)
33
+ return { reason: "no-stamina" };
34
+ stamina -= config.staminaCost;
35
+ dashStartMs = nowMs;
36
+ dashDir = direction;
37
+ cooldownUntil = nowMs + config.cooldownMs;
38
+ return { direction, durationMs: config.durationMs, distance: config.distance };
39
+ },
40
+ tick(dtSeconds, nowMs) {
41
+ if (dashStartMs !== null && nowMs - dashStartMs >= config.durationMs)
42
+ dashStartMs = null;
43
+ stamina = Math.min(config.staminaMax, stamina + config.staminaRegenPerSecond * dtSeconds);
44
+ },
45
+ stamina() {
46
+ return stamina;
47
+ },
48
+ staminaFraction() {
49
+ return config.staminaMax <= 0 ? 0 : stamina / config.staminaMax;
50
+ },
51
+ isDashing(nowMs) {
52
+ return elapsed(nowMs) !== null;
53
+ },
54
+ isInvulnerable(nowMs) {
55
+ const dt = elapsed(nowMs);
56
+ return dt !== null && iframeActive(config, dt);
57
+ },
58
+ offset(nowMs) {
59
+ const dt = elapsed(nowMs);
60
+ if (dt === null)
61
+ return [0, 0, 0];
62
+ return dashOffset(config, dashDir, dt);
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,45 @@
1
+ export interface SnapshotUnit {
2
+ id: string;
3
+ stats: Readonly<Record<string, number>>;
4
+ }
5
+ export interface BoardSnapshot {
6
+ ownerId: string;
7
+ units: readonly SnapshotUnit[];
8
+ stats: Readonly<Record<string, number>>;
9
+ seed: number;
10
+ capturedAt: number;
11
+ }
12
+ export interface SerializeBoardArgs {
13
+ ownerId: string;
14
+ units: readonly SnapshotUnit[];
15
+ stats?: Readonly<Record<string, number>>;
16
+ seed?: number;
17
+ capturedAt?: number;
18
+ }
19
+ export declare function serializeBoard(args: SerializeBoardArgs): BoardSnapshot;
20
+ export declare function cloneSnapshot(snapshot: BoardSnapshot): BoardSnapshot;
21
+ export interface CombatRules {
22
+ attackStat: string;
23
+ healthStat: string;
24
+ maxRounds?: number;
25
+ critChance?: number;
26
+ critMultiplier?: number;
27
+ }
28
+ export type CombatSide = "a" | "b" | "draw";
29
+ export interface ReplayBlow {
30
+ round: number;
31
+ attacker: CombatSide;
32
+ attackerUnit: string;
33
+ defenderUnit: string;
34
+ damage: number;
35
+ crit: boolean;
36
+ defenderHealthAfter: number;
37
+ }
38
+ export interface ReplayResult {
39
+ winner: CombatSide;
40
+ rounds: number;
41
+ blows: readonly ReplayBlow[];
42
+ survivorsA: readonly string[];
43
+ survivorsB: readonly string[];
44
+ }
45
+ export declare function replayCombat(a: BoardSnapshot, b: BoardSnapshot, rules: CombatRules): ReplayResult;