@robsonbittencourt/calc 0.10.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 (144) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc +12 -0
  3. package/bundle +107 -0
  4. package/dist/adaptable.d.ts +6 -0
  5. package/dist/adaptable.js +28 -0
  6. package/dist/adaptable.js.map +1 -0
  7. package/dist/calc.d.ts +6 -0
  8. package/dist/calc.js +26 -0
  9. package/dist/calc.js.map +1 -0
  10. package/dist/data/abilities.d.ts +15 -0
  11. package/dist/data/abilities.js +448 -0
  12. package/dist/data/abilities.js.map +1 -0
  13. package/dist/data/index.d.ts +2 -0
  14. package/dist/data/index.js +30 -0
  15. package/dist/data/index.js.map +1 -0
  16. package/dist/data/interface.d.ts +150 -0
  17. package/dist/data/interface.js +3 -0
  18. package/dist/data/interface.js.map +1 -0
  19. package/dist/data/items.d.ts +24 -0
  20. package/dist/data/items.js +708 -0
  21. package/dist/data/items.js.map +1 -0
  22. package/dist/data/moves.d.ts +86 -0
  23. package/dist/data/moves.js +5014 -0
  24. package/dist/data/moves.js.map +1 -0
  25. package/dist/data/natures.d.ts +17 -0
  26. package/dist/data/natures.js +127 -0
  27. package/dist/data/natures.js.map +1 -0
  28. package/dist/data/production.min.js +1 -0
  29. package/dist/data/species.d.ts +48 -0
  30. package/dist/data/species.js +10126 -0
  31. package/dist/data/species.js.map +1 -0
  32. package/dist/data/types.d.ts +23 -0
  33. package/dist/data/types.js +538 -0
  34. package/dist/data/types.js.map +1 -0
  35. package/dist/desc.d.ts +65 -0
  36. package/dist/desc.js +866 -0
  37. package/dist/desc.js.map +1 -0
  38. package/dist/field.d.ts +49 -0
  39. package/dist/field.js +111 -0
  40. package/dist/field.js.map +1 -0
  41. package/dist/index.d.ts +44 -0
  42. package/dist/index.js +99 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/items.d.ts +13 -0
  45. package/dist/items.js +434 -0
  46. package/dist/items.js.map +1 -0
  47. package/dist/mechanics/gen12.d.ts +6 -0
  48. package/dist/mechanics/gen12.js +271 -0
  49. package/dist/mechanics/gen12.js.map +1 -0
  50. package/dist/mechanics/gen3.d.ts +11 -0
  51. package/dist/mechanics/gen3.js +371 -0
  52. package/dist/mechanics/gen3.js.map +1 -0
  53. package/dist/mechanics/gen4.d.ts +11 -0
  54. package/dist/mechanics/gen4.js +596 -0
  55. package/dist/mechanics/gen4.js.map +1 -0
  56. package/dist/mechanics/gen56.d.ts +13 -0
  57. package/dist/mechanics/gen56.js +836 -0
  58. package/dist/mechanics/gen56.js.map +1 -0
  59. package/dist/mechanics/gen789.d.ts +14 -0
  60. package/dist/mechanics/gen789.js +1325 -0
  61. package/dist/mechanics/gen789.js.map +1 -0
  62. package/dist/mechanics/util.d.ts +39 -0
  63. package/dist/mechanics/util.js +675 -0
  64. package/dist/mechanics/util.js.map +1 -0
  65. package/dist/move.d.ts +50 -0
  66. package/dist/move.js +324 -0
  67. package/dist/move.js.map +1 -0
  68. package/dist/pokemon.d.ts +55 -0
  69. package/dist/pokemon.js +240 -0
  70. package/dist/pokemon.js.map +1 -0
  71. package/dist/production.min.js +1 -0
  72. package/dist/result.d.ts +34 -0
  73. package/dist/result.js +94 -0
  74. package/dist/result.js.map +1 -0
  75. package/dist/state.d.ts +77 -0
  76. package/dist/state.js +3 -0
  77. package/dist/state.js.map +1 -0
  78. package/dist/stats.d.ts +26 -0
  79. package/dist/stats.js +183 -0
  80. package/dist/stats.js.map +1 -0
  81. package/dist/test/calc.test.d.ts +1 -0
  82. package/dist/test/calc.test.js +1297 -0
  83. package/dist/test/calc.test.js.map +1 -0
  84. package/dist/test/data.test.d.ts +1 -0
  85. package/dist/test/data.test.js +368 -0
  86. package/dist/test/data.test.js.map +1 -0
  87. package/dist/test/gen.d.ts +135 -0
  88. package/dist/test/gen.js +636 -0
  89. package/dist/test/gen.js.map +1 -0
  90. package/dist/test/helper.d.ts +55 -0
  91. package/dist/test/helper.js +174 -0
  92. package/dist/test/helper.js.map +1 -0
  93. package/dist/test/move.test.d.ts +1 -0
  94. package/dist/test/move.test.js +14 -0
  95. package/dist/test/move.test.js.map +1 -0
  96. package/dist/test/pokemon.test.d.ts +1 -0
  97. package/dist/test/pokemon.test.js +102 -0
  98. package/dist/test/pokemon.test.js.map +1 -0
  99. package/dist/test/stats.test.d.ts +1 -0
  100. package/dist/test/stats.test.js +64 -0
  101. package/dist/test/stats.test.js.map +1 -0
  102. package/dist/test/utils.test.d.ts +1 -0
  103. package/dist/test/utils.test.js +19 -0
  104. package/dist/test/utils.test.js.map +1 -0
  105. package/dist/util.d.ts +17 -0
  106. package/dist/util.js +115 -0
  107. package/dist/util.js.map +1 -0
  108. package/jest.config.js +11 -0
  109. package/package.json +40 -0
  110. package/src/adaptable.ts +12 -0
  111. package/src/calc.ts +40 -0
  112. package/src/data/abilities.ts +383 -0
  113. package/src/data/index.ts +36 -0
  114. package/src/data/interface.ts +176 -0
  115. package/src/data/items.ts +632 -0
  116. package/src/data/moves.ts +5028 -0
  117. package/src/data/natures.ts +65 -0
  118. package/src/data/species.ts +10098 -0
  119. package/src/data/types.ts +478 -0
  120. package/src/desc.ts +1063 -0
  121. package/src/field.ts +124 -0
  122. package/src/index.ts +156 -0
  123. package/src/items.ts +423 -0
  124. package/src/mechanics/gen12.ts +297 -0
  125. package/src/mechanics/gen3.ts +444 -0
  126. package/src/mechanics/gen4.ts +702 -0
  127. package/src/mechanics/gen56.ts +1134 -0
  128. package/src/mechanics/gen789.ts +1788 -0
  129. package/src/mechanics/util.ts +676 -0
  130. package/src/move.ts +337 -0
  131. package/src/pokemon.ts +244 -0
  132. package/src/result.ts +106 -0
  133. package/src/state.ts +81 -0
  134. package/src/stats.ts +213 -0
  135. package/src/test/calc.test.ts +1588 -0
  136. package/src/test/data.test.ts +129 -0
  137. package/src/test/gen.ts +514 -0
  138. package/src/test/helper.ts +185 -0
  139. package/src/test/move.test.ts +13 -0
  140. package/src/test/pokemon.test.ts +121 -0
  141. package/src/test/stats.test.ts +84 -0
  142. package/src/test/utils.test.ts +18 -0
  143. package/src/util.ts +153 -0
  144. package/tsconfig.json +10 -0
