@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.
- package/.eslintignore +2 -0
- package/.eslintrc +12 -0
- package/bundle +107 -0
- package/dist/adaptable.d.ts +6 -0
- package/dist/adaptable.js +28 -0
- package/dist/adaptable.js.map +1 -0
- package/dist/calc.d.ts +6 -0
- package/dist/calc.js +26 -0
- package/dist/calc.js.map +1 -0
- package/dist/data/abilities.d.ts +15 -0
- package/dist/data/abilities.js +448 -0
- package/dist/data/abilities.js.map +1 -0
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +30 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/interface.d.ts +150 -0
- package/dist/data/interface.js +3 -0
- package/dist/data/interface.js.map +1 -0
- package/dist/data/items.d.ts +24 -0
- package/dist/data/items.js +708 -0
- package/dist/data/items.js.map +1 -0
- package/dist/data/moves.d.ts +86 -0
- package/dist/data/moves.js +5014 -0
- package/dist/data/moves.js.map +1 -0
- package/dist/data/natures.d.ts +17 -0
- package/dist/data/natures.js +127 -0
- package/dist/data/natures.js.map +1 -0
- package/dist/data/production.min.js +1 -0
- package/dist/data/species.d.ts +48 -0
- package/dist/data/species.js +10126 -0
- package/dist/data/species.js.map +1 -0
- package/dist/data/types.d.ts +23 -0
- package/dist/data/types.js +538 -0
- package/dist/data/types.js.map +1 -0
- package/dist/desc.d.ts +65 -0
- package/dist/desc.js +866 -0
- package/dist/desc.js.map +1 -0
- package/dist/field.d.ts +49 -0
- package/dist/field.js +111 -0
- package/dist/field.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/items.d.ts +13 -0
- package/dist/items.js +434 -0
- package/dist/items.js.map +1 -0
- package/dist/mechanics/gen12.d.ts +6 -0
- package/dist/mechanics/gen12.js +271 -0
- package/dist/mechanics/gen12.js.map +1 -0
- package/dist/mechanics/gen3.d.ts +11 -0
- package/dist/mechanics/gen3.js +371 -0
- package/dist/mechanics/gen3.js.map +1 -0
- package/dist/mechanics/gen4.d.ts +11 -0
- package/dist/mechanics/gen4.js +596 -0
- package/dist/mechanics/gen4.js.map +1 -0
- package/dist/mechanics/gen56.d.ts +13 -0
- package/dist/mechanics/gen56.js +836 -0
- package/dist/mechanics/gen56.js.map +1 -0
- package/dist/mechanics/gen789.d.ts +14 -0
- package/dist/mechanics/gen789.js +1325 -0
- package/dist/mechanics/gen789.js.map +1 -0
- package/dist/mechanics/util.d.ts +39 -0
- package/dist/mechanics/util.js +675 -0
- package/dist/mechanics/util.js.map +1 -0
- package/dist/move.d.ts +50 -0
- package/dist/move.js +324 -0
- package/dist/move.js.map +1 -0
- package/dist/pokemon.d.ts +55 -0
- package/dist/pokemon.js +240 -0
- package/dist/pokemon.js.map +1 -0
- package/dist/production.min.js +1 -0
- package/dist/result.d.ts +34 -0
- package/dist/result.js +94 -0
- package/dist/result.js.map +1 -0
- package/dist/state.d.ts +77 -0
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -0
- package/dist/stats.d.ts +26 -0
- package/dist/stats.js +183 -0
- package/dist/stats.js.map +1 -0
- package/dist/test/calc.test.d.ts +1 -0
- package/dist/test/calc.test.js +1297 -0
- package/dist/test/calc.test.js.map +1 -0
- package/dist/test/data.test.d.ts +1 -0
- package/dist/test/data.test.js +368 -0
- package/dist/test/data.test.js.map +1 -0
- package/dist/test/gen.d.ts +135 -0
- package/dist/test/gen.js +636 -0
- package/dist/test/gen.js.map +1 -0
- package/dist/test/helper.d.ts +55 -0
- package/dist/test/helper.js +174 -0
- package/dist/test/helper.js.map +1 -0
- package/dist/test/move.test.d.ts +1 -0
- package/dist/test/move.test.js +14 -0
- package/dist/test/move.test.js.map +1 -0
- package/dist/test/pokemon.test.d.ts +1 -0
- package/dist/test/pokemon.test.js +102 -0
- package/dist/test/pokemon.test.js.map +1 -0
- package/dist/test/stats.test.d.ts +1 -0
- package/dist/test/stats.test.js +64 -0
- package/dist/test/stats.test.js.map +1 -0
- package/dist/test/utils.test.d.ts +1 -0
- package/dist/test/utils.test.js +19 -0
- package/dist/test/utils.test.js.map +1 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.js +115 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +40 -0
- package/src/adaptable.ts +12 -0
- package/src/calc.ts +40 -0
- package/src/data/abilities.ts +383 -0
- package/src/data/index.ts +36 -0
- package/src/data/interface.ts +176 -0
- package/src/data/items.ts +632 -0
- package/src/data/moves.ts +5028 -0
- package/src/data/natures.ts +65 -0
- package/src/data/species.ts +10098 -0
- package/src/data/types.ts +478 -0
- package/src/desc.ts +1063 -0
- package/src/field.ts +124 -0
- package/src/index.ts +156 -0
- package/src/items.ts +423 -0
- package/src/mechanics/gen12.ts +297 -0
- package/src/mechanics/gen3.ts +444 -0
- package/src/mechanics/gen4.ts +702 -0
- package/src/mechanics/gen56.ts +1134 -0
- package/src/mechanics/gen789.ts +1788 -0
- package/src/mechanics/util.ts +676 -0
- package/src/move.ts +337 -0
- package/src/pokemon.ts +244 -0
- package/src/result.ts +106 -0
- package/src/state.ts +81 -0
- package/src/stats.ts +213 -0
- package/src/test/calc.test.ts +1588 -0
- package/src/test/data.test.ts +129 -0
- package/src/test/gen.ts +514 -0
- package/src/test/helper.ts +185 -0
- package/src/test/move.test.ts +13 -0
- package/src/test/pokemon.test.ts +121 -0
- package/src/test/stats.test.ts +84 -0
- package/src/test/utils.test.ts +18 -0
- package/src/util.ts +153 -0
- 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
|
+
}
|