@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
@@ -0,0 +1,1788 @@
1
+ import type {Generation, AbilityName, StatID, Terrain} from '../data/interface';
2
+ import {toID} from '../util';
3
+ import {
4
+ getBerryResistType,
5
+ getFlingPower,
6
+ getItemBoostType,
7
+ getMultiAttack,
8
+ getNaturalGift,
9
+ getTechnoBlast,
10
+ SEED_BOOSTED_STAT,
11
+ } from '../items';
12
+ import type {RawDesc} from '../desc';
13
+ import type {Field} from '../field';
14
+ import type {Move} from '../move';
15
+ import type {Pokemon} from '../pokemon';
16
+ import {Result} from '../result';
17
+ import {
18
+ chainMods,
19
+ checkAirLock,
20
+ checkDauntlessShield,
21
+ checkDownload,
22
+ checkEmbody,
23
+ checkForecast,
24
+ checkInfiltrator,
25
+ checkIntimidate,
26
+ checkIntrepidSword,
27
+ checkItem,
28
+ checkMultihitBoost,
29
+ checkSeedBoost,
30
+ checkTeraformZero,
31
+ checkWindRider,
32
+ checkWonderRoom,
33
+ computeFinalStats,
34
+ countBoosts,
35
+ getBaseDamage,
36
+ getStatDescriptionText,
37
+ getFinalDamage,
38
+ getModifiedStat,
39
+ getQPBoostedStat,
40
+ getMoveEffectiveness,
41
+ getShellSideArmCategory,
42
+ getWeight,
43
+ handleFixedDamageMoves,
44
+ isGrounded,
45
+ OF16, OF32,
46
+ pokeRound,
47
+ isQPActive,
48
+ getStabMod,
49
+ getStellarStabMod,
50
+ } from './util';
51
+
52
+ export function calculateSMSSSV(
53
+ gen: Generation,
54
+ attacker: Pokemon,
55
+ defender: Pokemon,
56
+ move: Move,
57
+ field: Field
58
+ ) {
59
+ // #region Initial
60
+
61
+ checkAirLock(attacker, field);
62
+ checkAirLock(defender, field);
63
+ checkTeraformZero(attacker, field);
64
+ checkTeraformZero(defender, field);
65
+ checkForecast(attacker, field.weather);
66
+ checkForecast(defender, field.weather);
67
+ checkItem(attacker, field.isMagicRoom);
68
+ checkItem(defender, field.isMagicRoom);
69
+ checkWonderRoom(attacker, field.isWonderRoom);
70
+ checkWonderRoom(defender, field.isWonderRoom);
71
+ checkSeedBoost(attacker, field);
72
+ checkSeedBoost(defender, field);
73
+ checkDauntlessShield(attacker, gen);
74
+ checkDauntlessShield(defender, gen);
75
+ checkEmbody(attacker, gen);
76
+ checkEmbody(defender, gen);
77
+
78
+ computeFinalStats(gen, attacker, defender, field, 'def', 'spd', 'spe');
79
+
80
+ checkIntimidate(gen, attacker, defender);
81
+ checkIntimidate(gen, defender, attacker);
82
+ checkDownload(attacker, defender, field.isWonderRoom);
83
+ checkDownload(defender, attacker, field.isWonderRoom);
84
+ checkIntrepidSword(attacker, gen);
85
+ checkIntrepidSword(defender, gen);
86
+
87
+ checkWindRider(attacker, field.attackerSide);
88
+ checkWindRider(defender, field.defenderSide);
89
+
90
+ if (move.named('Meteor Beam', 'Electro Shot')) {
91
+ attacker.boosts.spa +=
92
+ attacker.hasAbility('Simple') ? 2
93
+ : attacker.hasAbility('Contrary') ? -1
94
+ : 1;
95
+ // restrict to +- 6
96
+ attacker.boosts.spa = Math.min(6, Math.max(-6, attacker.boosts.spa));
97
+ }
98
+
99
+ computeFinalStats(gen, attacker, defender, field, 'atk', 'spa');
100
+
101
+ checkInfiltrator(attacker, field.defenderSide);
102
+ checkInfiltrator(defender, field.attackerSide);
103
+
104
+ const desc: RawDesc = {
105
+ attackerName: attacker.name,
106
+ moveName: move.name,
107
+ defenderName: defender.name,
108
+ isDefenderDynamaxed: defender.isDynamaxed,
109
+ isWonderRoom: field.isWonderRoom,
110
+ };
111
+
112
+ // only display tera type if it applies
113
+ if (attacker.teraType !== 'Stellar' || move.name === 'Tera Blast' || move.isStellarFirstUse) {
114
+ // tera blast has special behavior with tera stellar
115
+ desc.isStellarFirstUse = attacker.name !== 'Terapagos-Stellar' && move.name === 'Tera Blast' &&
116
+ attacker.teraType === 'Stellar' && move.isStellarFirstUse;
117
+ desc.attackerTera = attacker.teraType;
118
+ }
119
+ if (defender.teraType !== 'Stellar') desc.defenderTera = defender.teraType;
120
+
121
+ if (move.named('Photon Geyser', 'Light That Burns the Sky') ||
122
+ (move.named('Tera Blast') && attacker.teraType)) {
123
+ move.category = attacker.stats.atk > attacker.stats.spa ? 'Physical' : 'Special';
124
+ }
125
+
126
+ const result = new Result(gen, attacker, defender, move, field, 0, desc);
127
+
128
+ if (move.category === 'Status' && !move.named('Nature Power')) {
129
+ return result;
130
+ }
131
+
132
+ if (move.flags.punch && attacker.hasItem('Punching Glove')) {
133
+ desc.attackerItem = attacker.item;
134
+ move.flags.contact = 0;
135
+ }
136
+
137
+ if (move.named('Shell Side Arm') &&
138
+ getShellSideArmCategory(attacker, defender) === 'Physical') {
139
+ move.flags.contact = 1;
140
+ }
141
+
142
+ const breaksProtect = move.breaksProtect || move.isZ || attacker.isDynamaxed ||
143
+ (attacker.hasAbility('Unseen Fist') && move.flags.contact);
144
+
145
+ if (field.defenderSide.isProtected && !breaksProtect) {
146
+ desc.isProtected = true;
147
+ return result;
148
+ }
149
+
150
+ if (move.name === 'Pain Split') {
151
+ const average = Math.floor((attacker.curHP() + defender.curHP()) / 2);
152
+ const damage = Math.max(0, defender.curHP() - average);
153
+ result.damage = damage;
154
+ return result;
155
+ }
156
+
157
+ const defenderAbilityIgnored = defender.hasAbility(
158
+ 'Armor Tail', 'Aroma Veil', 'Aura Break', 'Battle Armor',
159
+ 'Big Pecks', 'Bulletproof', 'Clear Body', 'Contrary',
160
+ 'Damp', 'Dazzling', 'Disguise', 'Dry Skin',
161
+ 'Earth Eater', 'Filter', 'Flash Fire', 'Flower Gift',
162
+ 'Flower Veil', 'Fluffy', 'Friend Guard', 'Fur Coat',
163
+ 'Good as Gold', 'Grass Pelt', 'Guard Dog', 'Heatproof',
164
+ 'Heavy Metal', 'Hyper Cutter', 'Ice Face', 'Ice Scales',
165
+ 'Illuminate', 'Immunity', 'Inner Focus', 'Insomnia',
166
+ 'Keen Eye', 'Leaf Guard', 'Levitate', 'Light Metal',
167
+ 'Lightning Rod', 'Limber', 'Magic Bounce', 'Magma Armor',
168
+ 'Marvel Scale', "Mind's Eye", 'Mirror Armor', 'Motor Drive',
169
+ 'Multiscale', 'Oblivious', 'Overcoat', 'Own Tempo',
170
+ 'Pastel Veil', 'Punk Rock', 'Purifying Salt', 'Queenly Majesty',
171
+ 'Sand Veil', 'Sap Sipper', 'Shell Armor', 'Shield Dust',
172
+ 'Simple', 'Snow Cloak', 'Solid Rock', 'Soundproof',
173
+ 'Sticky Hold', 'Storm Drain', 'Sturdy', 'Suction Cups',
174
+ 'Sweet Veil', 'Tangled Feet', 'Telepathy', 'Tera Shell',
175
+ 'Thermal Exchange', 'Thick Fat', 'Unaware', 'Vital Spirit',
176
+ 'Volt Absorb', 'Water Absorb', 'Water Bubble', 'Water Veil',
177
+ 'Well-Baked Body', 'White Smoke', 'Wind Rider', 'Wonder Guard',
178
+ 'Wonder Skin'
179
+ );
180
+
181
+ const attackerIgnoresAbility = attacker.hasAbility('Mold Breaker', 'Teravolt', 'Turboblaze');
182
+ const moveIgnoresAbility = move.named(
183
+ 'G-Max Drum Solo',
184
+ 'G-Max Fire Ball',
185
+ 'G-Max Hydrosnipe',
186
+ 'Light That Burns the Sky',
187
+ 'Menacing Moonraze Maelstrom',
188
+ 'Moongeist Beam',
189
+ 'Photon Geyser',
190
+ 'Searing Sunraze Smash',
191
+ 'Sunsteel Strike'
192
+ );
193
+
194
+ if (defenderAbilityIgnored && (attackerIgnoresAbility || moveIgnoresAbility)) {
195
+ if (attackerIgnoresAbility) desc.attackerAbility = attacker.ability;
196
+ if (defender.hasItem('Ability Shield')) {
197
+ desc.defenderItem = defender.item;
198
+ } else {
199
+ defender.ability = '' as AbilityName;
200
+ }
201
+ }
202
+
203
+ const ignoresNeutralizingGas = [
204
+ 'As One (Glastrier)', 'As One (Spectrier)', 'Battle Bond', 'Comatose',
205
+ 'Disguise', 'Gulp Missile', 'Ice Face', 'Multitype', 'Neutralizing Gas',
206
+ 'Power Construct', 'RKS System', 'Schooling', 'Shields Down',
207
+ 'Stance Change', 'Tera Shift', 'Zen Mode', 'Zero to Hero',
208
+ ];
209
+
210
+ if (attacker.hasAbility('Neutralizing Gas') &&
211
+ !ignoresNeutralizingGas.includes(defender.ability || '')) {
212
+ desc.attackerAbility = attacker.ability;
213
+ if (defender.hasItem('Ability Shield')) {
214
+ desc.defenderItem = defender.item;
215
+ } else {
216
+ defender.ability = '' as AbilityName;
217
+ }
218
+ }
219
+
220
+ if (defender.hasAbility('Neutralizing Gas') &&
221
+ !ignoresNeutralizingGas.includes(attacker.ability || '')) {
222
+ desc.defenderAbility = defender.ability;
223
+ if (attacker.hasItem('Ability Shield')) {
224
+ desc.attackerItem = attacker.item;
225
+ } else {
226
+ attacker.ability = '' as AbilityName;
227
+ }
228
+ }
229
+
230
+ // Merciless does not ignore Shell Armor, damage dealt to a poisoned Pokemon with Shell Armor
231
+ // will not be a critical hit (UltiMario)
232
+ const isCritical = !defender.hasAbility('Battle Armor', 'Shell Armor') &&
233
+ (move.isCrit || (attacker.hasAbility('Merciless') && defender.hasStatus('psn', 'tox'))) &&
234
+ move.timesUsed === 1;
235
+
236
+ let type = move.type;
237
+ if (move.originalName === 'Weather Ball') {
238
+ const holdingUmbrella = attacker.hasItem('Utility Umbrella');
239
+ type =
240
+ field.hasWeather('Sun', 'Harsh Sunshine') && !holdingUmbrella ? 'Fire'
241
+ : field.hasWeather('Rain', 'Heavy Rain') && !holdingUmbrella ? 'Water'
242
+ : field.hasWeather('Sand') ? 'Rock'
243
+ : field.hasWeather('Hail', 'Snow') ? 'Ice'
244
+ : 'Normal';
245
+ desc.weather = field.weather;
246
+ desc.moveType = type;
247
+ } else if (move.named('Judgment') && attacker.item && attacker.item.includes('Plate')) {
248
+ type = getItemBoostType(attacker.item)!;
249
+ } else if (move.originalName === 'Techno Blast' &&
250
+ attacker.item && attacker.item.includes('Drive')) {
251
+ type = getTechnoBlast(attacker.item)!;
252
+ desc.moveType = type;
253
+ } else if (move.originalName === 'Multi-Attack' &&
254
+ attacker.item && attacker.item.includes('Memory')) {
255
+ type = getMultiAttack(attacker.item)!;
256
+ desc.moveType = type;
257
+ } else if (move.named('Natural Gift') && attacker.item?.endsWith('Berry')) {
258
+ const gift = getNaturalGift(gen, attacker.item)!;
259
+ type = gift.t;
260
+ desc.moveType = type;
261
+ desc.attackerItem = attacker.item;
262
+ } else if (
263
+ move.named('Nature Power') ||
264
+ (move.originalName === 'Terrain Pulse' && isGrounded(attacker, field))
265
+ ) {
266
+ type =
267
+ field.hasTerrain('Electric') ? 'Electric'
268
+ : field.hasTerrain('Grassy') ? 'Grass'
269
+ : field.hasTerrain('Misty') ? 'Fairy'
270
+ : field.hasTerrain('Psychic') ? 'Psychic'
271
+ : 'Normal';
272
+ desc.terrain = field.terrain;
273
+
274
+ if (move.isMax) {
275
+ desc.moveType = type;
276
+ }
277
+
278
+ // If the Nature Power user has the ability Prankster, it cannot affect
279
+ // Dark-types or grounded foes if Psychic Terrain is active
280
+ if (!(move.named('Nature Power') && attacker.hasAbility('Prankster')) &&
281
+ ((defender.types.includes('Dark') ||
282
+ (field.hasTerrain('Psychic') && isGrounded(defender, field))))) {
283
+ desc.moveType = type;
284
+ }
285
+ } else if (move.originalName === 'Revelation Dance') {
286
+ if (attacker.teraType) {
287
+ type = attacker.teraType;
288
+ } else {
289
+ type = attacker.types[0];
290
+ }
291
+ } else if (move.named('Aura Wheel')) {
292
+ if (attacker.named('Morpeko')) {
293
+ type = 'Electric';
294
+ } else if (attacker.named('Morpeko-Hangry')) {
295
+ type = 'Dark';
296
+ }
297
+ } else if (move.named('Raging Bull')) {
298
+ if (attacker.named('Tauros-Paldea-Combat')) {
299
+ type = 'Fighting';
300
+ } else if (attacker.named('Tauros-Paldea-Blaze')) {
301
+ type = 'Fire';
302
+ } else if (attacker.named('Tauros-Paldea-Aqua')) {
303
+ type = 'Water';
304
+ }
305
+
306
+ field.defenderSide.isReflect = false;
307
+ field.defenderSide.isLightScreen = false;
308
+ field.defenderSide.isAuroraVeil = false;
309
+ } else if (move.named('Ivy Cudgel')) {
310
+ if (attacker.name.includes('Ogerpon-Cornerstone')) {
311
+ type = 'Rock';
312
+ } else if (attacker.name.includes('Ogerpon-Hearthflame')) {
313
+ type = 'Fire';
314
+ } else if (attacker.name.includes('Ogerpon-Wellspring')) {
315
+ type = 'Water';
316
+ }
317
+ } else if (
318
+ move.named('Tera Starstorm') && attacker.name === 'Terapagos-Stellar'
319
+ ) {
320
+ move.target = 'allAdjacentFoes';
321
+ type = 'Stellar';
322
+ } else if (move.named('Brick Break', 'Psychic Fangs')) {
323
+ field.defenderSide.isReflect = false;
324
+ field.defenderSide.isLightScreen = false;
325
+ field.defenderSide.isAuroraVeil = false;
326
+ }
327
+
328
+ let hasAteAbilityTypeChange = false;
329
+ let isAerilate = false;
330
+ let isPixilate = false;
331
+ let isRefrigerate = false;
332
+ let isGalvanize = false;
333
+ let isLiquidVoice = false;
334
+ let isNormalize = false;
335
+ const noTypeChange = move.named(
336
+ 'Revelation Dance',
337
+ 'Judgment',
338
+ 'Nature Power',
339
+ 'Techno Blast',
340
+ 'Multi-Attack',
341
+ 'Natural Gift',
342
+ 'Weather Ball',
343
+ 'Terrain Pulse',
344
+ 'Struggle',
345
+ ) || (move.named('Tera Blast') && attacker.teraType);
346
+
347
+ if (!move.isZ && !noTypeChange) {
348
+ const normal = type === 'Normal';
349
+ if ((isAerilate = attacker.hasAbility('Aerilate') && normal)) {
350
+ type = 'Flying';
351
+ } else if ((isGalvanize = attacker.hasAbility('Galvanize') && normal)) {
352
+ type = 'Electric';
353
+ } else if ((isLiquidVoice = attacker.hasAbility('Liquid Voice') && !!move.flags.sound)) {
354
+ type = 'Water';
355
+ } else if ((isPixilate = attacker.hasAbility('Pixilate') && normal)) {
356
+ type = 'Fairy';
357
+ } else if ((isRefrigerate = attacker.hasAbility('Refrigerate') && normal)) {
358
+ type = 'Ice';
359
+ } else if ((isNormalize = attacker.hasAbility('Normalize'))) { // Boosts any type
360
+ type = 'Normal';
361
+ }
362
+ if (isGalvanize || isPixilate || isRefrigerate || isAerilate || isNormalize) {
363
+ desc.attackerAbility = attacker.ability;
364
+ hasAteAbilityTypeChange = true;
365
+ } else if (isLiquidVoice) {
366
+ desc.attackerAbility = attacker.ability;
367
+ }
368
+ }
369
+
370
+ if (move.named('Tera Blast') && attacker.teraType) {
371
+ type = attacker.teraType;
372
+ }
373
+
374
+ move.type = type;
375
+
376
+ // FIXME: this is incorrect, should be move.flags.heal, not move.drain
377
+ if ((attacker.hasAbility('Triage') && move.drain) ||
378
+ (attacker.hasAbility('Gale Wings') &&
379
+ move.hasType('Flying') &&
380
+ attacker.curHP() === attacker.maxHP())) {
381
+ move.priority = 1;
382
+ desc.attackerAbility = attacker.ability;
383
+ }
384
+
385
+ const isGhostRevealed =
386
+ attacker.hasAbility('Scrappy') || attacker.hasAbility('Mind\'s Eye') ||
387
+ field.defenderSide.isForesight;
388
+ const isRingTarget =
389
+ defender.hasItem('Ring Target') && !defender.hasAbility('Klutz');
390
+ const type1Effectiveness = getMoveEffectiveness(
391
+ gen,
392
+ move,
393
+ defender.types[0],
394
+ isGhostRevealed,
395
+ field.isGravity,
396
+ isRingTarget
397
+ );
398
+ const type2Effectiveness = defender.types[1]
399
+ ? getMoveEffectiveness(
400
+ gen,
401
+ move,
402
+ defender.types[1],
403
+ isGhostRevealed,
404
+ field.isGravity,
405
+ isRingTarget
406
+ )
407
+ : 1;
408
+ let typeEffectiveness = type1Effectiveness * type2Effectiveness;
409
+
410
+ if (defender.teraType && defender.teraType !== 'Stellar') {
411
+ typeEffectiveness = getMoveEffectiveness(
412
+ gen,
413
+ move,
414
+ defender.teraType,
415
+ isGhostRevealed,
416
+ field.isGravity,
417
+ isRingTarget
418
+ );
419
+ }
420
+
421
+ if (typeEffectiveness === 0 && move.hasType('Ground') &&
422
+ defender.hasItem('Iron Ball') && !defender.hasAbility('Klutz')) {
423
+ typeEffectiveness = 1;
424
+ }
425
+
426
+ if (typeEffectiveness === 0 && move.named('Thousand Arrows')) {
427
+ typeEffectiveness = 1;
428
+ }
429
+
430
+ if (typeEffectiveness === 0) {
431
+ return result;
432
+ }
433
+
434
+ if ((move.named('Sky Drop') &&
435
+ (defender.hasType('Flying') || defender.weightkg >= 200 || field.isGravity)) ||
436
+ (move.named('Synchronoise') && !defender.hasType(attacker.types[0]) &&
437
+ (!attacker.types[1] || !defender.hasType(attacker.types[1]))) ||
438
+ (move.named('Dream Eater') &&
439
+ (!(defender.hasStatus('slp') || defender.hasAbility('Comatose')))) ||
440
+ (move.named('Steel Roller') && !field.terrain) ||
441
+ (move.named('Poltergeist') && (!defender.item || isQPActive(defender, field)))
442
+ ) {
443
+ return result;
444
+ }
445
+
446
+ if (
447
+ (field.hasWeather('Harsh Sunshine') && move.hasType('Water')) ||
448
+ (field.hasWeather('Heavy Rain') && move.hasType('Fire'))
449
+ ) {
450
+ desc.weather = field.weather;
451
+ return result;
452
+ }
453
+
454
+ if (field.hasWeather('Strong Winds') && defender.hasType('Flying') &&
455
+ gen.types.get(toID(move.type))!.effectiveness['Flying']! > 1) {
456
+ typeEffectiveness /= 2;
457
+ desc.weather = field.weather;
458
+ }
459
+
460
+ if (move.type === 'Stellar') {
461
+ desc.defenderTera = defender.teraType; // always show in this case
462
+ typeEffectiveness = !defender.teraType ? 1 : 2;
463
+ }
464
+
465
+ const turn2typeEffectiveness = typeEffectiveness;
466
+
467
+ // Tera Shell works only at full HP, but for all hits of multi-hit moves
468
+ if (defender.hasAbility('Tera Shell') &&
469
+ defender.curHP() === defender.maxHP() &&
470
+ (!field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying')) ||
471
+ defender.hasItem('Heavy-Duty Boots'))
472
+ ) {
473
+ typeEffectiveness = 0.5;
474
+ desc.defenderAbility = defender.ability;
475
+ }
476
+
477
+ if ((defender.hasAbility('Wonder Guard') && typeEffectiveness <= 1) ||
478
+ (move.hasType('Grass') && defender.hasAbility('Sap Sipper')) ||
479
+ (move.hasType('Fire') && defender.hasAbility('Flash Fire', 'Well-Baked Body')) ||
480
+ (move.hasType('Water') && defender.hasAbility('Dry Skin', 'Storm Drain', 'Water Absorb')) ||
481
+ (move.hasType('Electric') &&
482
+ defender.hasAbility('Lightning Rod', 'Motor Drive', 'Volt Absorb')) ||
483
+ (move.hasType('Ground') &&
484
+ !field.isGravity && !move.named('Thousand Arrows') &&
485
+ !defender.hasItem('Iron Ball') && defender.hasAbility('Levitate')) ||
486
+ (move.flags.bullet && defender.hasAbility('Bulletproof')) ||
487
+ (move.flags.sound && !move.named('Clangorous Soul') && defender.hasAbility('Soundproof')) ||
488
+ (move.priority > 0 && defender.hasAbility('Queenly Majesty', 'Dazzling', 'Armor Tail')) ||
489
+ (move.hasType('Ground') && defender.hasAbility('Earth Eater')) ||
490
+ (move.flags.wind && defender.hasAbility('Wind Rider'))
491
+ ) {
492
+ desc.defenderAbility = defender.ability;
493
+ return result;
494
+ }
495
+
496
+ if (move.hasType('Ground') && !move.named('Thousand Arrows') &&
497
+ !field.isGravity && defender.hasItem('Air Balloon')) {
498
+ desc.defenderItem = defender.item;
499
+ return result;
500
+ }
501
+
502
+ if (move.priority > 0 && field.hasTerrain('Psychic') && isGrounded(defender, field)) {
503
+ desc.terrain = field.terrain;
504
+ return result;
505
+ }
506
+
507
+ const weightBasedMove = move.named('Heat Crash', 'Heavy Slam', 'Low Kick', 'Grass Knot');
508
+ if (defender.isDynamaxed && weightBasedMove) {
509
+ return result;
510
+ }
511
+
512
+ desc.HPEVs = getStatDescriptionText(gen, defender, 'hp');
513
+
514
+ const fixedDamage = handleFixedDamageMoves(attacker, move);
515
+ if (fixedDamage) {
516
+ if (attacker.hasAbility('Parental Bond')) {
517
+ result.damage = [fixedDamage, fixedDamage];
518
+ desc.attackerAbility = attacker.ability;
519
+ } else {
520
+ result.damage = fixedDamage;
521
+ }
522
+ return result;
523
+ }
524
+
525
+ if (move.named('Final Gambit')) {
526
+ result.damage = attacker.curHP();
527
+ return result;
528
+ }
529
+
530
+ if (move.named('Guardian of Alola')) {
531
+ let zLostHP = Math.floor((defender.curHP() * 3) / 4);
532
+ if (field.defenderSide.isProtected && attacker.item && attacker.item.includes(' Z')) {
533
+ zLostHP = Math.ceil(zLostHP / 4 - 0.5);
534
+ }
535
+ result.damage = zLostHP;
536
+ return result;
537
+ }
538
+
539
+ if (move.named('Nature\'s Madness')) {
540
+ const lostHP = field.defenderSide.isProtected ? 0 : Math.floor(defender.curHP() / 2);
541
+ result.damage = lostHP;
542
+ return result;
543
+ }
544
+
545
+ if (move.named('Spectral Thief')) {
546
+ let stat: StatID;
547
+ for (stat in defender.boosts) {
548
+ if (defender.boosts[stat] > 0) {
549
+ attacker.boosts[stat] +=
550
+ attacker.hasAbility('Contrary') ? -defender.boosts[stat]! : defender.boosts[stat]!;
551
+ if (attacker.boosts[stat] > 6) attacker.boosts[stat] = 6;
552
+ if (attacker.boosts[stat] < -6) attacker.boosts[stat] = -6;
553
+ attacker.stats[stat] = getModifiedStat(attacker.rawStats[stat]!, attacker.boosts[stat]!);
554
+ defender.boosts[stat] = 0;
555
+ defender.stats[stat] = defender.rawStats[stat];
556
+ }
557
+ }
558
+ }
559
+
560
+ if (move.hits > 1) {
561
+ desc.hits = move.hits;
562
+ }
563
+
564
+ const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last';
565
+
566
+ // #endregion
567
+ // #region Base Power
568
+
569
+ const basePower = calculateBasePowerSMSSSV(
570
+ gen,
571
+ attacker,
572
+ defender,
573
+ move,
574
+ field,
575
+ hasAteAbilityTypeChange,
576
+ desc
577
+ );
578
+ if (basePower === 0) {
579
+ return result;
580
+ }
581
+
582
+ // #endregion
583
+ // #region (Special) Attack
584
+ const attack = calculateAttackSMSSSV(gen, attacker, defender, move, field, desc, isCritical);
585
+ const attackStat =
586
+ move.named('Shell Side Arm') &&
587
+ getShellSideArmCategory(attacker, defender) === 'Physical'
588
+ ? 'atk'
589
+ : move.named('Body Press')
590
+ ? 'def'
591
+ : move.category === 'Special'
592
+ ? 'spa'
593
+ : 'atk';
594
+ // #endregion
595
+
596
+ // #region (Special) Defense
597
+
598
+ const defense = calculateDefenseSMSSSV(gen, attacker, defender, move, field, desc, isCritical);
599
+ const hitsPhysical = move.overrideDefensiveStat === 'def' || move.category === 'Physical' ||
600
+ (move.named('Shell Side Arm') && getShellSideArmCategory(attacker, defender) === 'Physical');
601
+ const defenseStat = hitsPhysical ? 'def' : 'spd';
602
+
603
+ // #endregion
604
+ // #region Damage
605
+
606
+ const baseDamage = calculateBaseDamageSMSSSV(
607
+ gen,
608
+ attacker,
609
+ defender,
610
+ basePower,
611
+ attack,
612
+ defense,
613
+ move,
614
+ field,
615
+ desc,
616
+ isCritical
617
+ );
618
+
619
+ if (hasTerrainSeed(defender) &&
620
+ field.hasTerrain(defender.item!.substring(0, defender.item!.indexOf(' ')) as Terrain) &&
621
+ SEED_BOOSTED_STAT[defender.item!] === defenseStat) {
622
+ // Last condition applies so the calc doesn't show a seed where it wouldn't affect the outcome
623
+ // (like Grassy Seed when being hit by a special move)
624
+ desc.defenderItem = defender.item;
625
+ }
626
+
627
+ // the random factor is applied between the crit mod and the stab mod, so don't apply anything
628
+ // below this until we're inside the loop
629
+ let preStellarStabMod = getStabMod(attacker, move, desc);
630
+ let stabMod = getStellarStabMod(attacker, move, preStellarStabMod);
631
+
632
+ const applyBurn =
633
+ attacker.hasStatus('brn') &&
634
+ move.category === 'Physical' &&
635
+ !attacker.hasAbility('Guts') &&
636
+ !move.named('Facade');
637
+ desc.isBurned = applyBurn;
638
+ const finalMods = calculateFinalModsSMSSSV(
639
+ gen,
640
+ attacker,
641
+ defender,
642
+ move,
643
+ field,
644
+ desc,
645
+ isCritical,
646
+ typeEffectiveness
647
+ );
648
+
649
+ let protect = false;
650
+ if (field.defenderSide.isProtected &&
651
+ (attacker.isDynamaxed || (move.isZ && attacker.item && attacker.item.includes(' Z')))) {
652
+ protect = true;
653
+ desc.isProtected = true;
654
+ }
655
+
656
+ const finalMod = chainMods(finalMods, 41, 131072);
657
+
658
+ const isSpread = field.gameType !== 'Singles' &&
659
+ ['allAdjacent', 'allAdjacentFoes'].includes(move.target);
660
+
661
+ let childDamage: number[] | undefined;
662
+ if (attacker.hasAbility('Parental Bond') && move.hits === 1 && !isSpread) {
663
+ const child = attacker.clone();
664
+ child.ability = 'Parental Bond (Child)' as AbilityName;
665
+ checkMultihitBoost(gen, child, defender, move, field, desc);
666
+ childDamage = calculateSMSSSV(gen, child, defender, move, field).damage as number[];
667
+ desc.attackerAbility = attacker.ability;
668
+ }
669
+
670
+ let damage = [];
671
+ for (let i = 0; i < 16; i++) {
672
+ damage[i] =
673
+ getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod, protect);
674
+ }
675
+
676
+ desc.attackBoost =
677
+ move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat];
678
+
679
+ if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) {
680
+ // store boosts so intermediate boosts don't show.
681
+ const origDefBoost = desc.defenseBoost;
682
+ const origAtkBoost = desc.attackBoost;
683
+
684
+ let numAttacks = 1;
685
+ if (move.dropsStats && move.timesUsed! > 1) {
686
+ desc.moveTurns = `over ${move.timesUsed} turns`;
687
+ numAttacks = move.timesUsed!;
688
+ } else {
689
+ numAttacks = move.hits;
690
+ }
691
+ let usedItems = [false, false];
692
+ for (let times = 1; times < numAttacks; times++) {
693
+ usedItems = checkMultihitBoost(gen, attacker, defender, move,
694
+ field, desc, usedItems[0], usedItems[1]);
695
+ const newAttack = calculateAttackSMSSSV(gen, attacker, defender, move,
696
+ field, desc, isCritical);
697
+ const newDefense = calculateDefenseSMSSSV(gen, attacker, defender, move,
698
+ field, desc, isCritical);
699
+ // Check if lost -ate ability. Typing stays the same, only boost is lost
700
+ // Cannot be regained during multihit move and no Normal moves with stat drawbacks
701
+ hasAteAbilityTypeChange = hasAteAbilityTypeChange &&
702
+ attacker.hasAbility('Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate', 'Normalize');
703
+
704
+ if ((move.dropsStats && move.timesUsed! > 1)) {
705
+ // Adaptability does not change between hits of a multihit, only between turns
706
+ preStellarStabMod = getStabMod(attacker, move, desc);
707
+ // Hack to make Tera Shell with multihit moves, but not over multiple turns
708
+ typeEffectiveness = turn2typeEffectiveness;
709
+ // Stellar damage boost applies for 1 turn, but all hits of multihit.
710
+ stabMod = getStellarStabMod(attacker, move, preStellarStabMod, times);
711
+ }
712
+
713
+ const newBasePower = calculateBasePowerSMSSSV(
714
+ gen,
715
+ attacker,
716
+ defender,
717
+ move,
718
+ field,
719
+ hasAteAbilityTypeChange,
720
+ desc,
721
+ times + 1
722
+ );
723
+ const newBaseDamage = calculateBaseDamageSMSSSV(
724
+ gen,
725
+ attacker,
726
+ defender,
727
+ newBasePower,
728
+ newAttack,
729
+ newDefense,
730
+ move,
731
+ field,
732
+ desc,
733
+ isCritical
734
+ );
735
+ const newFinalMods = calculateFinalModsSMSSSV(
736
+ gen,
737
+ attacker,
738
+ defender,
739
+ move,
740
+ field,
741
+ desc,
742
+ isCritical,
743
+ typeEffectiveness,
744
+ times
745
+ );
746
+ const newFinalMod = chainMods(newFinalMods, 41, 131072);
747
+
748
+ let damageMultiplier = 0;
749
+ damage = damage.map(affectedAmount => {
750
+ const newFinalDamage = getFinalDamage(
751
+ newBaseDamage,
752
+ damageMultiplier,
753
+ typeEffectiveness,
754
+ applyBurn,
755
+ stabMod,
756
+ newFinalMod,
757
+ protect
758
+ );
759
+ damageMultiplier++;
760
+ return affectedAmount + newFinalDamage;
761
+ });
762
+ }
763
+ desc.defenseBoost = origDefBoost;
764
+ desc.attackBoost = origAtkBoost;
765
+ }
766
+
767
+ result.damage = childDamage ? [damage, childDamage] : damage;
768
+
769
+ // #endregion
770
+
771
+ return result;
772
+ }
773
+
774
+ export function calculateBasePowerSMSSSV(
775
+ gen: Generation,
776
+ attacker: Pokemon,
777
+ defender: Pokemon,
778
+ move: Move,
779
+ field: Field,
780
+ hasAteAbilityTypeChange: boolean,
781
+ desc: RawDesc,
782
+ hit = 1,
783
+ ) {
784
+ const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last';
785
+
786
+ let basePower: number;
787
+
788
+ switch (move.name) {
789
+ case 'Payback':
790
+ basePower = move.bp * (turnOrder === 'last' ? 2 : 1);
791
+ desc.moveBP = basePower;
792
+ break;
793
+ case 'Bolt Beak':
794
+ case 'Fishious Rend':
795
+ basePower = move.bp * (turnOrder !== 'last' ? 2 : 1);
796
+ desc.moveBP = basePower;
797
+ break;
798
+ case 'Pursuit':
799
+ const switching = field.defenderSide.isSwitching === 'out';
800
+ basePower = move.bp * (switching ? 2 : 1);
801
+ if (switching) desc.isSwitching = 'out';
802
+ desc.moveBP = basePower;
803
+ break;
804
+ case 'Electro Ball':
805
+ const r = Math.floor(attacker.stats.spe / defender.stats.spe);
806
+ basePower = r >= 4 ? 150 : r >= 3 ? 120 : r >= 2 ? 80 : r >= 1 ? 60 : 40;
807
+ if (defender.stats.spe === 0) basePower = 40;
808
+ desc.moveBP = basePower;
809
+ break;
810
+ case 'Gyro Ball':
811
+ basePower = Math.min(150, Math.floor((25 * defender.stats.spe) / attacker.stats.spe) + 1);
812
+ if (attacker.stats.spe === 0) basePower = 1;
813
+ desc.moveBP = basePower;
814
+ break;
815
+ case 'Punishment':
816
+ basePower = Math.min(200, 60 + 20 * countBoosts(gen, defender.boosts));
817
+ desc.moveBP = basePower;
818
+ break;
819
+ case 'Low Kick':
820
+ case 'Grass Knot':
821
+ const w = getWeight(defender, desc, 'defender');
822
+ basePower = w >= 200 ? 120 : w >= 100 ? 100 : w >= 50 ? 80 : w >= 25 ? 60 : w >= 10 ? 40 : 20;
823
+ desc.moveBP = basePower;
824
+ break;
825
+ case 'Hex':
826
+ case 'Infernal Parade':
827
+ // Hex deals double damage to Pokemon with Comatose (ih8ih8sn0w)
828
+ basePower = move.bp * (defender.status || defender.hasAbility('Comatose') ? 2 : 1);
829
+ desc.moveBP = basePower;
830
+ break;
831
+ case 'Barb Barrage':
832
+ basePower = move.bp * (defender.hasStatus('psn', 'tox') ? 2 : 1);
833
+ desc.moveBP = basePower;
834
+ break;
835
+ case 'Heavy Slam':
836
+ case 'Heat Crash':
837
+ const wr =
838
+ getWeight(attacker, desc, 'attacker') /
839
+ getWeight(defender, desc, 'defender');
840
+ basePower = wr >= 5 ? 120 : wr >= 4 ? 100 : wr >= 3 ? 80 : wr >= 2 ? 60 : 40;
841
+ desc.moveBP = basePower;
842
+ break;
843
+ case 'Stored Power':
844
+ case 'Power Trip':
845
+ basePower = 20 + 20 * countBoosts(gen, attacker.boosts);
846
+ desc.moveBP = basePower;
847
+ break;
848
+ case 'Acrobatics':
849
+ basePower = move.bp * (attacker.hasItem('Flying Gem') ||
850
+ (!attacker.item || isQPActive(attacker, field)) ? 2 : 1);
851
+ desc.moveBP = basePower;
852
+ break;
853
+ case 'Assurance':
854
+ basePower = move.bp * (defender.hasAbility('Parental Bond (Child)') ? 2 : 1);
855
+ // NOTE: desc.attackerAbility = 'Parental Bond' will already reflect this boost
856
+ break;
857
+ case 'Wake-Up Slap':
858
+ // Wake-Up Slap deals double damage to Pokemon with Comatose (ih8ih8sn0w)
859
+ basePower = move.bp * (defender.hasStatus('slp') || defender.hasAbility('Comatose') ? 2 : 1);
860
+ desc.moveBP = basePower;
861
+ break;
862
+ case 'Smelling Salts':
863
+ basePower = move.bp * (defender.hasStatus('par') ? 2 : 1);
864
+ desc.moveBP = basePower;
865
+ break;
866
+ case 'Weather Ball':
867
+ basePower = move.bp * (field.weather && !field.hasWeather('Strong Winds') ? 2 : 1);
868
+ if (field.hasWeather('Sun', 'Harsh Sunshine', 'Rain', 'Heavy Rain') &&
869
+ attacker.hasItem('Utility Umbrella')) basePower = move.bp;
870
+ desc.moveBP = basePower;
871
+ break;
872
+ case 'Terrain Pulse':
873
+ basePower = move.bp * (isGrounded(attacker, field) && field.terrain ? 2 : 1);
874
+ desc.moveBP = basePower;
875
+ break;
876
+ case 'Rising Voltage':
877
+ basePower = move.bp * ((isGrounded(defender, field) && field.hasTerrain('Electric')) ? 2 : 1);
878
+ desc.moveBP = basePower;
879
+ break;
880
+ case 'Psyblade':
881
+ basePower = move.bp * (field.hasTerrain('Electric') ? 1.5 : 1);
882
+ if (field.hasTerrain('Electric')) {
883
+ desc.moveBP = basePower;
884
+ desc.terrain = field.terrain;
885
+ }
886
+ break;
887
+ case 'Fling':
888
+ basePower = getFlingPower(attacker.item);
889
+ desc.moveBP = basePower;
890
+ desc.attackerItem = attacker.item;
891
+ break;
892
+ case 'Dragon Energy':
893
+ case 'Eruption':
894
+ case 'Water Spout':
895
+ basePower = Math.max(1, Math.floor((150 * attacker.curHP()) / attacker.maxHP()));
896
+ desc.moveBP = basePower;
897
+ break;
898
+ case 'Flail':
899
+ case 'Reversal':
900
+ const p = Math.floor((48 * attacker.curHP()) / attacker.maxHP());
901
+ basePower = p <= 1 ? 200 : p <= 4 ? 150 : p <= 9 ? 100 : p <= 16 ? 80 : p <= 32 ? 40 : 20;
902
+ desc.moveBP = basePower;
903
+ break;
904
+ case 'Natural Gift':
905
+ if (attacker.item?.endsWith('Berry')) {
906
+ const gift = getNaturalGift(gen, attacker.item)!;
907
+ basePower = gift.p;
908
+ desc.attackerItem = attacker.item;
909
+ desc.moveBP = move.bp;
910
+ } else {
911
+ basePower = move.bp;
912
+ }
913
+ break;
914
+ case 'Nature Power':
915
+ move.category = 'Special';
916
+ move.secondaries = true;
917
+
918
+ // Nature Power cannot affect Dark-types if it is affected by Prankster
919
+ if (attacker.hasAbility('Prankster') && defender.types.includes('Dark')) {
920
+ basePower = 0;
921
+ desc.moveName = 'Nature Power';
922
+ desc.attackerAbility = 'Prankster';
923
+ break;
924
+ }
925
+ switch (field.terrain) {
926
+ case 'Electric':
927
+ basePower = 90;
928
+ desc.moveName = 'Thunderbolt';
929
+ break;
930
+ case 'Grassy':
931
+ basePower = 90;
932
+ desc.moveName = 'Energy Ball';
933
+ break;
934
+ case 'Misty':
935
+ basePower = 95;
936
+ desc.moveName = 'Moonblast';
937
+ break;
938
+ case 'Psychic':
939
+ // Nature Power does not affect grounded Pokemon if it is affected by
940
+ // Prankster and there is Psychic Terrain active
941
+ if (attacker.hasAbility('Prankster') && isGrounded(defender, field)) {
942
+ basePower = 0;
943
+ desc.attackerAbility = 'Prankster';
944
+ } else {
945
+ basePower = 90;
946
+ desc.moveName = 'Psychic';
947
+ }
948
+ break;
949
+ default:
950
+ basePower = 80;
951
+ desc.moveName = 'Tri Attack';
952
+ }
953
+ break;
954
+ case 'Water Shuriken':
955
+ basePower = attacker.named('Greninja-Ash') && attacker.hasAbility('Battle Bond') ? 20 : 15;
956
+ desc.moveBP = basePower;
957
+ break;
958
+ // Triple Axel's damage increases after each consecutive hit (20, 40, 60)
959
+ case 'Triple Axel':
960
+ basePower = hit * 20;
961
+ desc.moveBP = move.hits === 2 ? 60 : move.hits === 3 ? 120 : 20;
962
+ break;
963
+ // Triple Kick's damage increases after each consecutive hit (10, 20, 30)
964
+ case 'Triple Kick':
965
+ basePower = hit * 10;
966
+ desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10;
967
+ break;
968
+ case 'Crush Grip':
969
+ case 'Wring Out':
970
+ basePower = 100 * Math.floor((defender.curHP() * 4096) / defender.maxHP());
971
+ basePower = Math.floor(Math.floor((120 * basePower + 2048 - 1) / 4096) / 100) || 1;
972
+ desc.moveBP = basePower;
973
+ break;
974
+ case 'Hard Press':
975
+ basePower = 100 * Math.floor((defender.curHP() * 4096) / defender.maxHP());
976
+ basePower = Math.floor(Math.floor((100 * basePower + 2048 - 1) / 4096) / 100) || 1;
977
+ desc.moveBP = basePower;
978
+ break;
979
+ case 'Tera Blast':
980
+ basePower = attacker.teraType === 'Stellar' ? 100 : 80;
981
+ desc.moveBP = basePower;
982
+ break;
983
+ default:
984
+ basePower = move.bp;
985
+ }
986
+ if (basePower === 0) {
987
+ return 0;
988
+ }
989
+ if (move.named(
990
+ 'Breakneck Blitz', 'Bloom Doom', 'Inferno Overdrive', 'Hydro Vortex', 'Gigavolt Havoc',
991
+ 'Subzero Slammer', 'Supersonic Skystrike', 'Savage Spin-Out', 'Acid Downpour', 'Tectonic Rage',
992
+ 'Continental Crush', 'All-Out Pummeling', 'Shattered Psyche', 'Never-Ending Nightmare',
993
+ 'Devastating Drake', 'Black Hole Eclipse', 'Corkscrew Crash', 'Twinkle Tackle'
994
+ ) || move.isMax) {
995
+ // show z-move power in description
996
+ desc.moveBP = move.bp;
997
+ }
998
+ const bpMods = calculateBPModsSMSSSV(
999
+ gen,
1000
+ attacker,
1001
+ defender,
1002
+ move,
1003
+ field,
1004
+ desc,
1005
+ basePower,
1006
+ hasAteAbilityTypeChange,
1007
+ turnOrder
1008
+ );
1009
+ basePower = OF16(Math.max(1, pokeRound((basePower * chainMods(bpMods, 41, 2097152)) / 4096)));
1010
+ if (
1011
+ attacker.teraType && move.type === attacker.teraType &&
1012
+ attacker.hasType(attacker.teraType) && move.hits === 1 && !move.multiaccuracy &&
1013
+ move.priority <= 0 && move.bp > 0 && !move.named('Dragon Energy', 'Eruption', 'Water Spout') &&
1014
+ basePower < 60 && gen.num >= 9
1015
+ ) {
1016
+ basePower = 60;
1017
+ desc.moveBP = 60;
1018
+ }
1019
+ return basePower;
1020
+ }
1021
+
1022
+ export function calculateBPModsSMSSSV(
1023
+ gen: Generation,
1024
+ attacker: Pokemon,
1025
+ defender: Pokemon,
1026
+ move: Move,
1027
+ field: Field,
1028
+ desc: RawDesc,
1029
+ basePower: number,
1030
+ hasAteAbilityTypeChange: boolean,
1031
+ turnOrder: string
1032
+ ) {
1033
+ const bpMods = [];
1034
+
1035
+ // Move effects
1036
+ const defenderItem = (defender.item && defender.item !== '')
1037
+ ? defender.item : defender.disabledItem;
1038
+ let resistedKnockOffDamage =
1039
+ (!defenderItem || isQPActive(defender, field)) ||
1040
+ (defender.named('Dialga-Origin') && defenderItem === 'Adamant Crystal') ||
1041
+ (defender.named('Palkia-Origin') && defenderItem === 'Lustrous Globe') ||
1042
+ // Griseous Core for gen 9, Griseous Orb otherwise
1043
+ (defender.name.includes('Giratina-Origin') && defenderItem.includes('Griseous')) ||
1044
+ (defender.name.includes('Arceus') && defenderItem.includes('Plate')) ||
1045
+ (defender.name.includes('Genesect') && defenderItem.includes('Drive')) ||
1046
+ (defender.named('Groudon', 'Groudon-Primal') && defenderItem === 'Red Orb') ||
1047
+ (defender.named('Kyogre', 'Kyogre-Primal') && defenderItem === 'Blue Orb') ||
1048
+ (defender.name.includes('Silvally') && defenderItem.includes('Memory')) ||
1049
+ defenderItem.includes(' Z') ||
1050
+ (defender.named('Zacian') && defenderItem === 'Rusted Sword') ||
1051
+ (defender.named('Zamazenta') && defenderItem === 'Rusted Shield') ||
1052
+ (defender.name.includes('Ogerpon-Cornerstone') && defenderItem === 'Cornerstone Mask') ||
1053
+ (defender.name.includes('Ogerpon-Hearthflame') && defenderItem === 'Hearthflame Mask') ||
1054
+ (defender.name.includes('Ogerpon-Wellspring') && defenderItem === 'Wellspring Mask') ||
1055
+ (defender.named('Venomicon-Epilogue') && defenderItem === 'Vile Vial');
1056
+
1057
+ // The last case only applies when the Pokemon has the Mega Stone that matches its species
1058
+ // (or when it's already a Mega-Evolution)
1059
+ if (!resistedKnockOffDamage && defenderItem) {
1060
+ const item = gen.items.get(toID(defenderItem))!;
1061
+ resistedKnockOffDamage = !!item.megaEvolves && defender.name.includes(item.megaEvolves);
1062
+ }
1063
+
1064
+ if ((move.named('Facade') && attacker.hasStatus('brn', 'par', 'psn', 'tox')) ||
1065
+ (move.named('Brine') && defender.curHP() <= defender.maxHP() / 2) ||
1066
+ (move.named('Venoshock') && defender.hasStatus('psn', 'tox')) ||
1067
+ (move.named('Lash Out') && (countBoosts(gen, attacker.boosts) < 0))
1068
+ ) {
1069
+ bpMods.push(8192);
1070
+ desc.moveBP = basePower * 2;
1071
+ } else if (
1072
+ move.named('Expanding Force') && isGrounded(attacker, field) && field.hasTerrain('Psychic')
1073
+ ) {
1074
+ move.target = 'allAdjacentFoes';
1075
+ bpMods.push(6144);
1076
+ desc.moveBP = basePower * 1.5;
1077
+ } else if ((move.named('Knock Off') && !resistedKnockOffDamage) ||
1078
+ (move.named('Misty Explosion') && isGrounded(attacker, field) && field.hasTerrain('Misty')) ||
1079
+ (move.named('Grav Apple') && field.isGravity)
1080
+ ) {
1081
+ bpMods.push(6144);
1082
+ desc.moveBP = basePower * 1.5;
1083
+ } else if (move.named('Solar Beam', 'Solar Blade') &&
1084
+ field.hasWeather('Rain', 'Heavy Rain', 'Sand', 'Hail', 'Snow')) {
1085
+ bpMods.push(2048);
1086
+ desc.moveBP = basePower / 2;
1087
+ desc.weather = field.weather;
1088
+ } else if (move.named('Collision Course', 'Electro Drift')) {
1089
+ const isGhostRevealed =
1090
+ attacker.hasAbility('Scrappy') || attacker.hasAbility('Mind\'s Eye') ||
1091
+ field.defenderSide.isForesight;
1092
+ const isRingTarget =
1093
+ defender.hasItem('Ring Target') && !defender.hasAbility('Klutz');
1094
+ const types = defender.teraType && defender.teraType !== 'Stellar'
1095
+ ? [defender.teraType] : defender.types;
1096
+ const type1Effectiveness = getMoveEffectiveness(
1097
+ gen,
1098
+ move,
1099
+ types[0],
1100
+ isGhostRevealed,
1101
+ field.isGravity,
1102
+ isRingTarget
1103
+ );
1104
+ const type2Effectiveness = types[1] ? getMoveEffectiveness(
1105
+ gen,
1106
+ move,
1107
+ types[1],
1108
+ isGhostRevealed,
1109
+ field.isGravity,
1110
+ isRingTarget
1111
+ ) : 1;
1112
+ if (type1Effectiveness * type2Effectiveness >= 2) {
1113
+ bpMods.push(5461);
1114
+ desc.moveBP = basePower * (5461 / 4096);
1115
+ }
1116
+ }
1117
+
1118
+ if (field.attackerSide.isHelpingHand) {
1119
+ bpMods.push(6144);
1120
+ desc.isHelpingHand = true;
1121
+ }
1122
+
1123
+ // Field effects
1124
+
1125
+ const terrainMultiplier = gen.num > 7 ? 5325 : 6144;
1126
+ if (isGrounded(attacker, field)) {
1127
+ if ((field.hasTerrain('Electric') && move.hasType('Electric')) ||
1128
+ (field.hasTerrain('Grassy') && move.hasType('Grass')) ||
1129
+ (field.hasTerrain('Psychic') && move.hasType('Psychic'))
1130
+ ) {
1131
+ bpMods.push(terrainMultiplier);
1132
+ desc.terrain = field.terrain;
1133
+ }
1134
+ }
1135
+ if (isGrounded(defender, field)) {
1136
+ if ((field.hasTerrain('Misty') && move.hasType('Dragon')) ||
1137
+ (field.hasTerrain('Grassy') && move.named('Bulldoze', 'Earthquake'))
1138
+ ) {
1139
+ bpMods.push(2048);
1140
+ desc.terrain = field.terrain;
1141
+ }
1142
+ }
1143
+
1144
+ // Abilities
1145
+
1146
+ // Use BasePower after moves with custom BP to determine if Technician should boost
1147
+ if ((attacker.hasAbility('Technician') && basePower <= 60) ||
1148
+ (attacker.hasAbility('Flare Boost') &&
1149
+ attacker.hasStatus('brn') && move.category === 'Special') ||
1150
+ (attacker.hasAbility('Toxic Boost') &&
1151
+ attacker.hasStatus('psn', 'tox') && move.category === 'Physical') ||
1152
+ (attacker.hasAbility('Mega Launcher') && move.flags.pulse) ||
1153
+ (attacker.hasAbility('Strong Jaw') && move.flags.bite) ||
1154
+ (attacker.hasAbility('Steely Spirit') && move.hasType('Steel')) ||
1155
+ (attacker.hasAbility('Sharpness') && move.flags.slicing)
1156
+ ) {
1157
+ bpMods.push(6144);
1158
+ desc.attackerAbility = attacker.ability;
1159
+ }
1160
+
1161
+ const aura = `${move.type} Aura`;
1162
+ const isAttackerAura = attacker.hasAbility(aura);
1163
+ const isDefenderAura = defender.hasAbility(aura);
1164
+ const isUserAuraBreak = attacker.hasAbility('Aura Break') || defender.hasAbility('Aura Break');
1165
+ const isFieldAuraBreak = field.isAuraBreak;
1166
+ const isFieldFairyAura = field.isFairyAura && move.type === 'Fairy';
1167
+ const isFieldDarkAura = field.isDarkAura && move.type === 'Dark';
1168
+ const auraActive = isAttackerAura || isDefenderAura || isFieldFairyAura || isFieldDarkAura;
1169
+ const auraBreak = isFieldAuraBreak || isUserAuraBreak;
1170
+ if (auraActive) {
1171
+ if (auraBreak) {
1172
+ bpMods.push(3072);
1173
+ desc.attackerAbility = attacker.ability;
1174
+ desc.defenderAbility = defender.ability;
1175
+ } else {
1176
+ bpMods.push(5448);
1177
+ if (isAttackerAura) desc.attackerAbility = attacker.ability;
1178
+ if (isDefenderAura) desc.defenderAbility = defender.ability;
1179
+ }
1180
+ }
1181
+
1182
+ // Sheer Force does not power up max moves or remove the effects (SadisticMystic)
1183
+ if (
1184
+ (attacker.hasAbility('Sheer Force') &&
1185
+ (move.secondaries || move.named('Order Up')) && !move.isMax) ||
1186
+ (attacker.hasAbility('Sand Force') &&
1187
+ field.hasWeather('Sand') && move.hasType('Rock', 'Ground', 'Steel')) ||
1188
+ (attacker.hasAbility('Analytic') &&
1189
+ (turnOrder !== 'first' || field.defenderSide.isSwitching === 'out')) ||
1190
+ (attacker.hasAbility('Tough Claws') && move.flags.contact) ||
1191
+ (attacker.hasAbility('Punk Rock') && move.flags.sound)
1192
+ ) {
1193
+ bpMods.push(5325);
1194
+ desc.attackerAbility = attacker.ability;
1195
+ }
1196
+
1197
+ if (field.attackerSide.isBattery && move.category === 'Special') {
1198
+ bpMods.push(5325);
1199
+ desc.isBattery = true;
1200
+ }
1201
+
1202
+ if (field.attackerSide.isPowerSpot) {
1203
+ bpMods.push(5325);
1204
+ desc.isPowerSpot = true;
1205
+ }
1206
+
1207
+ if (attacker.hasAbility('Rivalry') && ![attacker.gender, defender.gender].includes('N')) {
1208
+ if (attacker.gender === defender.gender) {
1209
+ bpMods.push(5120);
1210
+ desc.rivalry = 'buffed';
1211
+ } else {
1212
+ bpMods.push(3072);
1213
+ desc.rivalry = 'nerfed';
1214
+ }
1215
+ desc.attackerAbility = attacker.ability;
1216
+ }
1217
+
1218
+ // The -ate abilities already changed move typing earlier, so most checks are done and desc is set
1219
+ // However, Max Moves also don't boost -ate Abilities
1220
+ if (!move.isMax && hasAteAbilityTypeChange) {
1221
+ bpMods.push(4915);
1222
+ }
1223
+
1224
+ if ((attacker.hasAbility('Reckless') && (move.recoil || move.hasCrashDamage)) ||
1225
+ (attacker.hasAbility('Iron Fist') && move.flags.punch)
1226
+ ) {
1227
+ bpMods.push(4915);
1228
+ desc.attackerAbility = attacker.ability;
1229
+ }
1230
+
1231
+ if (gen.num <= 8 && defender.hasAbility('Heatproof') && move.hasType('Fire')) {
1232
+ bpMods.push(2048);
1233
+ desc.defenderAbility = defender.ability;
1234
+ } else if (defender.hasAbility('Dry Skin') && move.hasType('Fire')) {
1235
+ bpMods.push(5120);
1236
+ desc.defenderAbility = defender.ability;
1237
+ }
1238
+
1239
+ if (attacker.hasAbility('Supreme Overlord') && attacker.alliesFainted) {
1240
+ const powMod = [4096, 4506, 4915, 5325, 5734, 6144];
1241
+ bpMods.push(powMod[Math.min(5, attacker.alliesFainted)]);
1242
+ desc.attackerAbility = attacker.ability;
1243
+ desc.alliesFainted = attacker.alliesFainted;
1244
+ }
1245
+
1246
+ // Items
1247
+
1248
+ if (attacker.hasItem(`${move.type} Gem`)) {
1249
+ bpMods.push(5325);
1250
+ desc.attackerItem = attacker.item;
1251
+ } else if (
1252
+ (((attacker.hasItem('Adamant Crystal') && attacker.named('Dialga-Origin')) ||
1253
+ (attacker.hasItem('Adamant Orb') && attacker.named('Dialga'))) &&
1254
+ move.hasType('Steel', 'Dragon')) ||
1255
+ (((attacker.hasItem('Lustrous Orb') &&
1256
+ attacker.named('Palkia')) ||
1257
+ (attacker.hasItem('Lustrous Globe') && attacker.named('Palkia-Origin'))) &&
1258
+ move.hasType('Water', 'Dragon')) ||
1259
+ (((attacker.hasItem('Griseous Orb') || attacker.hasItem('Griseous Core')) &&
1260
+ (attacker.named('Giratina-Origin') || attacker.named('Giratina'))) &&
1261
+ move.hasType('Ghost', 'Dragon')) ||
1262
+ (attacker.hasItem('Vile Vial') &&
1263
+ attacker.named('Venomicon-Epilogue') &&
1264
+ move.hasType('Poison', 'Flying')) ||
1265
+ (attacker.hasItem('Soul Dew') &&
1266
+ attacker.named('Latios', 'Latias', 'Latios-Mega', 'Latias-Mega') &&
1267
+ move.hasType('Psychic', 'Dragon')) ||
1268
+ attacker.item && move.hasType(getItemBoostType(attacker.item)) ||
1269
+ (attacker.name.includes('Ogerpon-Cornerstone') && attacker.hasItem('Cornerstone Mask')) ||
1270
+ (attacker.name.includes('Ogerpon-Hearthflame') && attacker.hasItem('Hearthflame Mask')) ||
1271
+ (attacker.name.includes('Ogerpon-Wellspring') && attacker.hasItem('Wellspring Mask'))
1272
+ ) {
1273
+ bpMods.push(4915);
1274
+ desc.attackerItem = attacker.item;
1275
+ } else if (
1276
+ (attacker.hasItem('Muscle Band') && move.category === 'Physical') ||
1277
+ (attacker.hasItem('Wise Glasses') && move.category === 'Special')
1278
+ ) {
1279
+ bpMods.push(4505);
1280
+ desc.attackerItem = attacker.item;
1281
+ } else if (attacker.hasItem('Punching Glove') && move.flags.punch) {
1282
+ bpMods.push(4506);
1283
+ }
1284
+ return bpMods;
1285
+ }
1286
+
1287
+ export function calculateAttackSMSSSV(
1288
+ gen: Generation,
1289
+ attacker: Pokemon,
1290
+ defender: Pokemon,
1291
+ move: Move,
1292
+ field: Field,
1293
+ desc: RawDesc,
1294
+ isCritical = false
1295
+ ) {
1296
+ let attack: number;
1297
+ const attackStat =
1298
+ move.named('Shell Side Arm') &&
1299
+ getShellSideArmCategory(attacker, defender) === 'Physical'
1300
+ ? 'atk'
1301
+ : move.named('Body Press')
1302
+ ? 'def'
1303
+ : move.category === 'Special'
1304
+ ? 'spa'
1305
+ : 'atk';
1306
+ desc.attackEVs =
1307
+ move.named('Foul Play')
1308
+ ? getStatDescriptionText(gen, defender, attackStat, defender.nature)
1309
+ : getStatDescriptionText(gen, attacker, attackStat, attacker.nature);
1310
+ const attackSource = move.named('Foul Play') ? defender : attacker;
1311
+ if (attackSource.boosts[attackStat] === 0 ||
1312
+ (isCritical && attackSource.boosts[attackStat] < 0)) {
1313
+ attack = attackSource.rawStats[attackStat];
1314
+ } else if (defender.hasAbility('Unaware')) {
1315
+ attack = attackSource.rawStats[attackStat];
1316
+ desc.defenderAbility = defender.ability;
1317
+ } else {
1318
+ attack = getModifiedStat(attackSource.rawStats[attackStat]!, attackSource.boosts[attackStat]!);
1319
+ desc.attackBoost = attackSource.boosts[attackStat];
1320
+ }
1321
+
1322
+ // unlike all other attack modifiers, Hustle gets applied directly
1323
+ if (attacker.hasAbility('Hustle') && move.category === 'Physical') {
1324
+ attack = pokeRound((attack * 3) / 2);
1325
+ desc.attackerAbility = attacker.ability;
1326
+ }
1327
+ const atMods = calculateAtModsSMSSSV(gen, attacker, defender, move, field, desc);
1328
+ attack = OF16(Math.max(1, pokeRound((attack * chainMods(atMods, 410, 131072)) / 4096)));
1329
+ return attack;
1330
+ }
1331
+
1332
+ export function calculateAtModsSMSSSV(
1333
+ gen: Generation,
1334
+ attacker: Pokemon,
1335
+ defender: Pokemon,
1336
+ move: Move,
1337
+ field: Field,
1338
+ desc: RawDesc
1339
+ ) {
1340
+ const atMods = [];
1341
+
1342
+ // Slow Start also halves damage with special Z-moves
1343
+ if ((attacker.hasAbility('Slow Start') && attacker.abilityOn &&
1344
+ (move.category === 'Physical' || (move.category === 'Special' && move.isZ))) ||
1345
+ (attacker.hasAbility('Defeatist') && attacker.curHP() <= attacker.maxHP() / 2)
1346
+ ) {
1347
+ atMods.push(2048);
1348
+ desc.attackerAbility = attacker.ability;
1349
+ } else if (
1350
+ (attacker.hasAbility('Solar Power') &&
1351
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
1352
+ move.category === 'Special') ||
1353
+ (attacker.named('Cherrim') &&
1354
+ attacker.hasAbility('Flower Gift') &&
1355
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
1356
+ move.category === 'Physical')) {
1357
+ atMods.push(6144);
1358
+ desc.attackerAbility = attacker.ability;
1359
+ desc.weather = field.weather;
1360
+ } else if (
1361
+ // Gorilla Tactics has no effect during Dynamax (Anubis)
1362
+ (attacker.hasAbility('Gorilla Tactics') && move.category === 'Physical' &&
1363
+ !attacker.isDynamaxed)) {
1364
+ atMods.push(6144);
1365
+ desc.attackerAbility = attacker.ability;
1366
+ } else if (
1367
+ (attacker.hasAbility('Guts') && attacker.status && move.category === 'Physical') ||
1368
+ (attacker.curHP() <= attacker.maxHP() / 3 &&
1369
+ ((attacker.hasAbility('Overgrow') && move.hasType('Grass')) ||
1370
+ (attacker.hasAbility('Blaze') && move.hasType('Fire')) ||
1371
+ (attacker.hasAbility('Torrent') && move.hasType('Water')) ||
1372
+ (attacker.hasAbility('Swarm') && move.hasType('Bug')))) ||
1373
+ (move.category === 'Special' && attacker.abilityOn && attacker.hasAbility('Plus', 'Minus'))
1374
+ ) {
1375
+ atMods.push(6144);
1376
+ desc.attackerAbility = attacker.ability;
1377
+ } else if (attacker.hasAbility('Flash Fire') && attacker.abilityOn && move.hasType('Fire')) {
1378
+ atMods.push(6144);
1379
+ desc.attackerAbility = 'Flash Fire';
1380
+ } else if (
1381
+ (attacker.hasAbility('Steelworker') && move.hasType('Steel')) ||
1382
+ (attacker.hasAbility('Dragon\'s Maw') && move.hasType('Dragon')) ||
1383
+ (attacker.hasAbility('Rocky Payload') && move.hasType('Rock'))
1384
+ ) {
1385
+ atMods.push(6144);
1386
+ desc.attackerAbility = attacker.ability;
1387
+ } else if (attacker.hasAbility('Transistor') && move.hasType('Electric')) {
1388
+ atMods.push(gen.num >= 9 ? 5325 : 6144);
1389
+ desc.attackerAbility = attacker.ability;
1390
+ } else if (attacker.hasAbility('Stakeout') && attacker.abilityOn) {
1391
+ atMods.push(8192);
1392
+ desc.attackerAbility = attacker.ability;
1393
+ } else if (
1394
+ (attacker.hasAbility('Water Bubble') && move.hasType('Water')) ||
1395
+ (attacker.hasAbility('Huge Power', 'Pure Power') && move.category === 'Physical')
1396
+ ) {
1397
+ atMods.push(8192);
1398
+ desc.attackerAbility = attacker.ability;
1399
+ }
1400
+
1401
+ if (
1402
+ field.attackerSide.isFlowerGift &&
1403
+ !attacker.hasAbility('Flower Gift') &&
1404
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
1405
+ move.category === 'Physical') {
1406
+ atMods.push(6144);
1407
+ desc.weather = field.weather;
1408
+ desc.isFlowerGiftAttacker = true;
1409
+ }
1410
+
1411
+ if (
1412
+ field.attackerSide.isSteelySpirit &&
1413
+ move.hasType('Steel')
1414
+ ) {
1415
+ atMods.push(6144);
1416
+ desc.isSteelySpiritAttacker = true;
1417
+ }
1418
+
1419
+ if ((defender.hasAbility('Thick Fat') && move.hasType('Fire', 'Ice')) ||
1420
+ (defender.hasAbility('Water Bubble') && move.hasType('Fire')) ||
1421
+ (defender.hasAbility('Purifying Salt') && move.hasType('Ghost'))) {
1422
+ atMods.push(2048);
1423
+ desc.defenderAbility = defender.ability;
1424
+ }
1425
+
1426
+ if (gen.num >= 9 && defender.hasAbility('Heatproof') && move.hasType('Fire')) {
1427
+ atMods.push(2048);
1428
+ desc.defenderAbility = defender.ability;
1429
+ }
1430
+ // Pokemon with "-of Ruin" Ability are immune to the opposing "-of Ruin" ability
1431
+ const isTabletsOfRuinActive = (defender.hasAbility('Tablets of Ruin') || field.isTabletsOfRuin) &&
1432
+ !attacker.hasAbility('Tablets of Ruin');
1433
+ const isVesselOfRuinActive = (defender.hasAbility('Vessel of Ruin') || field.isVesselOfRuin) &&
1434
+ !attacker.hasAbility('Vessel of Ruin');
1435
+ if (
1436
+ (isTabletsOfRuinActive && move.category === 'Physical') ||
1437
+ (isVesselOfRuinActive && move.category === 'Special')
1438
+ ) {
1439
+ if (defender.hasAbility('Tablets of Ruin') || defender.hasAbility('Vessel of Ruin')) {
1440
+ desc.defenderAbility = defender.ability;
1441
+ } else {
1442
+ desc[move.category === 'Special' ? 'isVesselOfRuin' : 'isTabletsOfRuin'] = true;
1443
+ }
1444
+ atMods.push(3072);
1445
+ }
1446
+
1447
+ if (isQPActive(attacker, field)) {
1448
+ if (
1449
+ (move.category === 'Physical' && getQPBoostedStat(attacker) === 'atk') ||
1450
+ (move.category === 'Special' && getQPBoostedStat(attacker) === 'spa')
1451
+ ) {
1452
+ atMods.push(5325);
1453
+ desc.attackerAbility = attacker.ability;
1454
+ }
1455
+ }
1456
+
1457
+ if (
1458
+ (attacker.hasAbility('Hadron Engine') && move.category === 'Special' &&
1459
+ field.hasTerrain('Electric')) ||
1460
+ (attacker.hasAbility('Orichalcum Pulse') && move.category === 'Physical' &&
1461
+ field.hasWeather('Sun', 'Harsh Sunshine') && !attacker.hasItem('Utility Umbrella'))
1462
+ ) {
1463
+ atMods.push(5461);
1464
+ desc.attackerAbility = attacker.ability;
1465
+ }
1466
+
1467
+ if ((attacker.hasItem('Thick Club') &&
1468
+ attacker.named('Cubone', 'Marowak', 'Marowak-Alola', 'Marowak-Alola-Totem') &&
1469
+ move.category === 'Physical') ||
1470
+ (attacker.hasItem('Deep Sea Tooth') &&
1471
+ attacker.named('Clamperl') &&
1472
+ move.category === 'Special') ||
1473
+ (attacker.hasItem('Light Ball') && attacker.name.includes('Pikachu') && !move.isZ)
1474
+ ) {
1475
+ atMods.push(8192);
1476
+ desc.attackerItem = attacker.item;
1477
+ // Choice Band/Scarf/Specs move lock and stat boosts are ignored during Dynamax (Anubis)
1478
+ } else if (!move.isZ && !move.isMax &&
1479
+ ((attacker.hasItem('Choice Band') && move.category === 'Physical') ||
1480
+ (attacker.hasItem('Choice Specs') && move.category === 'Special'))
1481
+ ) {
1482
+ atMods.push(6144);
1483
+ desc.attackerItem = attacker.item;
1484
+ }
1485
+ return atMods;
1486
+ }
1487
+
1488
+ export function calculateDefenseSMSSSV(
1489
+ gen: Generation,
1490
+ attacker: Pokemon,
1491
+ defender: Pokemon,
1492
+ move: Move,
1493
+ field: Field,
1494
+ desc: RawDesc,
1495
+ isCritical = false
1496
+ ) {
1497
+ let defense: number;
1498
+ const hitsPhysical = move.overrideDefensiveStat === 'def' || move.category === 'Physical' ||
1499
+ (move.named('Shell Side Arm') && getShellSideArmCategory(attacker, defender) === 'Physical');
1500
+ const defenseStat = hitsPhysical ? 'def' : 'spd';
1501
+ desc.defenseEVs = getStatDescriptionText(gen, defender, defenseStat, defender.nature);
1502
+ if (defender.boosts[defenseStat] === 0 ||
1503
+ (isCritical && defender.boosts[defenseStat] > 0) ||
1504
+ move.ignoreDefensive) {
1505
+ defense = defender.rawStats[defenseStat];
1506
+ } else if (attacker.hasAbility('Unaware')) {
1507
+ defense = defender.rawStats[defenseStat];
1508
+ desc.attackerAbility = attacker.ability;
1509
+ } else {
1510
+ defense = getModifiedStat(defender.rawStats[defenseStat]!, defender.boosts[defenseStat]!);
1511
+ desc.defenseBoost = defender.boosts[defenseStat];
1512
+ }
1513
+
1514
+ // unlike all other defense modifiers, Sandstorm SpD boost gets applied directly
1515
+ if (field.hasWeather('Sand') && defender.hasType('Rock') && !hitsPhysical) {
1516
+ defense = pokeRound((defense * 3) / 2);
1517
+ desc.weather = field.weather;
1518
+ }
1519
+ if (field.hasWeather('Snow') && defender.hasType('Ice') && hitsPhysical) {
1520
+ defense = pokeRound((defense * 3) / 2);
1521
+ desc.weather = field.weather;
1522
+ }
1523
+
1524
+ const dfMods = calculateDfModsSMSSSV(
1525
+ gen,
1526
+ attacker,
1527
+ defender,
1528
+ move,
1529
+ field,
1530
+ desc,
1531
+ isCritical,
1532
+ hitsPhysical
1533
+ );
1534
+
1535
+ return OF16(Math.max(1, pokeRound((defense * chainMods(dfMods, 410, 131072)) / 4096)));
1536
+ }
1537
+
1538
+ export function calculateDfModsSMSSSV(
1539
+ gen: Generation,
1540
+ attacker: Pokemon,
1541
+ defender: Pokemon,
1542
+ move: Move,
1543
+ field: Field,
1544
+ desc: RawDesc,
1545
+ isCritical = false,
1546
+ hitsPhysical = false
1547
+ ) {
1548
+ const dfMods = [];
1549
+ if (defender.hasAbility('Marvel Scale') && defender.status && hitsPhysical) {
1550
+ dfMods.push(6144);
1551
+ desc.defenderAbility = defender.ability;
1552
+ } else if (
1553
+ defender.named('Cherrim') &&
1554
+ defender.hasAbility('Flower Gift') &&
1555
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
1556
+ !hitsPhysical
1557
+ ) {
1558
+ dfMods.push(6144);
1559
+ desc.defenderAbility = defender.ability;
1560
+ desc.weather = field.weather;
1561
+ } else if (
1562
+ field.defenderSide.isFlowerGift &&
1563
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
1564
+ !hitsPhysical) {
1565
+ dfMods.push(6144);
1566
+ desc.weather = field.weather;
1567
+ desc.isFlowerGiftDefender = true;
1568
+ } else if (
1569
+ defender.hasAbility('Grass Pelt') &&
1570
+ field.hasTerrain('Grassy') &&
1571
+ hitsPhysical
1572
+ ) {
1573
+ dfMods.push(6144);
1574
+ desc.defenderAbility = defender.ability;
1575
+ } else if (defender.hasAbility('Fur Coat') && hitsPhysical) {
1576
+ dfMods.push(8192);
1577
+ desc.defenderAbility = defender.ability;
1578
+ }
1579
+ // Pokemon with "-of Ruin" Ability are immune to the opposing "-of Ruin" ability
1580
+ const isSwordOfRuinActive = (attacker.hasAbility('Sword of Ruin') || field.isSwordOfRuin) &&
1581
+ !defender.hasAbility('Sword of Ruin');
1582
+ const isBeadsOfRuinActive = (attacker.hasAbility('Beads of Ruin') || field.isBeadsOfRuin) &&
1583
+ !defender.hasAbility('Beads of Ruin');
1584
+ if (
1585
+ (isSwordOfRuinActive && hitsPhysical) ||
1586
+ (isBeadsOfRuinActive && !hitsPhysical)
1587
+ ) {
1588
+ if (attacker.hasAbility('Sword of Ruin') || attacker.hasAbility('Beads of Ruin')) {
1589
+ desc.attackerAbility = attacker.ability;
1590
+ } else {
1591
+ desc[hitsPhysical ? 'isSwordOfRuin' : 'isBeadsOfRuin'] = true;
1592
+ }
1593
+ dfMods.push(3072);
1594
+ }
1595
+
1596
+ if (isQPActive(defender, field)) {
1597
+ if (
1598
+ (hitsPhysical && getQPBoostedStat(defender) === 'def') ||
1599
+ (!hitsPhysical && getQPBoostedStat(defender) === 'spd')
1600
+ ) {
1601
+ desc.defenderAbility = defender.ability;
1602
+ dfMods.push(5324);
1603
+ }
1604
+ }
1605
+
1606
+ if ((defender.hasItem('Eviolite') &&
1607
+ (defender.name === 'Dipplin' || gen.species.get(toID(defender.name))?.nfe)) ||
1608
+ (!hitsPhysical && defender.hasItem('Assault Vest'))) {
1609
+ dfMods.push(6144);
1610
+ desc.defenderItem = defender.item;
1611
+ } else if (
1612
+ (defender.hasItem('Metal Powder') && defender.named('Ditto') && hitsPhysical) ||
1613
+ (defender.hasItem('Deep Sea Scale') && defender.named('Clamperl') && !hitsPhysical)
1614
+ ) {
1615
+ dfMods.push(8192);
1616
+ desc.defenderItem = defender.item;
1617
+ }
1618
+ return dfMods;
1619
+ }
1620
+
1621
+ function calculateBaseDamageSMSSSV(
1622
+ gen: Generation,
1623
+ attacker: Pokemon,
1624
+ defender: Pokemon,
1625
+ basePower: number,
1626
+ attack: number,
1627
+ defense: number,
1628
+ move: Move,
1629
+ field: Field,
1630
+ desc: RawDesc,
1631
+ isCritical = false,
1632
+ ) {
1633
+ let baseDamage = getBaseDamage(attacker.level, basePower, attack, defense);
1634
+ const isSpread = field.gameType !== 'Singles' &&
1635
+ ['allAdjacent', 'allAdjacentFoes'].includes(move.target);
1636
+ if (isSpread) {
1637
+ baseDamage = pokeRound(OF32(baseDamage * 3072) / 4096);
1638
+ }
1639
+
1640
+ if (attacker.hasAbility('Parental Bond (Child)')) {
1641
+ baseDamage = pokeRound(OF32(baseDamage * 1024) / 4096);
1642
+ }
1643
+
1644
+ if (
1645
+ field.hasWeather('Sun') && move.named('Hydro Steam') && !attacker.hasItem('Utility Umbrella')
1646
+ ) {
1647
+ baseDamage = pokeRound(OF32(baseDamage * 6144) / 4096);
1648
+ desc.weather = field.weather;
1649
+ } else if (!defender.hasItem('Utility Umbrella')) {
1650
+ if (
1651
+ (field.hasWeather('Sun', 'Harsh Sunshine') && move.hasType('Fire')) ||
1652
+ (field.hasWeather('Rain', 'Heavy Rain') && move.hasType('Water'))
1653
+ ) {
1654
+ baseDamage = pokeRound(OF32(baseDamage * 6144) / 4096);
1655
+ desc.weather = field.weather;
1656
+ } else if (
1657
+ (field.hasWeather('Sun') && move.hasType('Water')) ||
1658
+ (field.hasWeather('Rain') && move.hasType('Fire'))
1659
+ ) {
1660
+ baseDamage = pokeRound(OF32(baseDamage * 2048) / 4096);
1661
+ desc.weather = field.weather;
1662
+ }
1663
+ }
1664
+
1665
+ if (isCritical) {
1666
+ baseDamage = Math.floor(OF32(baseDamage * 1.5));
1667
+ desc.isCritical = isCritical;
1668
+ }
1669
+
1670
+ return baseDamage;
1671
+ }
1672
+
1673
+ export function calculateFinalModsSMSSSV(
1674
+ gen: Generation,
1675
+ attacker: Pokemon,
1676
+ defender: Pokemon,
1677
+ move: Move,
1678
+ field: Field,
1679
+ desc: RawDesc,
1680
+ isCritical = false,
1681
+ typeEffectiveness: number,
1682
+ hitCount = 0
1683
+ ) {
1684
+ const finalMods = [];
1685
+
1686
+ if (field.defenderSide.isReflect && move.category === 'Physical' &&
1687
+ !isCritical && !field.defenderSide.isAuroraVeil) {
1688
+ // doesn't stack with Aurora Veil
1689
+ finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048);
1690
+ desc.isReflect = true;
1691
+ } else if (
1692
+ field.defenderSide.isLightScreen && move.category === 'Special' &&
1693
+ !isCritical && !field.defenderSide.isAuroraVeil
1694
+ ) {
1695
+ // doesn't stack with Aurora Veil
1696
+ finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048);
1697
+ desc.isLightScreen = true;
1698
+ }
1699
+ if (field.defenderSide.isAuroraVeil && !isCritical) {
1700
+ finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048);
1701
+ desc.isAuroraVeil = true;
1702
+ }
1703
+
1704
+ if (attacker.hasAbility('Neuroforce') && typeEffectiveness > 1) {
1705
+ finalMods.push(5120);
1706
+ desc.attackerAbility = attacker.ability;
1707
+ } else if (attacker.hasAbility('Sniper') && isCritical) {
1708
+ finalMods.push(6144);
1709
+ desc.attackerAbility = attacker.ability;
1710
+ } else if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) {
1711
+ finalMods.push(8192);
1712
+ desc.attackerAbility = attacker.ability;
1713
+ }
1714
+
1715
+ if (defender.isDynamaxed && move.named('Dynamax Cannon', 'Behemoth Blade', 'Behemoth Bash')) {
1716
+ finalMods.push(8192);
1717
+ }
1718
+
1719
+ if (defender.hasAbility('Multiscale', 'Shadow Shield') &&
1720
+ defender.curHP() === defender.maxHP() &&
1721
+ hitCount === 0 &&
1722
+ (!field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying')) ||
1723
+ defender.hasItem('Heavy-Duty Boots')) && !attacker.hasAbility('Parental Bond (Child)')
1724
+ ) {
1725
+ finalMods.push(2048);
1726
+ desc.defenderAbility = defender.ability;
1727
+ }
1728
+
1729
+ if (defender.hasAbility('Fluffy') && move.flags.contact && !attacker.hasAbility('Long Reach')) {
1730
+ finalMods.push(2048);
1731
+ desc.defenderAbility = defender.ability;
1732
+ } else if (
1733
+ (defender.hasAbility('Punk Rock') && move.flags.sound) ||
1734
+ (defender.hasAbility('Ice Scales') && move.category === 'Special')
1735
+ ) {
1736
+ finalMods.push(2048);
1737
+ desc.defenderAbility = defender.ability;
1738
+ }
1739
+
1740
+ if (defender.hasAbility('Solid Rock', 'Filter', 'Prism Armor') && typeEffectiveness > 1) {
1741
+ finalMods.push(3072);
1742
+ desc.defenderAbility = defender.ability;
1743
+ }
1744
+
1745
+ if (field.defenderSide.isFriendGuard) {
1746
+ finalMods.push(3072);
1747
+ desc.isFriendGuard = true;
1748
+ }
1749
+
1750
+ if (defender.hasAbility('Fluffy') && move.hasType('Fire')) {
1751
+ finalMods.push(8192);
1752
+ desc.defenderAbility = defender.ability;
1753
+ }
1754
+
1755
+ if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1 && !move.isZ) {
1756
+ finalMods.push(4915);
1757
+ desc.attackerItem = attacker.item;
1758
+ } else if (attacker.hasItem('Life Orb')) {
1759
+ finalMods.push(5324);
1760
+ desc.attackerItem = attacker.item;
1761
+ } else if (attacker.hasItem('Metronome') && move.timesUsedWithMetronome! >= 1) {
1762
+ const timesUsedWithMetronome = Math.floor(move.timesUsedWithMetronome!);
1763
+ if (timesUsedWithMetronome <= 4) {
1764
+ finalMods.push(4096 + timesUsedWithMetronome * 819);
1765
+ } else {
1766
+ finalMods.push(8192);
1767
+ }
1768
+ desc.attackerItem = attacker.item;
1769
+ }
1770
+
1771
+ if (move.hasType(getBerryResistType(defender.item)) &&
1772
+ (typeEffectiveness > 1 || move.hasType('Normal')) &&
1773
+ hitCount === 0 &&
1774
+ !attacker.hasAbility('Unnerve', 'As One (Glastrier)', 'As One (Spectrier)')) {
1775
+ if (defender.hasAbility('Ripen')) {
1776
+ finalMods.push(1024);
1777
+ } else {
1778
+ finalMods.push(2048);
1779
+ }
1780
+ desc.defenderItem = defender.item;
1781
+ }
1782
+
1783
+ return finalMods;
1784
+ }
1785
+
1786
+ function hasTerrainSeed(pokemon: Pokemon) {
1787
+ return pokemon.hasItem('Electric Seed', 'Misty Seed', 'Grassy Seed', 'Psychic Seed');
1788
+ }