package/src/desc.ts ADDED
@@ -0,0 +1,1063 @@
1
+ import type {Generation, Weather, Terrain, TypeName, ID, AbilityName} from './data/interface';
2
+ import type {Field, Side} from './field';
3
+ import type {Move} from './move';
4
+ import type {Pokemon} from './pokemon';
5
+ import {type Damage, damageRange} from './result';
6
+ import {error} from './util';
7
+ // NOTE: This needs to come last to simplify bundling
8
+ import {isGrounded} from './mechanics/util';
9
+
10
+ export interface RawDesc {
11
+ HPEVs?: string;
12
+ attackBoost?: number;
13
+ attackEVs?: string;
14
+ attackerAbility?: string;
15
+ attackerItem?: string;
16
+ attackerName: string;
17
+ attackerTera?: string;
18
+ defenderAbility?: string;
19
+ defenderItem?: string;
20
+ defenderName: string;
21
+ defenderTera?: string;
22
+ defenseBoost?: number;
23
+ defenseEVs?: string;
24
+ hits?: number;
25
+ alliesFainted?: number;
26
+ isStellarFirstUse?: boolean;
27
+ isBeadsOfRuin?: boolean;
28
+ isSwordOfRuin?: boolean;
29
+ isTabletsOfRuin?: boolean;
30
+ isVesselOfRuin?: boolean;
31
+ isAuroraVeil?: boolean;
32
+ isFlowerGiftAttacker?: boolean;
33
+ isFlowerGiftDefender?: boolean;
34
+ isSteelySpiritAttacker?: boolean;
35
+ isFriendGuard?: boolean;
36
+ isHelpingHand?: boolean;
37
+ isCritical?: boolean;
38
+ isLightScreen?: boolean;
39
+ isBurned?: boolean;
40
+ isProtected?: boolean;
41
+ isReflect?: boolean;
42
+ isBattery?: boolean;
43
+ isPowerSpot?: boolean;
44
+ isWonderRoom?: boolean;
45
+ isSwitching?: 'out' | 'in';
46
+ moveBP?: number;
47
+ moveName: string;
48
+ moveTurns?: string;
49
+ moveType?: TypeName;
50
+ rivalry?: 'buffed' | 'nerfed';
51
+ terrain?: Terrain;
52
+ weather?: Weather;
53
+ isDefenderDynamaxed?: boolean;
54
+ }
55
+
56
+ export function display(
57
+ gen: Generation,
58
+ attacker: Pokemon,
59
+ defender: Pokemon,
60
+ move: Move,
61
+ field: Field,
62
+ damage: Damage,
63
+ rawDesc: RawDesc,
64
+ notation = '%',
65
+ err = true
66
+ ) {
67
+ const [minDamage, maxDamage] = damageRange(damage);
68
+ const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]);
69
+ const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]);
70
+
71
+ const minDisplay = toDisplay(notation, min, defender.maxHP());
72
+ const maxDisplay = toDisplay(notation, max, defender.maxHP());
73
+
74
+ const desc = buildDescription(rawDesc, attacker, defender);
75
+ const damageText = `${min}-${max} (${minDisplay} - ${maxDisplay}${notation})`;
76
+
77
+ if (move.category === 'Status' && !move.named('Nature Power')) return `${desc}: ${damageText}`;
78
+ const koChanceText = getKOChance(gen, attacker, defender, move, field, damage, err).text;
79
+ return koChanceText ? `${desc}: ${damageText} -- ${koChanceText}` : `${desc}: ${damageText}`;
80
+ }
81
+
82
+ export function displayMove(
83
+ gen: Generation,
84
+ attacker: Pokemon,
85
+ defender: Pokemon,
86
+ move: Move,
87
+ damage: Damage,
88
+ notation = '%'
89
+ ) {
90
+ const [minDamage, maxDamage] = damageRange(damage);
91
+ const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]);
92
+ const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]);
93
+
94
+ const minDisplay = toDisplay(notation, min, defender.maxHP());
95
+ const maxDisplay = toDisplay(notation, max, defender.maxHP());
96
+
97
+ const recoveryText = getRecovery(gen, attacker, defender, move, damage, notation).text;
98
+ const recoilText = getRecoil(gen, attacker, defender, move, damage, notation).text;
99
+
100
+ return `${minDisplay} - ${maxDisplay}${notation}${recoveryText &&
101
+ ` (${recoveryText})`}${recoilText && ` (${recoilText})`}`;
102
+ }
103
+
104
+ export function getRecovery(
105
+ gen: Generation,
106
+ attacker: Pokemon,
107
+ defender: Pokemon,
108
+ move: Move,
109
+ damage: Damage,
110
+ notation = '%'
111
+ ) {
112
+ const [minDamage, maxDamage] = damageRange(damage);
113
+ const minD = typeof minDamage === 'number' ? [minDamage] : minDamage;
114
+ const maxD = typeof maxDamage === 'number' ? [maxDamage] : maxDamage;
115
+
116
+ const recovery = [0, 0] as [number, number];
117
+ let text = '';
118
+
119
+ const ignoresShellBell =
120
+ gen.num === 3 && move.named('Doom Desire', 'Future Sight');
121
+ if (attacker.hasItem('Shell Bell') && !ignoresShellBell) {
122
+ const max = Math.round(defender.maxHP() / 8);
123
+ for (let i = 0; i < minD.length; i++) {
124
+ recovery[0] += Math.min(Math.round(minD[i] * move.hits / 8), max);
125
+ recovery[1] += Math.min(Math.round(maxD[i] * move.hits / 8), max);
126
+ }
127
+ }
128
+
129
+ if (move.named('G-Max Finale')) {
130
+ recovery[0] = recovery[1] = Math.round(attacker.maxHP() / 6);
131
+ }
132
+
133
+ if (move.named('Pain Split')) {
134
+ const average = Math.floor((attacker.curHP() + defender.curHP()) / 2);
135
+ recovery[0] = recovery[1] = average - attacker.curHP();
136
+ }
137
+
138
+ if (move.drain) {
139
+ const percentHealed = move.drain[0] / move.drain[1];
140
+ const max = Math.round(defender.maxHP() * percentHealed);
141
+ for (let i = 0; i < minD.length; i++) {
142
+ const range = [minD[i], maxD[i]];
143
+ for (const j in recovery) {
144
+ let drained = Math.round(range[j] * percentHealed);
145
+ if (attacker.hasItem('Big Root')) drained = Math.trunc(drained * 5324 / 4096);
146
+ recovery[j] += Math.min(drained * move.hits, max);
147
+ }
148
+ }
149
+ }
150
+
151
+ if (recovery[1] === 0) return {recovery, text};
152
+
153
+ const minHealthRecovered = toDisplay(notation, recovery[0], attacker.maxHP());
154
+ const maxHealthRecovered = toDisplay(notation, recovery[1], attacker.maxHP());
155
+ const change = recovery[0] > 0 ? 'recovered' : 'lost';
156
+ text = `${minHealthRecovered} - ${maxHealthRecovered}${notation} ${change}`;
157
+
158
+ return {recovery, text};
159
+ }
160
+
161
+ // TODO: return recoil damage as exact HP
162
+ export function getRecoil(
163
+ gen: Generation,
164
+ attacker: Pokemon,
165
+ defender: Pokemon,
166
+ move: Move,
167
+ damage: Damage,
168
+ notation = '%'
169
+ ) {
170
+ const [minDamage, maxDamage] = damageRange(damage);
171
+ const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]) * move.hits;
172
+ const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]) * move.hits;
173
+
174
+ let recoil: [number, number] | number = [0, 0];
175
+ let text = '';
176
+
177
+ const damageOverflow = minDamage > defender.curHP() || maxDamage > defender.curHP();
178
+ if (move.recoil) {
179
+ const mod = (move.recoil[0] / move.recoil[1]) * 100;
180
+ let minRecoilDamage, maxRecoilDamage;
181
+ if (damageOverflow) {
182
+ minRecoilDamage =
183
+ toDisplay(notation, defender.curHP() * mod, attacker.maxHP(), 100);
184
+ maxRecoilDamage =
185
+ toDisplay(notation, defender.curHP() * mod, attacker.maxHP(), 100);
186
+ } else {
187
+ minRecoilDamage = toDisplay(
188
+ notation, Math.min(min, defender.curHP()) * mod, attacker.maxHP(), 100
189
+ );
190
+ maxRecoilDamage = toDisplay(
191
+ notation, Math.min(max, defender.curHP()) * mod, attacker.maxHP(), 100
192
+ );
193
+ }
194
+ if (!attacker.hasAbility('Rock Head')) {
195
+ recoil = [minRecoilDamage, maxRecoilDamage];
196
+ text = `${minRecoilDamage} - ${maxRecoilDamage}${notation} recoil damage`;
197
+ }
198
+ } else if (move.hasCrashDamage) {
199
+ const genMultiplier = gen.num === 2 ? 12.5 : gen.num >= 3 ? 50 : 1;
200
+
201
+ let minRecoilDamage, maxRecoilDamage;
202
+ if (damageOverflow && gen.num !== 2) {
203
+ minRecoilDamage =
204
+ toDisplay(notation, defender.curHP() * genMultiplier, attacker.maxHP(), 100);
205
+ maxRecoilDamage =
206
+ toDisplay(notation, defender.curHP() * genMultiplier, attacker.maxHP(), 100);
207
+ } else {
208
+ minRecoilDamage = toDisplay(
209
+ notation, Math.min(min, defender.maxHP()) * genMultiplier, attacker.maxHP(), 100
210
+ );
211
+ maxRecoilDamage = toDisplay(
212
+ notation, Math.min(max, defender.maxHP()) * genMultiplier, attacker.maxHP(), 100
213
+ );
214
+ }
215
+
216
+ recoil = [minRecoilDamage, maxRecoilDamage];
217
+ switch (gen.num) {
218
+ case 1:
219
+ recoil = toDisplay(notation, 1, attacker.maxHP());
220
+ text = '1hp damage on miss';
221
+ break;
222
+ case 2: case 3: case 4:
223
+ if (defender.hasType('Ghost')) {
224
+ if (gen.num === 4) {
225
+ const gen4CrashDamage = Math.floor(((defender.maxHP() * 0.5) / attacker.maxHP()) * 100);
226
+ recoil = notation === '%' ? gen4CrashDamage : Math.floor((gen4CrashDamage / 100) * 48);
227
+ text = `${gen4CrashDamage}% crash damage`;
228
+ } else {
229
+ recoil = 0;
230
+ text = 'no crash damage on Ghost types';
231
+ }
232
+ } else {
233
+ text = `${minRecoilDamage} - ${maxRecoilDamage}${notation} crash damage on miss`;
234
+ }
235
+ break;
236
+ default:
237
+ recoil = notation === '%' ? 24 : 50;
238
+ text = '50% crash damage';
239
+ }
240
+ } else if (move.struggleRecoil) {
241
+ recoil = notation === '%' ? 12 : 25;
242
+ text = '25% struggle damage';
243
+ // Struggle recoil is actually rounded down in Gen 4 per DaWoblefet's research, but until we
244
+ // return recoil damage as exact HP the best we can do is add some more text to this effect
245
+ if (gen.num === 4) text += ' (rounded down)';
246
+ } else if (move.mindBlownRecoil) {
247
+ recoil = notation === '%' ? 24 : 50;
248
+ text = '50% recoil damage';
249
+ }
250
+
251
+ return {recoil, text};
252
+ }
253
+
254
+ export function getKOChance(
255
+ gen: Generation,
256
+ attacker: Pokemon,
257
+ defender: Pokemon,
258
+ move: Move,
259
+ field: Field,
260
+ damage: Damage,
261
+ err = true
262
+ ) {
263
+ damage = combine(damage);
264
+ if (isNaN(damage[0])) {
265
+ error(err, 'damage[0] must be a number.');
266
+ return {chance: 0, n: 0, text: ''};
267
+ }
268
+ if (damage[damage.length - 1] === 0) {
269
+ error(err, 'damage[damage.length - 1] === 0.');
270
+ return {chance: 0, n: 0, text: ''};
271
+ }
272
+
273
+ // Code doesn't really work if these aren't set.
274
+ if (move.timesUsed === undefined) move.timesUsed = 1;
275
+ if (move.timesUsedWithMetronome === undefined) move.timesUsedWithMetronome = 1;
276
+
277
+ if (damage[0] >= defender.maxHP() && move.timesUsed === 1 && move.timesUsedWithMetronome === 1) {
278
+ return {chance: 1, n: 1, text: 'guaranteed OHKO'};
279
+ }
280
+
281
+ const hazards = getHazards(gen, defender, field.defenderSide);
282
+ const eot = getEndOfTurn(gen, attacker, defender, move, field);
283
+ const toxicCounter =
284
+ defender.hasStatus('tox') && !defender.hasAbility('Magic Guard', 'Poison Heal')
285
+ ? defender.toxicCounter : 0;
286
+
287
+ // multi-hit moves have too many possibilities for brute-forcing to work, so reduce it
288
+ // to an approximate distribution
289
+ let qualifier = move.hits > 1 ? 'approx. ' : '';
290
+
291
+ const hazardsText = hazards.texts.length > 0
292
+ ? ' after ' + serializeText(hazards.texts)
293
+ : '';
294
+ const afterText =
295
+ hazards.texts.length > 0 || eot.texts.length > 0
296
+ ? ' after ' + serializeText(hazards.texts.concat(eot.texts))
297
+ : '';
298
+ const afterTextNoHazards = eot.texts.length > 0 ? ' after ' + serializeText(eot.texts) : '';
299
+
300
+ function roundChance(chance: number) {
301
+ // prevent displaying misleading 100% or 0% chances
302
+ return Math.max(Math.min(Math.round(chance * 1000), 999), 1) / 10;
303
+ }
304
+
305
+ function KOChance(
306
+ chanceWithoutEot: number | undefined,
307
+ chanceWithEot: number | undefined,
308
+ n: number,
309
+ multipleTurns = false,
310
+ ) {
311
+ // chanceWithoutEot and chanceWithEot are calculated separately for OHKOs
312
+ // because the difference between KOing at start of turn is very important in some cases
313
+ // for 2HKOs and onward, only chanceWithEot is calculated,
314
+ // so chanceWithoutEot will be set to 0 for the purposes of this function
315
+ // all this really does is skip straight to that last else if block
316
+ // using the number of hits we can determine the type of KO we are checking for
317
+ // chance is the value that is returned by this function,
318
+ // and is the higher of the two chance parameters
319
+ const KOTurnText = n === 1 ? 'OHKO'
320
+ : (multipleTurns ? `KO in ${n} turns` : `${n}HKO`);
321
+ let text = qualifier;
322
+ let chance = undefined;
323
+ if (chanceWithoutEot === undefined || chanceWithEot === undefined) {
324
+ text += `possible ${KOTurnText}`;
325
+ // not a KO
326
+ } else if (chanceWithoutEot + chanceWithEot === 0) {
327
+ chance = 0;
328
+ text += 'not a KO';
329
+ // if the move OHKOing is guaranteed even without end of turn damage
330
+ } else if (chanceWithoutEot === 1) {
331
+ chance = chanceWithoutEot;
332
+ if (qualifier === '') text += 'guaranteed ';
333
+ text += `OHKO${hazardsText}`;
334
+ } else if (chanceWithoutEot > 0) {
335
+ chance = chanceWithEot;
336
+ // if the move OHKOing is possible, but eot damage guarantees the OHKO
337
+ // I have it so that the text specifies the chance of the OHKO without eot damage,
338
+ // because it might matter in some scenarios
339
+ // eg. if your opponent has a move that can OHKO you but you're faster,
340
+ // it might be important to get the OKKO before they can move
341
+ if (chanceWithEot === 1) {
342
+ text += `${roundChance(chanceWithoutEot)}% chance to ${KOTurnText}${hazardsText} ` +
343
+ `(guaranteed ${KOTurnText}${afterTextNoHazards})`;
344
+ // if the move OHKOing is possible, and eot damage increases the odds of the KO
345
+ } else if (chanceWithEot > chanceWithoutEot) {
346
+ text += `${roundChance(chanceWithoutEot)}% chance to ${KOTurnText}${hazardsText} ` +
347
+ `(${qualifier}${roundChance(chanceWithEot)}% chance to ` +
348
+ `${KOTurnText}${afterTextNoHazards})`;
349
+ // if the move KOing is possible, and eot damage does not increase the odds of the KO
350
+ } else if (chanceWithoutEot > 0) {
351
+ text += `${roundChance(chanceWithoutEot)}% chance to ${KOTurnText}${hazardsText}`;
352
+ }
353
+ } else if (chanceWithoutEot === 0) {
354
+ chance = chanceWithEot;
355
+ // if the move KOing is not possible, but eot damage guarantees the OHKO
356
+ if (chanceWithEot === 1) {
357
+ if (qualifier === '') text += 'guaranteed ';
358
+ text += `${KOTurnText}${afterText}`;
359
+ // if the move KOing is not possible, but eot damage might KO
360
+ } else if (chanceWithEot > 0) {
361
+ text += `${roundChance(chanceWithEot)}% chance to ${KOTurnText}${afterText}`;
362
+ }
363
+ }
364
+ return {chance, n, text};
365
+ }
366
+
367
+ if ((move.timesUsed === 1 && move.timesUsedWithMetronome === 1) || move.isZ) {
368
+ const chance = computeKOChance(
369
+ damage, defender.curHP() - hazards.damage, 0, 1, 1, defender.maxHP(), 0
370
+ );
371
+ const chanceWithEot = computeKOChance(
372
+ damage, defender.curHP() - hazards.damage, eot.damage, 1, 1, defender.maxHP(), toxicCounter
373
+ );
374
+
375
+ // checks if either chance is greater than 0
376
+ if (chance + chanceWithEot > 0) return KOChance(chance, chanceWithEot, 1);
377
+
378
+ // Parental Bond's combined first + second hit only is accurate for chance to OHKO, for
379
+ // multihit KOs its only approximated. We should be doing squashMultihit here instead of
380
+ // pretending we ar emore accurate than we are, but just throwing on an qualifer should be
381
+ // sufficient.
382
+ if (damage.length === 256) {
383
+ qualifier = 'approx. ';
384
+ // damage = squashMultihit(gen, damage, move.hits, err);
385
+ }
386
+
387
+ for (let i = 2; i <= 4; i++) {
388
+ const chance = computeKOChance(
389
+ damage, defender.curHP() - hazards.damage, eot.damage, i, 1, defender.maxHP(), toxicCounter
390
+ );
391
+ if (chance > 0) return KOChance(0, chance, i);
392
+ }
393
+
394
+ for (let i = 5; i <= 9; i++) {
395
+ if (
396
+ predictTotal(damage[0], eot.damage, i, 1, toxicCounter, defender.maxHP()) >=
397
+ defender.curHP() - hazards.damage
398
+ ) {
399
+ return KOChance(0, 1, i);
400
+ } else if (
401
+ predictTotal(damage[damage.length - 1], eot.damage, i, 1, toxicCounter, defender.maxHP()) >=
402
+ defender.curHP() - hazards.damage
403
+ ) {
404
+ // possible but no concrete chance
405
+ return KOChance(undefined, undefined, i);
406
+ }
407
+ }
408
+ } else {
409
+ const chance = computeKOChance(
410
+ damage, defender.maxHP() - hazards.damage,
411
+ eot.damage,
412
+ move.hits || 1,
413
+ move.timesUsed || 1,
414
+ defender.maxHP(),
415
+ toxicCounter
416
+ );
417
+ if (chance > 0) return KOChance(0, chance, move.timesUsed, chance === 1);
418
+
419
+ if (predictTotal(
420
+ damage[0],
421
+ eot.damage,
422
+ 1,
423
+ move.timesUsed,
424
+ toxicCounter,
425
+ defender.maxHP()
426
+ ) >=
427
+ defender.curHP() - hazards.damage
428
+ ) {
429
+ return KOChance(0, 1, move.timesUsed, true);
430
+ } else if (
431
+ predictTotal(
432
+ damage[damage.length - 1],
433
+ eot.damage,
434
+ 1,
435
+ move.timesUsed,
436
+ toxicCounter,
437
+ defender.maxHP()
438
+ ) >=
439
+ defender.curHP() - hazards.damage
440
+ ) {
441
+ // possible but no real idea
442
+ return KOChance(undefined, undefined, move.timesUsed, true);
443
+ }
444
+ return KOChance(0, 0, move.timesUsed);
445
+ }
446
+
447
+ return {chance: 0, n: 0, text: ''};
448
+ }
449
+
450
+ function combine(damage: Damage) {
451
+ // Fixed Damage
452
+ if (typeof damage === 'number') return [damage];
453
+ // Standard Damage
454
+ if (damage.length > 2) {
455
+ if (damage[0] > damage[damage.length - 1]) damage = damage.slice().sort() as number[];
456
+ return damage as number[];
457
+ }
458
+ // Fixed Parental Bond Damage
459
+ if (typeof damage[0] === 'number' && typeof damage[1] === 'number') {
460
+ return [damage[0] + damage[1]];
461
+ }
462
+ // Parental Bond Damage
463
+ const d = damage as [number[], number[]];
464
+ const combined = [];
465
+ for (let i = 0; i < d[0].length; i++) { // eslint-disable-line
466
+ for (let j = 0; j < d[1].length; j++) { // eslint-disable-line
467
+ combined.push(d[0][i] + d[1][j]);
468
+ }
469
+ }
470
+ return combined.sort();
471
+ }
472
+
473
+ const TRAPPING = [
474
+ 'Bind', 'Clamp', 'Fire Spin', 'Infestation', 'Magma Storm', 'Sand Tomb',
475
+ 'Thunder Cage', 'Whirlpool', 'Wrap', 'G-Max Sandblast', 'G-Max Centiferno',
476
+ ];
477
+
478
+ function getHazards(gen: Generation, defender: Pokemon, defenderSide: Side) {
479
+ let damage = 0;
480
+ const texts: string[] = [];
481
+
482
+ if (defender.hasItem('Heavy-Duty Boots')) {
483
+ return {damage, texts};
484
+ }
485
+ if (defenderSide.isSR && !defender.hasAbility('Magic Guard', 'Mountaineer')) {
486
+ const rockType = gen.types.get('rock' as ID)!;
487
+ const effectiveness =
488
+ rockType.effectiveness[defender.types[0]]! *
489
+ (defender.types[1] ? rockType.effectiveness[defender.types[1]]! : 1);
490
+ damage += Math.floor((effectiveness * defender.maxHP()) / 8);
491
+ texts.push('Stealth Rock');
492
+ }
493
+ if (defenderSide.steelsurge && !defender.hasAbility('Magic Guard', 'Mountaineer')) {
494
+ const steelType = gen.types.get('steel' as ID)!;
495
+ const effectiveness =
496
+ steelType.effectiveness[defender.types[0]]! *
497
+ (defender.types[1] ? steelType.effectiveness[defender.types[1]]! : 1);
498
+ damage += Math.floor((effectiveness * defender.maxHP()) / 8);
499
+ texts.push('Steelsurge');
500
+ }
501
+
502
+ if (!defender.hasType('Flying') &&
503
+ !defender.hasAbility('Magic Guard', 'Levitate') &&
504
+ !defender.hasItem('Air Balloon')
505
+ ) {
506
+ if (defenderSide.spikes === 1) {
507
+ damage += Math.floor(defender.maxHP() / 8);
508
+ if (gen.num === 2) {
509
+ texts.push('Spikes');
510
+ } else {
511
+ texts.push('1 layer of Spikes');
512
+ }
513
+ } else if (defenderSide.spikes === 2) {
514
+ damage += Math.floor(defender.maxHP() / 6);
515
+ texts.push('2 layers of Spikes');
516
+ } else if (defenderSide.spikes === 3) {
517
+ damage += Math.floor(defender.maxHP() / 4);
518
+ texts.push('3 layers of Spikes');
519
+ }
520
+ }
521
+
522
+ if (isNaN(damage)) {
523
+ damage = 0;
524
+ }
525
+
526
+ return {damage, texts};
527
+ }
528
+
529
+ function getEndOfTurn(
530
+ gen: Generation,
531
+ attacker: Pokemon,
532
+ defender: Pokemon,
533
+ move: Move,
534
+ field: Field
535
+ ) {
536
+ let damage = 0;
537
+ const texts = [];
538
+
539
+ if (field.hasWeather('Sun', 'Harsh Sunshine')) {
540
+ if (defender.hasAbility('Dry Skin', 'Solar Power')) {
541
+ damage -= Math.floor(defender.maxHP() / 8);
542
+ texts.push(defender.ability + ' damage');
543
+ }
544
+ } else if (field.hasWeather('Rain', 'Heavy Rain')) {
545
+ if (defender.hasAbility('Dry Skin')) {
546
+ damage += Math.floor(defender.maxHP() / 8);
547
+ texts.push('Dry Skin recovery');
548
+ } else if (defender.hasAbility('Rain Dish')) {
549
+ damage += Math.floor(defender.maxHP() / 16);
550
+ texts.push('Rain Dish recovery');
551
+ }
552
+ } else if (field.hasWeather('Sand')) {
553
+ if (
554
+ !defender.hasType('Rock', 'Ground', 'Steel') &&
555
+ !defender.hasAbility('Magic Guard', 'Overcoat', 'Sand Force', 'Sand Rush', 'Sand Veil') &&
556
+ !defender.hasItem('Safety Goggles')
557
+ ) {
558
+ damage -= Math.floor(defender.maxHP() / (gen.num === 2 ? 8 : 16));
559
+ texts.push('sandstorm damage');
560
+ }
561
+ } else if (field.hasWeather('Hail', 'Snow')) {
562
+ if (defender.hasAbility('Ice Body')) {
563
+ damage += Math.floor(defender.maxHP() / 16);
564
+ texts.push('Ice Body recovery');
565
+ } else if (
566
+ !defender.hasType('Ice') &&
567
+ !defender.hasAbility('Magic Guard', 'Overcoat', 'Snow Cloak') &&
568
+ !defender.hasItem('Safety Goggles') &&
569
+ field.hasWeather('Hail')
570
+ ) {
571
+ damage -= Math.floor(defender.maxHP() / 16);
572
+ texts.push('hail damage');
573
+ }
574
+ }
575
+
576
+ const loseItem = move.named('Knock Off') && !defender.hasAbility('Sticky Hold');
577
+ if (defender.hasItem('Leftovers') && !loseItem) {
578
+ damage += Math.floor(defender.maxHP() / 16);
579
+ texts.push('Leftovers recovery');
580
+ } else if (defender.hasItem('Black Sludge') && !loseItem) {
581
+ if (defender.hasType('Poison')) {
582
+ damage += Math.floor(defender.maxHP() / 16);
583
+ texts.push('Black Sludge recovery');
584
+ } else if (!defender.hasAbility('Magic Guard', 'Klutz')) {
585
+ damage -= Math.floor(defender.maxHP() / 8);
586
+ texts.push('Black Sludge damage');
587
+ }
588
+ } else if (defender.hasItem('Sticky Barb')) {
589
+ damage -= Math.floor(defender.maxHP() / 8);
590
+ texts.push('Sticky Barb damage');
591
+ }
592
+
593
+ if (field.defenderSide.isSeeded) {
594
+ if (!defender.hasAbility('Magic Guard')) {
595
+ // 1/16 in gen 1, 1/8 in gen 2 onwards
596
+ damage -= Math.floor(defender.maxHP() / (gen.num >= 2 ? 8 : 16));
597
+ texts.push('Leech Seed damage');
598
+ }
599
+ }
600
+
601
+ if (field.attackerSide.isSeeded && !attacker.hasAbility('Magic Guard')) {
602
+ let recovery = Math.floor(attacker.maxHP() / (gen.num >= 2 ? 8 : 16));
603
+ if (defender.hasItem('Big Root')) recovery = Math.trunc(recovery * 5324 / 4096);
604
+ if (attacker.hasAbility('Liquid Ooze')) {
605
+ damage -= recovery;
606
+ texts.push('Liquid Ooze damage');
607
+ } else {
608
+ damage += recovery;
609
+ texts.push('Leech Seed recovery');
610
+ }
611
+ }
612
+
613
+ if (field.hasTerrain('Grassy')) {
614
+ if (isGrounded(defender, field)) {
615
+ damage += Math.floor(defender.maxHP() / 16);
616
+ texts.push('Grassy Terrain recovery');
617
+ }
618
+ }
619
+
620
+ if (defender.hasStatus('psn')) {
621
+ if (defender.hasAbility('Poison Heal')) {
622
+ damage += Math.floor(defender.maxHP() / 8);
623
+ texts.push('Poison Heal');
624
+ } else if (!defender.hasAbility('Magic Guard')) {
625
+ damage -= Math.floor(defender.maxHP() / (gen.num === 1 ? 16 : 8));
626
+ texts.push('poison damage');
627
+ }
628
+ } else if (defender.hasStatus('tox')) {
629
+ if (defender.hasAbility('Poison Heal')) {
630
+ damage += Math.floor(defender.maxHP() / 8);
631
+ texts.push('Poison Heal');
632
+ } else if (!defender.hasAbility('Magic Guard')) {
633
+ texts.push('toxic damage');
634
+ }
635
+ } else if (defender.hasStatus('brn')) {
636
+ if (defender.hasAbility('Heatproof')) {
637
+ damage -= Math.floor(defender.maxHP() / (gen.num > 6 ? 32 : 16));
638
+ texts.push('reduced burn damage');
639
+ } else if (!defender.hasAbility('Magic Guard')) {
640
+ damage -= Math.floor(defender.maxHP() / (gen.num === 1 || gen.num > 6 ? 16 : 8));
641
+ texts.push('burn damage');
642
+ }
643
+ } else if (
644
+ (defender.hasStatus('slp') || defender.hasAbility('Comatose')) &&
645
+ attacker.hasAbility('isBadDreams') &&
646
+ !defender.hasAbility('Magic Guard')
647
+ ) {
648
+ damage -= Math.floor(defender.maxHP() / 8);
649
+ texts.push('Bad Dreams');
650
+ }
651
+
652
+ if (!defender.hasAbility('Magic Guard') && TRAPPING.includes(move.name)) {
653
+ if (attacker.hasItem('Binding Band')) {
654
+ damage -= gen.num > 5 ? Math.floor(defender.maxHP() / 6) : Math.floor(defender.maxHP() / 8);
655
+ texts.push('trapping damage');
656
+ } else {
657
+ damage -= gen.num > 5 ? Math.floor(defender.maxHP() / 8) : Math.floor(defender.maxHP() / 16);
658
+ texts.push('trapping damage');
659
+ }
660
+ }
661
+ if (defender.isSaltCure && !defender.hasAbility('Magic Guard')) {
662
+ const isWaterOrSteel = defender.hasType('Water', 'Steel') ||
663
+ (defender.teraType && ['Water', 'Steel'].includes(defender.teraType));
664
+ damage -= Math.floor(defender.maxHP() / (isWaterOrSteel ? 4 : 8));
665
+ texts.push('Salt Cure');
666
+ }
667
+ if (!defender.hasType('Fire') && !defender.hasAbility('Magic Guard') &&
668
+ (move.named('Fire Pledge (Grass Pledge Boosted)', 'Grass Pledge (Fire Pledge Boosted)'))) {
669
+ damage -= Math.floor(defender.maxHP() / 8);
670
+ texts.push('Sea of Fire damage');
671
+ }
672
+
673
+ if (!defender.hasAbility('Magic Guard') && !defender.hasType('Grass') &&
674
+ (field.defenderSide.vinelash || move.named('G-Max Vine Lash'))) {
675
+ damage -= Math.floor(defender.maxHP() / 6);
676
+ texts.push('Vine Lash damage');
677
+ }
678
+
679
+ if (!defender.hasAbility('Magic Guard') && !defender.hasType('Fire') &&
680
+ (field.defenderSide.wildfire || move.named('G-Max Wildfire'))) {
681
+ damage -= Math.floor(defender.maxHP() / 6);
682
+ texts.push('Wildfire damage');
683
+ }
684
+
685
+ if (!defender.hasAbility('Magic Guard') && !defender.hasType('Water') &&
686
+ (field.defenderSide.cannonade || move.named('G-Max Cannonade'))) {
687
+ damage -= Math.floor(defender.maxHP() / 6);
688
+ texts.push('Cannonade damage');
689
+ }
690
+
691
+ if (!defender.hasAbility('Magic Guard') && !defender.hasType('Rock') &&
692
+ (field.defenderSide.volcalith || move.named('G-Max Volcalith'))) {
693
+ damage -= Math.floor(defender.maxHP() / 6);
694
+ texts.push('Volcalith damage');
695
+ }
696
+
697
+ return {damage, texts};
698
+ }
699
+
700
+ function computeKOChance(
701
+ damage: number[],
702
+ hp: number,
703
+ eot: number,
704
+ hits: number,
705
+ timesUsed: number,
706
+ maxHP: number,
707
+ toxicCounter: number
708
+ ) {
709
+ let toxicDamage = 0;
710
+ if (toxicCounter > 0) {
711
+ toxicDamage = Math.floor((toxicCounter * maxHP) / 16);
712
+ toxicCounter++;
713
+ }
714
+ const n = damage.length;
715
+ if (hits === 1) {
716
+ // ignore end of turn healing for the hit that KOs
717
+ // so that the pokemon doesnt "revive" from being KO'd
718
+ // since recovery happens before toxic damage (and therefore always reduces toxic damage),
719
+ // if the net healing is greater than zero, toxicDamage should also be set to zero.
720
+ if (eot - toxicDamage > 0) {
721
+ eot = 0;
722
+ toxicDamage = 0;
723
+ }
724
+ for (let i = 0; i < n; i++) {
725
+ if (damage[n - 1] - eot + toxicDamage < hp) return 0;
726
+ if (damage[i] - eot + toxicDamage >= hp) {
727
+ return (n - i) / n;
728
+ }
729
+ }
730
+ }
731
+
732
+ let sum = 0;
733
+ let lastc = 0;
734
+ for (let i = 0; i < n; i++) {
735
+ let c;
736
+ if (i === 0 || damage[i] !== damage[i - 1]) {
737
+ c = computeKOChance(
738
+ damage,
739
+ hp - damage[i] + eot - toxicDamage,
740
+ eot,
741
+ hits - 1,
742
+ timesUsed,
743
+ maxHP,
744
+ toxicCounter
745
+ );
746
+ } else {
747
+ c = lastc;
748
+ }
749
+ if (c === 1) {
750
+ sum += n - i;
751
+ break;
752
+ } else {
753
+ sum += c;
754
+ }
755
+ lastc = c;
756
+ }
757
+ return sum / n;
758
+ }
759
+
760
+ function predictTotal(
761
+ damage: number,
762
+ eot: number,
763
+ hits: number,
764
+ timesUsed: number,
765
+ toxicCounter: number,
766
+ maxHP: number
767
+ ) {
768
+ let toxicDamage = 0;
769
+ // hits - 1 is used in this for loop, as well as in the total = ... calcs later
770
+ // the last turn of eot damage is calculated separately
771
+ // since if the damage is less than 0 (healing)
772
+ // we want to exclude that from the calculations
773
+ // since on the last turn the pokemon has been ko'd by the attack
774
+ // and should not be able to heal after fainting
775
+ let lastTurnEot = eot;
776
+ if (toxicCounter > 0) {
777
+ for (let i = 0; i < hits - 1; i++) {
778
+ toxicDamage += Math.floor(((toxicCounter + i) * maxHP) / 16);
779
+ }
780
+ lastTurnEot -= Math.floor(((toxicCounter + (hits - 1)) * maxHP) / 16);
781
+ }
782
+ let total = 0;
783
+ if (hits > 1 && timesUsed === 1) {
784
+ total = damage * hits - eot * (hits - 1) + toxicDamage;
785
+ } else {
786
+ total = damage - eot * (hits - 1) + toxicDamage;
787
+ }
788
+ // if the net eot health gain is negative for the last turn, include it in the total
789
+ if (lastTurnEot < 0) total -= lastTurnEot;
790
+ return total;
791
+ }
792
+
793
+ function squashMultihit(gen: Generation, d: number[], hits: number, err = true) {
794
+ if (d.length === 1) {
795
+ return [d[0] * hits];
796
+ } else if (gen.num === 1) {
797
+ const r = [];
798
+ for (let i = 0; i < d.length; i++) {
799
+ r[i] = d[i] * hits;
800
+ }
801
+ return r;
802
+ } else if (d.length === 16) {
803
+ switch (hits) {
804
+ case 2:
805
+ return [
806
+ 2 * d[0], d[2] + d[3], d[4] + d[4], d[4] + d[5], d[5] + d[6], d[6] + d[6],
807
+ d[6] + d[7], d[7] + d[7], d[8] + d[8], d[8] + d[9], d[9] + d[9], d[9] + d[10],
808
+ d[10] + d[11], d[11] + d[11], d[12] + d[13], 2 * d[15],
809
+ ];
810
+ case 3:
811
+ return [
812
+ 3 * d[0], d[3] + d[3] + d[4], d[4] + d[4] + d[5], d[5] + d[5] + d[6],
813
+ d[5] + d[6] + d[6], d[6] + d[6] + d[7], d[6] + d[7] + d[7], d[7] + d[7] + d[8],
814
+ d[7] + d[8] + d[8], d[8] + d[8] + d[9], d[8] + d[9] + d[9], d[9] + d[9] + d[10],
815
+ d[9] + d[10] + d[10], d[10] + d[11] + d[11], d[11] + d[12] + d[12], 3 * d[15],
816
+ ];
817
+ case 4:
818
+ return [
819
+ 4 * d[0], 4 * d[4], d[4] + d[5] + d[5] + d[5], d[5] + d[5] + d[6] + d[6],
820
+ 4 * d[6], d[6] + d[6] + d[7] + d[7], 4 * d[7], d[7] + d[7] + d[7] + d[8],
821
+ d[7] + d[8] + d[8] + d[8], 4 * d[8], d[8] + d[8] + d[9] + d[9], 4 * d[9],
822
+ d[9] + d[9] + d[10] + d[10], d[10] + d[10] + d[10] + d[11], 4 * d[11], 4 * d[15],
823
+ ];
824
+ case 5:
825
+ return [
826
+ 5 * d[0], d[4] + d[4] + d[4] + d[5] + d[5], d[5] + d[5] + d[5] + d[5] + d[6],
827
+ d[5] + d[6] + d[6] + d[6] + d[6], d[6] + d[6] + d[6] + d[6] + d[7],
828
+ d[6] + d[6] + d[7] + d[7] + d[7], 5 * d[7], d[7] + d[7] + d[7] + d[8] + d[8],
829
+ d[7] + d[7] + d[8] + d[8] + d[8], 5 * d[8], d[8] + d[8] + d[8] + d[9] + d[9],
830
+ d[8] + d[9] + d[9] + d[9] + d[9], d[9] + d[9] + d[9] + d[9] + d[10],
831
+ d[9] + d[10] + d[10] + d[10] + d[10], d[10] + d[10] + d[11] + d[11] + d[11], 5 * d[15],
832
+ ];
833
+ case 10:
834
+ return [
835
+ 10 * d[0], 10 * d[4], 3 * d[4] + 7 * d[5], 5 * d[5] + 5 * d[6], 10 * d[6],
836
+ 5 * d[6] + 5 * d[7], 10 * d[7], 7 * d[7] + 3 * d[8], 3 * d[7] + 7 * d[8], 10 * d[8],
837
+ 5 * d[8] + 5 * d[9], 4 * d[9], 5 * d[9] + 5 * d[10], 7 * d[10] + 3 * d[11], 10 * d[11],
838
+ 10 * d[15],
839
+ ];
840
+ default:
841
+ error(err, `Unexpected # of hits: ${hits}`);
842
+ return d;
843
+ }
844
+ } else if (d.length === 39) {
845
+ switch (hits) {
846
+ case 2:
847
+ return [
848
+ 2 * d[0], 2 * d[7], 2 * d[10], 2 * d[12], 2 * d[14], d[15] + d[16],
849
+ 2 * d[17], d[18] + d[19], d[19] + d[20], 2 * d[21], d[22] + d[23],
850
+ 2 * d[24], 2 * d[26], 2 * d[28], 2 * d[31], 2 * d[38],
851
+ ];
852
+ case 3:
853
+ return [
854
+ 3 * d[0], 3 * d[9], 3 * d[12], 3 * d[13], 3 * d[15], 3 * d[16],
855
+ 3 * d[17], 3 * d[18], 3 * d[20], 3 * d[21], 3 * d[22], 3 * d[23],
856
+ 3 * d[25], 3 * d[26], 3 * d[29], 3 * d[38],
857
+ ];
858
+ case 4:
859
+ return [
860
+ 4 * d[0], 2 * d[10] + 2 * d[11], 4 * d[13], 4 * d[14], 2 * d[15] + 2 * d[16],
861
+ 2 * d[16] + 2 * d[17], 2 * d[17] + 2 * d[18], 2 * d[18] + 2 * d[19],
862
+ 2 * d[19] + 2 * d[20], 2 * d[20] + 2 * d[21], 2 * d[21] + 2 * d[22],
863
+ 2 * d[22] + 2 * d[23], 4 * d[24], 4 * d[25], 2 * d[27] + 2 * d[28], 4 * d[38],
864
+ ];
865
+ case 5:
866
+ return [
867
+ 5 * d[0], 5 * d[11], 5 * d[13], 5 * d[15], 5 * d[16], 5 * d[17],
868
+ 5 * d[18], 5 * d[19], 5 * d[19], 5 * d[20], 5 * d[21], 5 * d[22],
869
+ 5 * d[23], 5 * d[25], 5 * d[27], 5 * d[38],
870
+ ];
871
+ case 10:
872
+ return [
873
+ 10 * d[0], 10 * d[11], 10 * d[13], 10 * d[15], 10 * d[16], 10 * d[17],
874
+ 10 * d[18], 10 * d[19], 10 * d[19], 10 * d[20], 10 * d[21], 10 * d[22],
875
+ 10 * d[23], 10 * d[25], 10 * d[27], 10 * d[38],
876
+ ];
877
+ default:
878
+ error(err, `Unexpected # of hits: ${hits}`);
879
+ return d;
880
+ }
881
+ } else if (d.length === 256) {
882
+ if (hits > 1) {
883
+ error(err, `Unexpected # of hits for Parental Bond: ${hits}`);
884
+ }
885
+ // FIXME: Come up with a better Parental Bond approximation
886
+ const r: number[] = [];
887
+ for (let i = 0; i < 16; i++) {
888
+ let val = 0;
889
+ for (let j = 0; j < 16; j++) {
890
+ val += d[i + j];
891
+ }
892
+ r[i] = Math.round(val / 16);
893
+ }
894
+ return r;
895
+ } else {
896
+ error(err, `Unexpected # of possible damage values: ${d.length}`);
897
+ return d;
898
+ }
899
+ }
900
+
901
+ function buildDescription(description: RawDesc, attacker: Pokemon, defender: Pokemon) {
902
+ const [attackerLevel, defenderLevel] = getDescriptionLevels(attacker, defender);
903
+ let output = '';
904
+ if (description.attackBoost) {
905
+ if (description.attackBoost > 0) {
906
+ output += '+';
907
+ }
908
+ output += description.attackBoost + ' ';
909
+ }
910
+ output = appendIfSet(output, attackerLevel);
911
+ output = appendIfSet(output, description.attackEVs);
912
+ output = appendIfSet(output, description.attackerItem);
913
+ output = appendIfSet(output, description.attackerAbility);
914
+ output = appendIfSet(output, description.rivalry);
915
+ if (description.isBurned) {
916
+ output += 'burned ';
917
+ }
918
+ if (description.alliesFainted) {
919
+ output += Math.min(5, description.alliesFainted) +
920
+ ` ${description.alliesFainted === 1 ? 'ally' : 'allies'} fainted `;
921
+ }
922
+ if (description.attackerTera) {
923
+ output += `Tera ${description.attackerTera} `;
924
+ }
925
+
926
+ if (description.isStellarFirstUse) {
927
+ output += '(First Use) ';
928
+ }
929
+
930
+ if (description.isBeadsOfRuin) {
931
+ output += 'Beads of Ruin ';
932
+ }
933
+ if (description.isSwordOfRuin) {
934
+ output += 'Sword of Ruin ';
935
+ }
936
+ output += description.attackerName + ' ';
937
+ if (description.isHelpingHand) {
938
+ output += 'Helping Hand ';
939
+ }
940
+ if (description.isFlowerGiftAttacker) {
941
+ output += 'with an ally\'s Flower Gift ';
942
+ }
943
+ if (description.isSteelySpiritAttacker) {
944
+ output += 'with an ally\'s Steely Spirit ';
945
+ }
946
+ if (description.isBattery) {
947
+ output += 'Battery boosted ';
948
+ }
949
+ if (description.isPowerSpot) {
950
+ output += 'Power Spot boosted ';
951
+ }
952
+ if (description.isSwitching) {
953
+ output += 'switching boosted ';
954
+ }
955
+ output += description.moveName + ' ';
956
+ if (description.moveBP && description.moveType) {
957
+ output += '(' + description.moveBP + ' BP ' + description.moveType + ') ';
958
+ } else if (description.moveBP) {
959
+ output += '(' + description.moveBP + ' BP) ';
960
+ } else if (description.moveType) {
961
+ output += '(' + description.moveType + ') ';
962
+ }
963
+ if (description.hits) {
964
+ output += '(' + description.hits + ' hits) ';
965
+ }
966
+ output = appendIfSet(output, description.moveTurns);
967
+ output += 'vs. ';
968
+ if (description.defenseBoost) {
969
+ if (description.defenseBoost > 0) {
970
+ output += '+';
971
+ }
972
+ output += description.defenseBoost + ' ';
973
+ }
974
+ output = appendIfSet(output, defenderLevel);
975
+ output = appendIfSet(output, description.HPEVs);
976
+ if (description.defenseEVs) {
977
+ output += '/ ' + description.defenseEVs + ' ';
978
+ }
979
+ output = appendIfSet(output, description.defenderItem);
980
+ output = appendIfSet(output, description.defenderAbility);
981
+ if (description.isTabletsOfRuin) {
982
+ output += 'Tablets of Ruin ';
983
+ }
984
+ if (description.isVesselOfRuin) {
985
+ output += 'Vessel of Ruin ';
986
+ }
987
+ if (description.isProtected) {
988
+ output += 'protected ';
989
+ }
990
+ if (description.isDefenderDynamaxed) {
991
+ output += 'Dynamax ';
992
+ }
993
+ if (description.defenderTera) {
994
+ output += `Tera ${description.defenderTera} `;
995
+ }
996
+ output += description.defenderName;
997
+ if (description.weather && description.terrain) {
998
+ // do nothing
999
+ } else if (description.weather) {
1000
+ output += ' in ' + description.weather;
1001
+ } else if (description.terrain) {
1002
+ output += ' in ' + description.terrain + ' Terrain';
1003
+ }
1004
+ if (description.isReflect) {
1005
+ output += ' through Reflect';
1006
+ } else if (description.isLightScreen) {
1007
+ output += ' through Light Screen';
1008
+ }
1009
+ if (description.isFlowerGiftDefender) {
1010
+ output += ' with an ally\'s Flower Gift';
1011
+ }
1012
+ if (description.isFriendGuard) {
1013
+ output += ' with an ally\'s Friend Guard';
1014
+ }
1015
+ if (description.isAuroraVeil) {
1016
+ output += ' with an ally\'s Aurora Veil';
1017
+ }
1018
+ if (description.isCritical) {
1019
+ output += ' on a critical hit';
1020
+ }
1021
+ if (description.isWonderRoom) {
1022
+ output += ' in Wonder Room';
1023
+ }
1024
+ return output;
1025
+ }
1026
+
1027
+ function getDescriptionLevels(attacker: Pokemon, defender: Pokemon) {
1028
+ if (attacker.level !== defender.level) {
1029
+ return [
1030
+ attacker.level === 100 ? '' : `Lvl ${attacker.level}`,
1031
+ defender.level === 100 ? '' : `Lvl ${defender.level}`,
1032
+ ];
1033
+ }
1034
+ // There's an argument for showing any level thats not 100, but VGC and LC players
1035
+ // probably would rather not see level cruft in their calcs
1036
+ const elide = [100, 50, 5].includes(attacker.level);
1037
+ const level = elide ? '' : `Lvl ${attacker.level}`;
1038
+ return [level, level];
1039
+ }
1040
+
1041
+ function serializeText(arr: string[]) {
1042
+ if (arr.length === 0) {
1043
+ return '';
1044
+ } else if (arr.length === 1) {
1045
+ return arr[0];
1046
+ } else if (arr.length === 2) {
1047
+ return arr[0] + ' and ' + arr[1];
1048
+ } else {
1049
+ let text = '';
1050
+ for (let i = 0; i < arr.length - 1; i++) {
1051
+ text += arr[i] + ', ';
1052
+ }
1053
+ return text + 'and ' + arr[arr.length - 1];
1054
+ }
1055
+ }
1056
+
1057
+ function appendIfSet(str: string, toAppend?: string) {
1058
+ return toAppend ? `${str}${toAppend} ` : str;
1059
+ }
1060
+
1061
+ function toDisplay(notation: string, a: number, b: number, f = 1) {
1062
+ return notation === '%' ? Math.floor((a * (1000 / f)) / b) / 10 : Math.floor((a * (48 / f)) / b);
1063
+ }