ogamejs 2.1.6 → 4.0.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 (132) hide show
  1. package/README.md +485 -228
  2. package/package.json +71 -33
  3. package/src/buildings/buildTime.js +25 -0
  4. package/src/buildings/crystal.js +35 -0
  5. package/src/buildings/deut.js +31 -0
  6. package/src/buildings/fusion-reactor.js +35 -0
  7. package/src/buildings/index.js +28 -0
  8. package/src/buildings/info.js +42 -0
  9. package/src/buildings/infocompte.js +150 -0
  10. package/src/buildings/metal.js +26 -0
  11. package/src/buildings/production.js +150 -0
  12. package/src/buildings/solar-plant.js +25 -0
  13. package/src/buildings/storage.js +59 -0
  14. package/src/cost.js +70 -0
  15. package/src/fleets/combat.js +291 -0
  16. package/src/fleets/distance.js +39 -0
  17. package/src/fleets/flight.js +105 -0
  18. package/src/fleets/getDebris.js +21 -0
  19. package/src/fleets/index.js +19 -0
  20. package/src/fleets/speed.js +108 -0
  21. package/src/i18n.js +43 -0
  22. package/src/index.js +28 -0
  23. package/src/models/buildings.js +319 -0
  24. package/src/models/destroyable.js +929 -0
  25. package/src/models/research.js +169 -0
  26. package/src/research/index.js +9 -0
  27. package/src/research/researchTime.js +25 -0
  28. package/src/trades/crystal.js +16 -0
  29. package/src/trades/deut.js +16 -0
  30. package/src/trades/index.js +13 -0
  31. package/src/trades/metal.js +16 -0
  32. package/src/trades/parseRate.js +49 -0
  33. package/src/trades/utils.js +9 -0
  34. package/src/types.js +146 -0
  35. package/types/buildings/buildTime.d.ts +16 -0
  36. package/types/buildings/buildTime.d.ts.map +1 -0
  37. package/types/buildings/crystal.d.ts +12 -0
  38. package/types/buildings/crystal.d.ts.map +1 -0
  39. package/types/buildings/deut.d.ts +12 -0
  40. package/types/buildings/deut.d.ts.map +1 -0
  41. package/types/buildings/fusion-reactor.d.ts +15 -0
  42. package/types/buildings/fusion-reactor.d.ts.map +1 -0
  43. package/types/buildings/index.d.ts +27 -0
  44. package/types/buildings/index.d.ts.map +1 -0
  45. package/types/buildings/info.d.ts +17 -0
  46. package/types/buildings/info.d.ts.map +1 -0
  47. package/types/buildings/infocompte.d.ts +41 -0
  48. package/types/buildings/infocompte.d.ts.map +1 -0
  49. package/types/buildings/metal.d.ts +11 -0
  50. package/types/buildings/metal.d.ts.map +1 -0
  51. package/types/buildings/production.d.ts +78 -0
  52. package/types/buildings/production.d.ts.map +1 -0
  53. package/types/buildings/solar-plant.d.ts +10 -0
  54. package/types/buildings/solar-plant.d.ts.map +1 -0
  55. package/types/buildings/storage.d.ts +29 -0
  56. package/types/buildings/storage.d.ts.map +1 -0
  57. package/types/cost.d.ts +25 -0
  58. package/types/cost.d.ts.map +1 -0
  59. package/types/fleets/combat.d.ts +57 -0
  60. package/types/fleets/combat.d.ts.map +1 -0
  61. package/types/fleets/distance.d.ts +13 -0
  62. package/types/fleets/distance.d.ts.map +1 -0
  63. package/types/fleets/flight.d.ts +57 -0
  64. package/types/fleets/flight.d.ts.map +1 -0
  65. package/types/fleets/getDebris.d.ts +12 -0
  66. package/types/fleets/getDebris.d.ts.map +1 -0
  67. package/types/fleets/index.d.ts +18 -0
  68. package/types/fleets/index.d.ts.map +1 -0
  69. package/types/fleets/speed.d.ts +54 -0
  70. package/types/fleets/speed.d.ts.map +1 -0
  71. package/types/i18n.d.ts +23 -0
  72. package/types/i18n.d.ts.map +1 -0
  73. package/types/index.d.ts +1316 -0
  74. package/types/index.d.ts.map +1 -0
  75. package/types/models/buildings.d.ts +373 -0
  76. package/types/models/buildings.d.ts.map +1 -0
  77. package/types/models/destroyable.d.ts +695 -0
  78. package/types/models/destroyable.d.ts.map +1 -0
  79. package/types/models/research.d.ts +262 -0
  80. package/types/models/research.d.ts.map +1 -0
  81. package/types/research/index.d.ts +8 -0
  82. package/types/research/index.d.ts.map +1 -0
  83. package/types/research/researchTime.d.ts +16 -0
  84. package/types/research/researchTime.d.ts.map +1 -0
  85. package/types/trades/crystal.d.ts +6 -0
  86. package/types/trades/crystal.d.ts.map +1 -0
  87. package/types/trades/deut.d.ts +6 -0
  88. package/types/trades/deut.d.ts.map +1 -0
  89. package/types/trades/index.d.ts +12 -0
  90. package/types/trades/index.d.ts.map +1 -0
  91. package/types/trades/metal.d.ts +6 -0
  92. package/types/trades/metal.d.ts.map +1 -0
  93. package/types/trades/parseRate.d.ts +2 -0
  94. package/types/trades/parseRate.d.ts.map +1 -0
  95. package/types/trades/utils.d.ts +2 -0
  96. package/types/trades/utils.d.ts.map +1 -0
  97. package/types/types.d.ts +449 -0
  98. package/types/types.d.ts.map +1 -0
  99. package/CHANGELOG.md +0 -50
  100. package/build/buildings/crystal.js +0 -61
  101. package/build/buildings/crystal.test.js +0 -32
  102. package/build/buildings/deut.js +0 -56
  103. package/build/buildings/deut.test.js +0 -32
  104. package/build/buildings/fusion-reactor.js +0 -60
  105. package/build/buildings/fusion-reactor.test.js +0 -22
  106. package/build/buildings/index.js +0 -31
  107. package/build/buildings/infocompte.js +0 -150
  108. package/build/buildings/infocompte.test.js +0 -107
  109. package/build/buildings/metal.js +0 -50
  110. package/build/buildings/metal.test.js +0 -32
  111. package/build/buildings/solar-plant.js +0 -42
  112. package/build/buildings/solar-plant.test.js +0 -21
  113. package/build/fleets/getDebris.js +0 -27
  114. package/build/fleets/getDebris.test.js +0 -26
  115. package/build/fleets/index.js +0 -16
  116. package/build/index.js +0 -30
  117. package/build/models/buildings.js +0 -65
  118. package/build/models/destroyable.js +0 -730
  119. package/build/trades/crystal.js +0 -33
  120. package/build/trades/crystal.test.js +0 -16
  121. package/build/trades/deut.js +0 -33
  122. package/build/trades/deut.test.js +0 -16
  123. package/build/trades/index.js +0 -25
  124. package/build/trades/metal.js +0 -33
  125. package/build/trades/metal.test.js +0 -16
  126. package/build/trades/parseRate.js +0 -60
  127. package/build/trades/parseRate.test.js +0 -40
  128. package/build/trades/trades.js +0 -119
  129. package/build/trades/trades.test.js +0 -65
  130. package/build/trades/utils.js +0 -17
  131. package/build/trades/utils.test.js +0 -16
  132. package/build/winston.js +0 -16
