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.
- package/README.md +485 -228
- package/package.json +71 -33
- package/src/buildings/buildTime.js +25 -0
- package/src/buildings/crystal.js +35 -0
- package/src/buildings/deut.js +31 -0
- package/src/buildings/fusion-reactor.js +35 -0
- package/src/buildings/index.js +28 -0
- package/src/buildings/info.js +42 -0
- package/src/buildings/infocompte.js +150 -0
- package/src/buildings/metal.js +26 -0
- package/src/buildings/production.js +150 -0
- package/src/buildings/solar-plant.js +25 -0
- package/src/buildings/storage.js +59 -0
- package/src/cost.js +70 -0
- package/src/fleets/combat.js +291 -0
- package/src/fleets/distance.js +39 -0
- package/src/fleets/flight.js +105 -0
- package/src/fleets/getDebris.js +21 -0
- package/src/fleets/index.js +19 -0
- package/src/fleets/speed.js +108 -0
- package/src/i18n.js +43 -0
- package/src/index.js +28 -0
- package/src/models/buildings.js +319 -0
- package/src/models/destroyable.js +929 -0
- package/src/models/research.js +169 -0
- package/src/research/index.js +9 -0
- package/src/research/researchTime.js +25 -0
- package/src/trades/crystal.js +16 -0
- package/src/trades/deut.js +16 -0
- package/src/trades/index.js +13 -0
- package/src/trades/metal.js +16 -0
- package/src/trades/parseRate.js +49 -0
- package/src/trades/utils.js +9 -0
- package/src/types.js +146 -0
- package/types/buildings/buildTime.d.ts +16 -0
- package/types/buildings/buildTime.d.ts.map +1 -0
- package/types/buildings/crystal.d.ts +12 -0
- package/types/buildings/crystal.d.ts.map +1 -0
- package/types/buildings/deut.d.ts +12 -0
- package/types/buildings/deut.d.ts.map +1 -0
- package/types/buildings/fusion-reactor.d.ts +15 -0
- package/types/buildings/fusion-reactor.d.ts.map +1 -0
- package/types/buildings/index.d.ts +27 -0
- package/types/buildings/index.d.ts.map +1 -0
- package/types/buildings/info.d.ts +17 -0
- package/types/buildings/info.d.ts.map +1 -0
- package/types/buildings/infocompte.d.ts +41 -0
- package/types/buildings/infocompte.d.ts.map +1 -0
- package/types/buildings/metal.d.ts +11 -0
- package/types/buildings/metal.d.ts.map +1 -0
- package/types/buildings/production.d.ts +78 -0
- package/types/buildings/production.d.ts.map +1 -0
- package/types/buildings/solar-plant.d.ts +10 -0
- package/types/buildings/solar-plant.d.ts.map +1 -0
- package/types/buildings/storage.d.ts +29 -0
- package/types/buildings/storage.d.ts.map +1 -0
- package/types/cost.d.ts +25 -0
- package/types/cost.d.ts.map +1 -0
- package/types/fleets/combat.d.ts +57 -0
- package/types/fleets/combat.d.ts.map +1 -0
- package/types/fleets/distance.d.ts +13 -0
- package/types/fleets/distance.d.ts.map +1 -0
- package/types/fleets/flight.d.ts +57 -0
- package/types/fleets/flight.d.ts.map +1 -0
- package/types/fleets/getDebris.d.ts +12 -0
- package/types/fleets/getDebris.d.ts.map +1 -0
- package/types/fleets/index.d.ts +18 -0
- package/types/fleets/index.d.ts.map +1 -0
- package/types/fleets/speed.d.ts +54 -0
- package/types/fleets/speed.d.ts.map +1 -0
- package/types/i18n.d.ts +23 -0
- package/types/i18n.d.ts.map +1 -0
- package/types/index.d.ts +1316 -0
- package/types/index.d.ts.map +1 -0
- package/types/models/buildings.d.ts +373 -0
- package/types/models/buildings.d.ts.map +1 -0
- package/types/models/destroyable.d.ts +695 -0
- package/types/models/destroyable.d.ts.map +1 -0
- package/types/models/research.d.ts +262 -0
- package/types/models/research.d.ts.map +1 -0
- package/types/research/index.d.ts +8 -0
- package/types/research/index.d.ts.map +1 -0
- package/types/research/researchTime.d.ts +16 -0
- package/types/research/researchTime.d.ts.map +1 -0
- package/types/trades/crystal.d.ts +6 -0
- package/types/trades/crystal.d.ts.map +1 -0
- package/types/trades/deut.d.ts +6 -0
- package/types/trades/deut.d.ts.map +1 -0
- package/types/trades/index.d.ts +12 -0
- package/types/trades/index.d.ts.map +1 -0
- package/types/trades/metal.d.ts +6 -0
- package/types/trades/metal.d.ts.map +1 -0
- package/types/trades/parseRate.d.ts +2 -0
- package/types/trades/parseRate.d.ts.map +1 -0
- package/types/trades/utils.d.ts +2 -0
- package/types/trades/utils.d.ts.map +1 -0
- package/types/types.d.ts +449 -0
- package/types/types.d.ts.map +1 -0
- package/CHANGELOG.md +0 -50
- package/build/buildings/crystal.js +0 -61
- package/build/buildings/crystal.test.js +0 -32
- package/build/buildings/deut.js +0 -56
- package/build/buildings/deut.test.js +0 -32
- package/build/buildings/fusion-reactor.js +0 -60
- package/build/buildings/fusion-reactor.test.js +0 -22
- package/build/buildings/index.js +0 -31
- package/build/buildings/infocompte.js +0 -150
- package/build/buildings/infocompte.test.js +0 -107
- package/build/buildings/metal.js +0 -50
- package/build/buildings/metal.test.js +0 -32
- package/build/buildings/solar-plant.js +0 -42
- package/build/buildings/solar-plant.test.js +0 -21
- package/build/fleets/getDebris.js +0 -27
- package/build/fleets/getDebris.test.js +0 -26
- package/build/fleets/index.js +0 -16
- package/build/index.js +0 -30
- package/build/models/buildings.js +0 -65
- package/build/models/destroyable.js +0 -730
- package/build/trades/crystal.js +0 -33
- package/build/trades/crystal.test.js +0 -16
- package/build/trades/deut.js +0 -33
- package/build/trades/deut.test.js +0 -16
- package/build/trades/index.js +0 -25
- package/build/trades/metal.js +0 -33
- package/build/trades/metal.test.js +0 -16
- package/build/trades/parseRate.js +0 -60
- package/build/trades/parseRate.test.js +0 -40
- package/build/trades/trades.js +0 -119
- package/build/trades/trades.test.js +0 -65
- package/build/trades/utils.js +0 -17
- package/build/trades/utils.test.js +0 -16
- 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;
|