@@ -0,0 +1,59 @@
1
+ import getCost, { assertLevel } from '../cost.js';
2
+
3
+ /**
4
+ *
5
+ * Return the capacity of a storage building at a given level
6
+ *
7
+ * Level 0 is the free 10.000 units every planet starts with.
8
+ * @param {number} level The storage level, >= 0
9
+ * @returns {number} The protected capacity, in resource units
10
+ */
11
+ function getStorageCapacity(level) {
12
+ if (!Number.isInteger(level) || level < 0) {
13
+ throw new Error(`level must be an integer >= 0, received ${level}`);
14
+ }
15
+
16
+ return 5000 * Math.floor(2.5 * Math.exp((20 * level) / 33));
17
+ }
18
+
19
+ /**
20
+ *
21
+ * Return the cost and the capacity of a storage building at a given level
22
+ * @param {import('../types.js').BuildingEntry} storage A storage entry of models/buildings.js (22, 23 or 24)
23
+ * @param {number} targetLevel The level to reach, >= 1
24
+ * @returns {import('../types.js').Cost & {capacity: number}} The cost of that level plus the capacity
25
+ */
26
+ function getStorage(storage, targetLevel) {
27
+ assertLevel(targetLevel);
28
+
29
+ if (!storage || !storage.storage) {
30
+ throw new Error('storage must be a storage building entry (Buildings 22, 23 or 24)');
31
+ }
32
+
33
+ return {
34
+ ...getCost(storage, targetLevel),
35
+ capacity: getStorageCapacity(targetLevel),
36
+ };
37
+ }
38
+
39
+ /**
40
+ *
41
+ * Return the lowest storage level able to protect a given amount of resources
42
+ * @param {number} amount The amount of resources to protect
43
+ * @returns {number} The required storage level
44
+ */
45
+ function getStorageLevelFor(amount) {
46
+ if (!Number.isFinite(amount) || amount < 0) {
47
+ throw new Error(`amount must be a positive number, received ${amount}`);
48
+ }
49
+
50
+ let level = 0;
51
+ while (getStorageCapacity(level) < amount) {
52
+ level += 1;
53
+ }
54
+
55
+ return level;
56
+ }
57
+
58
+ export { getStorageCapacity, getStorageLevelFor };
59
+ export default getStorage;
package/src/cost.js ADDED
@@ -0,0 +1,70 @@
1
+ function assertLevel(targetLevel) {
2
+ if (!Number.isInteger(targetLevel) || targetLevel < 1) {
3
+ throw new Error(`targetLevel must be an integer >= 1, received ${targetLevel}`);
4
+ }
5
+ }
6
+
7
+ /**
8
+ * Guard against the pre-4.0 habit of passing `Buildings[id].base` around.
9
+ * Every calculator now needs the whole entry, because the cost `factor` lives
10
+ * on it and not on its `base`.
11
+ */
12
+ function assertEntry(entry, targetLevel) {
13
+ assertLevel(targetLevel);
14
+
15
+ if (!entry || typeof entry !== 'object') {
16
+ throw new Error('expected a Buildings or Research entry, received nothing');
17
+ }
18
+
19
+ if (!entry.base || !entry.factor) {
20
+ const looksLikeABase = 'metal' in entry && 'crystal' in entry;
21
+
22
+ throw new Error(
23
+ looksLikeABase
24
+ ? 'expected a Buildings or Research entry, received its `base`: pass Buildings[id], not Buildings[id].base'
25
+ : 'expected a Buildings or Research entry with a `base` and a `factor`',
26
+ );
27
+ }
28
+ }
29
+
30
+ function levelCost(baseCost, factor, level, roundTo) {
31
+ const cost = baseCost * factor ** (level - 1);
32
+
33
+ if (roundTo) {
34
+ return Math.ceil(cost / roundTo) * roundTo;
35
+ }
36
+
37
+ return Math.floor(cost);
38
+ }
39
+
40
+ /**
41
+ *
42
+ * Return the cost of a building or a technology at a given level
43
+ *
44
+ * Works for anything carrying a `base` and a `factor`, so both
45
+ * `models/buildings.js` and `models/research.js` entries are accepted.
46
+ * Use the dedicated `getMetalMine`/`getCrystalMine`/... helpers when you also
47
+ * need the production or the consumption of a mine or a plant.
48
+ *
49
+ * @param {import('./types.js').BuildingEntry|import('./types.js').ResearchEntry} entry A Buildings or
50
+ * Research entry (not its `base`)
51
+ * @param {number} targetLevel The level to reach, >= 1
52
+ * @returns {import('./types.js').Cost} The resources, and the energy, paid to reach that level
53
+ */
54
+ function getCost(entry, targetLevel) {
55
+ assertEntry(entry, targetLevel);
56
+
57
+ const { base, factor, roundTo } = entry;
58
+ // Only the space dock scales its energy cost on a factor of its own.
59
+ const energyFactor = entry.energyFactor ?? factor;
60
+
61
+ return {
62
+ metal: levelCost(base.metal, factor, targetLevel, roundTo),
63
+ crystal: levelCost(base.crystal, factor, targetLevel, roundTo),
64
+ deuterium: levelCost(base.deuterium, factor, targetLevel, roundTo),
65
+ energyCost: levelCost(base.energyCost, energyFactor, targetLevel, roundTo),
66
+ };
67
+ }
68
+
69
+ export { assertLevel, assertEntry };
70
+ export default getCost;
@@ -0,0 +1,291 @@
1
+ import DESTROYABLE, { ATTRIBUTES } from '../models/destroyable.js';
2
+ import getDebris from './getDebris.js';
3
+
4
+ const ROUNDS = 6;
5
+
6
+ /**
7
+ * A unit with a rapid fire of 1250 legitimately chains hundreds of shots, but
8
+ * the chain is unbounded in principle, so cap it rather than risk a hang.
9
+ */
10
+ const MAX_SHOTS = 10000;
11
+
12
+ /** A shot weaker than this share of the target shield simply bounces off. */
13
+ const BOUNCE_RATIO = 0.01;
14
+
15
+ /** Below that share of its hull, a unit may explode at the end of the round. */
16
+ const EXPLOSION_THRESHOLD = 0.7;
17
+
18
+ /** Rapid-fire tables point at library ids, so we need the way back. */
19
+ const LIBRARY_ID = new Map(
20
+ Object.entries(DESTROYABLE).map(([id, entry]) => [entry, Number(id)]),
21
+ );
22
+
23
+ /** Callers routinely spread a model entry, which loses object identity. */
24
+ const LIBRARY_ID_BY_OGAME_ID = new Map(
25
+ Object.entries(DESTROYABLE).map(([id, entry]) => [entry.ogameId, Number(id)]),
26
+ );
27
+
28
+ function libraryIdOf(ship) {
29
+ const id = LIBRARY_ID.get(ship) ?? LIBRARY_ID_BY_OGAME_ID.get(ship.ogameId);
30
+
31
+ if (id === undefined) {
32
+ // Without an id the rapid-fire tables, which point at ids, cannot resolve.
33
+ throw new Error('every ship must come from models/destroyable.js, or at least carry its ogameId');
34
+ }
35
+
36
+ return id;
37
+ }
38
+
39
+ /**
40
+ * A tiny deterministic PRNG (mulberry32), so a battle can be replayed.
41
+ */
42
+ function createRandom(seed) {
43
+ let state = seed >>> 0;
44
+
45
+ return () => {
46
+ state += 0x6d2b79f5;
47
+ let t = state;
48
+ t = Math.imul(t ^ (t >>> 15), t | 1);
49
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
50
+
51
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
52
+ };
53
+ }
54
+
55
+ /** Two entries for the same ship would otherwise be counted twice. */
56
+ function mergeFleet(fleet, side) {
57
+ if (!Array.isArray(fleet) || fleet.length === 0) {
58
+ throw new Error(`${side} fleet must be a non empty array of { ship, count }`);
59
+ }
60
+
61
+ const merged = new Map();
62
+
63
+ for (const { ship, count } of fleet) {
64
+ if (!ship || !Array.isArray(ship.rapidFire)) {
65
+ throw new Error(`${side} fleet must hold entries of models/destroyable.js`);
66
+ }
67
+
68
+ if (!Number.isInteger(count) || count < 0) {
69
+ throw new Error(`${side} fleet counts must be integers >= 0, received ${count}`);
70
+ }
71
+
72
+ merged.set(ship, (merged.get(ship) ?? 0) + count);
73
+ }
74
+
75
+ return [...merged].map(([ship, count]) => ({ ship, count }));
76
+ }
77
+
78
+ /**
79
+ * Turn a fleet into individual units with live hull and shield values. Hull
80
+ * points are a tenth of the metal plus crystal cost.
81
+ */
82
+ function deploy(fleet, techs) {
83
+ const { weapons = 0, shielding = 0, armour = 0 } = techs;
84
+ const units = [];
85
+
86
+ for (const { ship, count } of fleet) {
87
+ const maxHull = (ship.structure / 10) * (1 + 0.1 * armour);
88
+ const maxShield = ship.shield * (1 + 0.1 * shielding);
89
+ const attack = ship.attack * (1 + 0.1 * weapons);
90
+ const libraryId = libraryIdOf(ship);
91
+
92
+ for (let i = 0; i < count; i += 1) {
93
+ units.push({
94
+ ship, libraryId, maxHull, hull: maxHull, maxShield, shield: maxShield, attack,
95
+ });
96
+ }
97
+ }
98
+
99
+ return units;
100
+ }
101
+
102
+ function rapidFireAgainst(unit, target) {
103
+ return unit.ship.rapidFire.find((entry) => entry.target === target.libraryId)?.fire ?? 1;
104
+ }
105
+
106
+ /**
107
+ * One unit fires at a random enemy, then keeps firing while its rapid-fire
108
+ * bonus rolls in its favour.
109
+ */
110
+ function fire(unit, enemies, random) {
111
+ for (let shots = 0; shots < MAX_SHOTS; shots += 1) {
112
+ if (enemies.length === 0) {
113
+ return;
114
+ }
115
+
116
+ const target = enemies[Math.floor(random() * enemies.length)];
117
+ const damage = unit.attack;
118
+
119
+ // A shot too weak to dent the shield bounces off without doing anything.
120
+ if (damage >= target.shield * BOUNCE_RATIO) {
121
+ if (damage <= target.shield) {
122
+ target.shield -= damage;
123
+ } else {
124
+ target.hull -= damage - target.shield;
125
+ target.shield = 0;
126
+ }
127
+ }
128
+
129
+ const rapid = rapidFireAgainst(unit, target);
130
+
131
+ // With a rapid fire of N, the unit shoots again with a (N - 1) / N chance.
132
+ if (rapid <= 1 || random() >= (rapid - 1) / rapid) {
133
+ return;
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Destroyed units are removed, damaged ones may explode, survivors get their
140
+ * shield back for the next round.
141
+ */
142
+ function endRound(units, random) {
143
+ const survivors = [];
144
+
145
+ for (const unit of units) {
146
+ if (unit.hull <= 0) {
147
+ continue;
148
+ }
149
+
150
+ const integrity = unit.hull / unit.maxHull;
151
+
152
+ if (integrity < EXPLOSION_THRESHOLD && random() < 1 - integrity) {
153
+ continue;
154
+ }
155
+
156
+ unit.shield = unit.maxShield;
157
+ survivors.push(unit);
158
+ }
159
+
160
+ return survivors;
161
+ }
162
+
163
+ function summarise(units, initial) {
164
+ const remaining = new Map();
165
+
166
+ for (const unit of units) {
167
+ remaining.set(unit.ship, (remaining.get(unit.ship) ?? 0) + 1);
168
+ }
169
+
170
+ const survivors = [];
171
+ const losses = [];
172
+
173
+ for (const { ship, count } of initial) {
174
+ const left = remaining.get(ship) ?? 0;
175
+
176
+ if (left > 0) {
177
+ survivors.push({ ship, count: left });
178
+ }
179
+
180
+ if (count - left > 0) {
181
+ losses.push({ ship, count: count - left });
182
+ }
183
+ }
184
+
185
+ return { survivors, losses };
186
+ }
187
+
188
+ function debrisOf(losses, options) {
189
+ const { debrisFactor, deuteriumDebrisFactor, defenseDebris } = options;
190
+
191
+ return losses
192
+ // On most universes only ships leave debris behind.
193
+ .filter(({ ship }) => defenseDebris || ship.category === ATTRIBUTES.CATEGORIES.SHIPS)
194
+ .map(({ ship, count }) => getDebris(ship, count, debrisFactor, deuteriumDebrisFactor))
195
+ .reduce((total, debris) => ({
196
+ metal: total.metal + debris.metal,
197
+ crystal: total.crystal + debris.crystal,
198
+ deuterium: total.deuterium + debris.deuterium,
199
+ }), { metal: 0, crystal: 0, deuterium: 0 });
200
+ }
201
+
202
+ /**
203
+ *
204
+ * Simulate a battle between two fleets
205
+ *
206
+ * Follows the game rules: up to six rounds, every unit fires once per round at a
207
+ * random enemy, rapid fire grants extra shots, shots below 1% of the target
208
+ * shield bounce off, shields come back every round, and a unit under 70% hull
209
+ * may explode at the end of the round.
210
+ *
211
+ * A battle is random, so one run is one possible outcome. The `seed` makes a run
212
+ * reproducible; average several seeds to get a feel for the likely result.
213
+ * @param {object} attacker The attacking side
214
+ * @param {import('../types.js').FleetEntry[]} attacker.fleet Its ships
215
+ * @param {import('../types.js').CombatTechs} [attacker.techs] Its combat technology levels
216
+ * @param {object} defender The defending side, same shape, defenses included
217
+ * @param {object} [options] Simulation options
218
+ * @param {number} [options.seed] The PRNG seed, for a reproducible battle
219
+ * @param {number} [options.debrisFactor] The universe debris factor
220
+ * @param {number} [options.deuteriumDebrisFactor] The universe deuterium debris factor
221
+ * @param {boolean} [options.defenseDebris] Whether destroyed defenses leave debris
222
+ * @returns {{
223
+ * winner: 'attacker'|'defender'|'draw', rounds: number, seed: number,
224
+ * attacker: {survivors: import('../types.js').FleetEntry[], losses: import('../types.js').FleetEntry[]},
225
+ * defender: {survivors: import('../types.js').FleetEntry[], losses: import('../types.js').FleetEntry[]},
226
+ * debris: import('../types.js').Resources,
227
+ * }} Who won, how long it took, what is left on each side, and the debris field
228
+ */
229
+ function simulateCombat(attacker, defender, options = {}) {
230
+ const {
231
+ seed = Date.now(),
232
+ debrisFactor = 0.3,
233
+ deuteriumDebrisFactor = 0,
234
+ defenseDebris = false,
235
+ } = options;
236
+
237
+ const random = createRandom(seed);
238
+ const attackerFleet = mergeFleet(attacker.fleet, 'attacker');
239
+ const defenderFleet = mergeFleet(defender.fleet, 'defender');
240
+
241
+ let attackers = deploy(attackerFleet, attacker.techs ?? {});
242
+ let defenders = deploy(defenderFleet, defender.techs ?? {});
243
+
244
+ let rounds = 0;
245
+
246
+ while (rounds < ROUNDS && attackers.length > 0 && defenders.length > 0) {
247
+ rounds += 1;
248
+
249
+ // Both sides shoot with the units they started the round with.
250
+ const shootingAttackers = [...attackers];
251
+ const shootingDefenders = [...defenders];
252
+
253
+ for (const unit of shootingAttackers) {
254
+ fire(unit, defenders, random);
255
+ }
256
+
257
+ for (const unit of shootingDefenders) {
258
+ fire(unit, attackers, random);
259
+ }
260
+
261
+ attackers = endRound(attackers, random);
262
+ defenders = endRound(defenders, random);
263
+ }
264
+
265
+ const attackerResult = summarise(attackers, attackerFleet);
266
+ const defenderResult = summarise(defenders, defenderFleet);
267
+
268
+ let winner = 'draw';
269
+
270
+ if (attackers.length > 0 && defenders.length === 0) {
271
+ winner = 'attacker';
272
+ } else if (defenders.length > 0 && attackers.length === 0) {
273
+ winner = 'defender';
274
+ }
275
+
276
+ return {
277
+ winner,
278
+ rounds,
279
+ seed,
280
+ attacker: attackerResult,
281
+ defender: defenderResult,
282
+ debris: debrisOf([...attackerResult.losses, ...defenderResult.losses], {
283
+ debrisFactor,
284
+ deuteriumDebrisFactor,
285
+ defenseDebris,
286
+ }),
287
+ };
288
+ }
289
+
290
+ export { createRandom, ROUNDS };
291
+ export default simulateCombat;
@@ -0,0 +1,39 @@
1
+ function assertCoordinates(coordinates, name) {
2
+ const { galaxy, system, position } = coordinates ?? {};
3
+
4
+ if (![galaxy, system, position].every((value) => Number.isInteger(value) && value > 0)) {
5
+ throw new Error(`${name} must be a { galaxy, system, position } of positive integers`);
6
+ }
7
+ }
8
+
9
+ /**
10
+ *
11
+ * Return the distance between two coordinates, in OGame distance units
12
+ *
13
+ * The scale is not linear: crossing a galaxy costs far more than crossing a
14
+ * system, which costs far more than moving inside one.
15
+ * @param {import('../types.js').Coordinates} origin Where the fleet leaves from
16
+ * @param {import('../types.js').Coordinates} target Where it goes
17
+ * @returns {number} The distance
18
+ */
19
+ function getDistance(origin, target) {
20
+ assertCoordinates(origin, 'origin');
21
+ assertCoordinates(target, 'target');
22
+
23
+ if (origin.galaxy !== target.galaxy) {
24
+ return 20000 * Math.abs(origin.galaxy - target.galaxy);
25
+ }
26
+
27
+ if (origin.system !== target.system) {
28
+ return 2700 + 95 * Math.abs(origin.system - target.system);
29
+ }
30
+
31
+ if (origin.position !== target.position) {
32
+ return 1000 + 5 * Math.abs(origin.position - target.position);
33
+ }
34
+
35
+ // Same planet: a moon to planet hop, or a planet to its own debris field.
36
+ return 5;
37
+ }
38
+
39
+ export default getDistance;
@@ -0,0 +1,105 @@
1
+ import getDistance from './distance.js';
2
+ import { getFleetSpeed, getActiveDrive } from './speed.js';
3
+
4
+ function assertSpeedPercent(speedPercent) {
5
+ if (!Number.isFinite(speedPercent) || speedPercent <= 0 || speedPercent > 100) {
6
+ throw new Error(`speedPercent must be between 1 and 100, received ${speedPercent}`);
7
+ }
8
+ }
9
+
10
+ /**
11
+ *
12
+ * Return the one way flight time of a fleet
13
+ *
14
+ * `(10 + 35000 / speedPercent * sqrt(distance * 10 / fleetSpeed)) / universeFleetSpeed`
15
+ * @param {number} distance The distance to cross, from `getDistance`
16
+ * @param {number} fleetSpeed The speed of the slowest ship, from `getFleetSpeed`
17
+ * @param {number} [speedPercent] The fleet speed slider, 10 to 100 in game
18
+ * @param {number} [universeFleetSpeed] The universe fleet speed
19
+ * @returns {number} The flight time, in seconds
20
+ */
21
+ function getFlightTime(distance, fleetSpeed, speedPercent = 100, universeFleetSpeed = 1) {
22
+ assertSpeedPercent(speedPercent);
23
+
24
+ if (!(fleetSpeed > 0)) {
25
+ throw new Error(`fleetSpeed must be greater than 0, received ${fleetSpeed}`);
26
+ }
27
+
28
+ const seconds = 10 + (35000 / speedPercent) * Math.sqrt((distance * 10) / fleetSpeed);
29
+
30
+ return Math.round(seconds / universeFleetSpeed);
31
+ }
32
+
33
+ /**
34
+ *
35
+ * Return the deuterium a fleet burns for a one way trip
36
+ *
37
+ * `1 + round(sum(consumption * count) * distance / 35000 * (speedPercent / 100 + 1) ** 2)`
38
+ * @param {import('../types.js').FleetEntry[]} fleet The ships taking off
39
+ * @param {number} distance The distance to cross, from `getDistance`
40
+ * @param {number} [speedPercent] The fleet speed slider, 10 to 100 in game
41
+ * @param {import('../types.js').Drives} [drives] Drive levels, needed because a
42
+ * ship that switched drive also changed its consumption
43
+ * @returns {number} The fuel needed, in deuterium
44
+ */
45
+ function getFuelConsumption(fleet, distance, speedPercent = 100, drives = {}) {
46
+ assertSpeedPercent(speedPercent);
47
+
48
+ if (!Array.isArray(fleet) || fleet.length === 0) {
49
+ throw new Error('fleet must be a non empty array of { ship, count }');
50
+ }
51
+
52
+ const speedFactor = (speedPercent / 100 + 1) ** 2;
53
+
54
+ const consumption = fleet.reduce((total, { ship, count }) => {
55
+ // A ship that switched drive also changed how much it burns.
56
+ const { fuelConsumption } = getActiveDrive(ship, drives);
57
+
58
+ return total + fuelConsumption * count;
59
+ }, 0);
60
+
61
+ return 1 + Math.round((consumption * distance) / 35000 * speedFactor);
62
+ }
63
+
64
+ /**
65
+ *
66
+ * Return everything about a trip: distance, duration, fuel and cargo left
67
+ * @param {import('../types.js').FleetEntry[]} fleet The ships taking off
68
+ * @param {import('../types.js').Coordinates} origin Where the fleet leaves from
69
+ * @param {import('../types.js').Coordinates} target Where it goes
70
+ * @param {object} [options] Trip options
71
+ * @param {number} [options.speedPercent] The fleet speed slider, 10 to 100 in game
72
+ * @param {number} [options.universeFleetSpeed] The universe fleet speed
73
+ * @param {import('../types.js').Drives} [options.drives] The drive technology levels
74
+ * @param {boolean} [options.roundTrip] Whether to account for the way back too
75
+ * @returns {{
76
+ * distance: number, fleetSpeed: number, duration: number, fuel: number,
77
+ * cargo: number, cargoAfterFuel: number,
78
+ * }} The trip, with durations in seconds and `cargoAfterFuel` the room left once
79
+ * the fuel is loaded
80
+ */
81
+ function getTrip(fleet, origin, target, options = {}) {
82
+ const {
83
+ speedPercent = 100,
84
+ universeFleetSpeed = 1,
85
+ drives = {},
86
+ roundTrip = false,
87
+ } = options;
88
+
89
+ const distance = getDistance(origin, target);
90
+ const fleetSpeed = getFleetSpeed(fleet, drives);
91
+ const oneWay = getFlightTime(distance, fleetSpeed, speedPercent, universeFleetSpeed);
92
+ const fuel = getFuelConsumption(fleet, distance, speedPercent, drives) * (roundTrip ? 2 : 1);
93
+ const cargo = fleet.reduce((total, { ship, count }) => total + ship.cargo * count, 0);
94
+
95
+ return {
96
+ distance,
97
+ fleetSpeed,
98
+ duration: roundTrip ? oneWay * 2 : oneWay,
99
+ fuel,
100
+ cargo,
101
+ cargoAfterFuel: Math.max(0, cargo - fuel),
102
+ };
103
+ }
104
+
105
+ export { getFlightTime, getFuelConsumption, getTrip };
@@ -0,0 +1,21 @@
1
+ /**
2
+ *
3
+ * Return the debris field left behind by destroyed ships or defenses
4
+ * @param {import('../types.js').DestroyableEntry} ship An entry of models/destroyable.js
5
+ * @param {number} number The number of destroyed units
6
+ * @param {number} factor The universe debris factor, e.g. 0.3 for 30%
7
+ * @param {number} [deuteriumFactor] The universe deuterium debris factor, 0 on most universes
8
+ * @return {import('../types.js').Resources} The debris generated
9
+ */
10
+ function getDebris(ship, number, factor, deuteriumFactor = 0) {
11
+ const { cost } = ship;
12
+ const deuteriumCost = cost.deuterium ?? cost.deut ?? 0;
13
+
14
+ return {
15
+ metal: (cost.metal ?? 0) * factor * number,
16
+ crystal: (cost.crystal ?? 0) * factor * number,
17
+ deuterium: deuteriumCost * deuteriumFactor * number,
18
+ };
19
+ }
20
+
21
+ export default getDebris;
@@ -0,0 +1,19 @@
1
+ import getDebris from './getDebris.js';
2
+ import getDistance from './distance.js';
3
+ import getShipSpeed, { getActiveDrive, getFleetSpeed } from './speed.js';
4
+ import { getFlightTime, getFuelConsumption, getTrip } from './flight.js';
5
+ import simulateCombat from './combat.js';
6
+
7
+ const Fleets = {
8
+ getDebris,
9
+ getDistance,
10
+ getShipSpeed,
11
+ getActiveDrive,
12
+ getFleetSpeed,
13
+ getFlightTime,
14
+ getFuelConsumption,
15
+ getTrip,
16
+ simulateCombat,
17
+ };
18
+
19
+ export default Fleets;