@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,1134 @@
1
+ import type {Generation, AbilityName} from '../data/interface';
2
+ import {toID} from '../util';
3
+ import {
4
+ getItemBoostType,
5
+ getNaturalGift,
6
+ getFlingPower,
7
+ getBerryResistType,
8
+ getTechnoBlast,
9
+ } from '../items';
10
+ import type {RawDesc} from '../desc';
11
+ import type {Field} from '../field';
12
+ import type {Move} from '../move';
13
+ import type {Pokemon} from '../pokemon';
14
+ import {Result} from '../result';
15
+ import {
16
+ chainMods,
17
+ checkAirLock,
18
+ checkDownload,
19
+ checkForecast,
20
+ checkInfiltrator,
21
+ checkIntimidate,
22
+ checkItem,
23
+ checkMultihitBoost,
24
+ checkSeedBoost,
25
+ checkWonderRoom,
26
+ computeFinalStats,
27
+ countBoosts,
28
+ getBaseDamage,
29
+ getStatDescriptionText,
30
+ getFinalDamage,
31
+ getModifiedStat,
32
+ getMoveEffectiveness,
33
+ getStabMod,
34
+ getWeight,
35
+ handleFixedDamageMoves,
36
+ isGrounded,
37
+ OF16, OF32,
38
+ pokeRound,
39
+ } from './util';
40
+
41
+ export function calculateBWXY(
42
+ gen: Generation,
43
+ attacker: Pokemon,
44
+ defender: Pokemon,
45
+ move: Move,
46
+ field: Field
47
+ ) {
48
+ // #region Initial
49
+
50
+ checkAirLock(attacker, field);
51
+ checkAirLock(defender, field);
52
+ checkForecast(attacker, field.weather);
53
+ checkForecast(defender, field.weather);
54
+ checkItem(attacker, field.isMagicRoom);
55
+ checkItem(defender, field.isMagicRoom);
56
+ checkWonderRoom(attacker, field.isWonderRoom);
57
+ checkWonderRoom(defender, field.isWonderRoom);
58
+ checkSeedBoost(attacker, field);
59
+ checkSeedBoost(defender, field);
60
+
61
+ computeFinalStats(gen, attacker, defender, field, 'def', 'spd', 'spe');
62
+
63
+ checkIntimidate(gen, attacker, defender);
64
+ checkIntimidate(gen, defender, attacker);
65
+ checkDownload(attacker, defender, field.isWonderRoom);
66
+ checkDownload(defender, attacker, field.isWonderRoom);
67
+
68
+ computeFinalStats(gen, attacker, defender, field, 'atk', 'spa');
69
+
70
+ checkInfiltrator(attacker, field.defenderSide);
71
+ checkInfiltrator(defender, field.attackerSide);
72
+
73
+ const desc: RawDesc = {
74
+ attackerName: attacker.name,
75
+ moveName: move.name,
76
+ defenderName: defender.name,
77
+ isWonderRoom: field.isWonderRoom,
78
+ };
79
+
80
+ const result = new Result(gen, attacker, defender, move, field, 0, desc);
81
+
82
+ if (move.category === 'Status' && !move.named('Nature Power')) {
83
+ return result;
84
+ }
85
+
86
+ if (field.defenderSide.isProtected && !move.breaksProtect) {
87
+ desc.isProtected = true;
88
+ return result;
89
+ }
90
+
91
+ if (move.name === 'Pain Split') {
92
+ const average = Math.floor((attacker.curHP() + defender.curHP()) / 2);
93
+ const damage = Math.max(0, defender.curHP() - average);
94
+ result.damage = damage;
95
+ return result;
96
+ }
97
+
98
+ const defenderAbilityIgnored = defender.hasAbility(
99
+ 'Aroma Veil', 'Aura Break', 'Battle Armor', 'Big Pecks',
100
+ 'Bulletproof', 'Clear Body', 'Contrary', 'Damp',
101
+ 'Dark Aura', 'Dry Skin', 'Fairy Aura', 'Filter',
102
+ 'Flash Fire', 'Flower Gift', 'Flower Veil', 'Friend Guard',
103
+ 'Fur Coat', 'Grass Pelt', 'Heatproof', 'Heavy Metal',
104
+ 'Hyper Cutter', 'Immunity', 'Inner Focus', 'Insomnia',
105
+ 'Keen Eye', 'Leaf Guard', 'Levitate', 'Light Metal',
106
+ 'Lightning Rod', 'Limber', 'Magic Bounce', 'Magma Armor',
107
+ 'Marvel Scale', 'Motor Drive', 'Multiscale', 'Oblivious',
108
+ 'Overcoat', 'Own Tempo', 'Sand Veil', 'Sap Sipper',
109
+ 'Shell Armor', 'Shield Dust', 'Simple', 'Snow Cloak',
110
+ 'Solid Rock', 'Soundproof', 'Sticky Hold', 'Storm Drain',
111
+ 'Sturdy', 'Suction Cups', 'Sweet Veil', 'Tangled Feet',
112
+ 'Telepathy', 'Thick Fat', 'Unaware', 'Vital Spirit',
113
+ 'Volt Absorb', 'Water Absorb', 'Water Veil', 'White Smoke',
114
+ 'Wonder Guard', 'Wonder Skin'
115
+ );
116
+
117
+ if (attacker.hasAbility('Mold Breaker', 'Teravolt', 'Turboblaze') && defenderAbilityIgnored) {
118
+ defender.ability = '' as AbilityName;
119
+ desc.attackerAbility = attacker.ability;
120
+ }
121
+
122
+ const isCritical =
123
+ move.isCrit && !defender.hasAbility('Battle Armor', 'Shell Armor') && move.timesUsed === 1;
124
+
125
+ if (move.named('Weather Ball')) {
126
+ move.type =
127
+ field.hasWeather('Sun', 'Harsh Sunshine') ? 'Fire'
128
+ : field.hasWeather('Rain', 'Heavy Rain') ? 'Water'
129
+ : field.hasWeather('Sand') ? 'Rock'
130
+ : field.hasWeather('Hail') ? 'Ice'
131
+ : 'Normal';
132
+ desc.weather = field.weather;
133
+ desc.moveType = move.type;
134
+ } else if (move.named('Judgment') && attacker.item && attacker.item.includes('Plate')) {
135
+ move.type = getItemBoostType(attacker.item)!;
136
+ } else if (move.named('Techno Blast') && attacker.item && attacker.item.includes('Drive')) {
137
+ move.type = getTechnoBlast(attacker.item)!;
138
+ } else if (move.named('Natural Gift') && attacker.item?.endsWith('Berry')) {
139
+ const gift = getNaturalGift(gen, attacker.item)!;
140
+ move.type = gift.t;
141
+ move.bp = gift.p;
142
+ desc.attackerItem = attacker.item;
143
+ desc.moveBP = move.bp;
144
+ desc.moveType = move.type;
145
+ } else if (move.named('Nature Power')) {
146
+ if (gen.num === 5) {
147
+ move.type = 'Ground';
148
+ } else {
149
+ move.type =
150
+ field.hasTerrain('Electric') ? 'Electric'
151
+ : field.hasTerrain('Grassy') ? 'Grass'
152
+ : field.hasTerrain('Misty') ? 'Fairy'
153
+ : 'Normal';
154
+ }
155
+ } else if (move.named('Brick Break')) {
156
+ field.defenderSide.isReflect = false;
157
+ field.defenderSide.isLightScreen = false;
158
+ }
159
+
160
+ let hasAteAbilityTypeChange = false;
161
+ let isAerilate = false;
162
+ let isPixilate = false;
163
+ let isRefrigerate = false;
164
+ let isNormalize = false;
165
+ const noTypeChange =
166
+ move.named('Judgment', 'Nature Power', 'Techo Blast', 'Natural Gift', 'Weather Ball',
167
+ 'Struggle');
168
+
169
+ if (!move.isZ && !noTypeChange) {
170
+ const normal = move.hasType('Normal');
171
+ if ((isAerilate = attacker.hasAbility('Aerilate') && normal)) {
172
+ move.type = 'Flying';
173
+ } else if ((isPixilate = attacker.hasAbility('Pixilate') && normal)) {
174
+ move.type = 'Fairy';
175
+ } else if ((isRefrigerate = attacker.hasAbility('Refrigerate') && normal)) {
176
+ move.type = 'Ice';
177
+ } else if ((isNormalize = attacker.hasAbility('Normalize'))) {
178
+ move.type = 'Normal';
179
+ }
180
+ if (isPixilate || isRefrigerate || isAerilate || isNormalize) {
181
+ desc.attackerAbility = attacker.ability;
182
+ }
183
+ if (isPixilate || isRefrigerate || isAerilate) {
184
+ hasAteAbilityTypeChange = true;
185
+ }
186
+ }
187
+
188
+ if (attacker.hasAbility('Gale Wings') && move.hasType('Flying')) {
189
+ move.priority = 1;
190
+ desc.attackerAbility = attacker.ability;
191
+ }
192
+
193
+ const isGhostRevealed = attacker.hasAbility('Scrappy') || field.defenderSide.isForesight;
194
+ const isRingTarget = defender.hasItem('Ring Target') && !defender.hasAbility('Klutz');
195
+ const type1Effectiveness =
196
+ getMoveEffectiveness(gen, move, defender.types[0], isGhostRevealed, field.isGravity,
197
+ isRingTarget);
198
+ const type2Effectiveness = defender.types[1]
199
+ ? getMoveEffectiveness(gen, move, defender.types[1], isGhostRevealed, field.isGravity,
200
+ isRingTarget)
201
+ : 1;
202
+ let typeEffectiveness = type1Effectiveness * type2Effectiveness;
203
+
204
+ if (typeEffectiveness === 0 && move.named('Thousand Arrows')) {
205
+ typeEffectiveness = 1;
206
+ } else if (typeEffectiveness === 0 && move.hasType('Ground') &&
207
+ defender.hasItem('Iron Ball') && !defender.hasAbility('Klutz')) {
208
+ typeEffectiveness = 1;
209
+ }
210
+
211
+ if (typeEffectiveness === 0) {
212
+ return result;
213
+ }
214
+
215
+ if ((move.named('Sky Drop') &&
216
+ (defender.hasType('Flying') || defender.weightkg >= 200 || field.isGravity)) ||
217
+ (move.named('Synchronoise') && !defender.hasType(attacker.types[0]) &&
218
+ (!attacker.types[1] || !defender.hasType(attacker.types[1]))) ||
219
+ (move.named('Dream Eater') && !defender.hasStatus('slp'))
220
+ ) {
221
+ return result;
222
+ }
223
+
224
+ if (
225
+ (field.hasWeather('Harsh Sunshine') && move.hasType('Water')) ||
226
+ (field.hasWeather('Heavy Rain') && move.hasType('Fire'))
227
+ ) {
228
+ desc.weather = field.weather;
229
+ return result;
230
+ }
231
+
232
+ if (field.hasWeather('Strong Winds') && defender.hasType('Flying') &&
233
+ gen.types.get(toID(move.type))!.effectiveness['Flying']! > 1) {
234
+ typeEffectiveness /= 2;
235
+ desc.weather = field.weather;
236
+ }
237
+
238
+ if ((defender.hasAbility('Wonder Guard') && typeEffectiveness <= 1) ||
239
+ (move.hasType('Grass') && defender.hasAbility('Sap Sipper')) ||
240
+ (move.hasType('Fire') && defender.hasAbility('Flash Fire')) ||
241
+ (move.hasType('Water') && defender.hasAbility('Dry Skin', 'Storm Drain', 'Water Absorb')) ||
242
+ (move.hasType('Electric') &&
243
+ defender.hasAbility('Lightning Rod', 'Motor Drive', 'Volt Absorb')) ||
244
+ (move.hasType('Ground') &&
245
+ !field.isGravity && !move.named('Thousand Arrows') &&
246
+ !defender.hasItem('Iron Ball') && defender.hasAbility('Levitate')) ||
247
+ (move.flags.bullet && defender.hasAbility('Bulletproof')) ||
248
+ (move.flags.sound && defender.hasAbility('Soundproof'))
249
+ ) {
250
+ desc.defenderAbility = defender.ability;
251
+ return result;
252
+ }
253
+
254
+ if (move.hasType('Ground') && !move.named('Thousand Arrows') &&
255
+ !field.isGravity && defender.hasItem('Air Balloon')) {
256
+ desc.defenderItem = defender.item;
257
+ return result;
258
+ }
259
+
260
+ if (move.priority > 0 && field.hasTerrain('Psychic') && isGrounded(defender, field)) {
261
+ desc.terrain = field.terrain;
262
+ return result;
263
+ }
264
+
265
+ desc.HPEVs = getStatDescriptionText(gen, defender, 'hp');
266
+
267
+ const fixedDamage = handleFixedDamageMoves(attacker, move);
268
+ if (fixedDamage) {
269
+ if (attacker.hasAbility('Parental Bond')) {
270
+ result.damage = [fixedDamage, fixedDamage];
271
+ desc.attackerAbility = attacker.ability;
272
+ } else {
273
+ result.damage = fixedDamage;
274
+ }
275
+ return result;
276
+ }
277
+
278
+ if (move.named('Final Gambit')) {
279
+ result.damage = attacker.curHP();
280
+ return result;
281
+ }
282
+
283
+ if (move.hits > 1) {
284
+ desc.hits = move.hits;
285
+ }
286
+
287
+ // #endregion
288
+ // #region Base Power
289
+
290
+ const basePower = calculateBasePowerBWXY(
291
+ gen,
292
+ attacker,
293
+ defender,
294
+ move,
295
+ field,
296
+ hasAteAbilityTypeChange,
297
+ desc
298
+ );
299
+ if (basePower === 0) {
300
+ return result;
301
+ }
302
+
303
+ // #endregion
304
+ // #region (Special) Attack
305
+
306
+ const attack = calculateAttackBWXY(gen, attacker, defender, move, field, desc, isCritical);
307
+ const attackStat = move.category === 'Special' ? 'spa' : 'atk';
308
+
309
+ // #endregion
310
+
311
+ // #region (Special) Defense
312
+
313
+ const defense = calculateDefenseBWXY(gen, attacker, defender, move, field, desc, isCritical);
314
+
315
+ // #endregion
316
+ // #region Damage
317
+
318
+ const baseDamage = calculateBaseDamageBWXY(
319
+ gen,
320
+ attacker,
321
+ basePower,
322
+ attack,
323
+ defense,
324
+ move,
325
+ field,
326
+ desc,
327
+ isCritical
328
+ );
329
+
330
+ // the random factor is applied between the crit mod and the stab mod, so don't apply anything
331
+ // below this until we're inside the loop
332
+ let stabMod = getStabMod(attacker, move, desc);
333
+
334
+ const applyBurn =
335
+ attacker.hasStatus('brn') &&
336
+ move.category === 'Physical' &&
337
+ !attacker.hasAbility('Guts') &&
338
+ !(move.named('Facade') && gen.num === 6);
339
+ desc.isBurned = applyBurn;
340
+
341
+ const finalMods = calculateFinalModsBWXY(
342
+ gen,
343
+ attacker,
344
+ defender,
345
+ move,
346
+ field,
347
+ desc,
348
+ isCritical,
349
+ typeEffectiveness
350
+ );
351
+ const finalMod = chainMods(finalMods, 41, 131072);
352
+
353
+ const isSpread = field.gameType !== 'Singles' &&
354
+ ['allAdjacent', 'allAdjacentFoes'].includes(move.target);
355
+
356
+ let childDamage: number[] | undefined;
357
+ if (attacker.hasAbility('Parental Bond') && move.hits === 1 && !isSpread) {
358
+ const child = attacker.clone();
359
+ child.ability = 'Parental Bond (Child)' as AbilityName;
360
+ checkMultihitBoost(gen, child, defender, move, field, desc);
361
+ childDamage = calculateBWXY(gen, child, defender, move, field).damage as number[];
362
+ desc.attackerAbility = attacker.ability;
363
+ }
364
+
365
+ let damage: number[] = [];
366
+ for (let i = 0; i < 16; i++) {
367
+ damage[i] =
368
+ getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod);
369
+ }
370
+
371
+ desc.attackBoost =
372
+ move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat];
373
+
374
+ if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) {
375
+ // store boosts so intermediate boosts don't show.
376
+ const origDefBoost = desc.defenseBoost;
377
+ const origAtkBoost = desc.attackBoost;
378
+ let numAttacks = 1;
379
+ if (move.dropsStats && move.timesUsed! > 1) {
380
+ desc.moveTurns = `over ${move.timesUsed} turns`;
381
+ numAttacks = move.timesUsed!;
382
+ } else {
383
+ numAttacks = move.hits;
384
+ }
385
+ let usedItems = [false, false];
386
+ for (let times = 1; times < numAttacks; times++) {
387
+ usedItems = checkMultihitBoost(gen, attacker, defender, move,
388
+ field, desc, usedItems[0], usedItems[1]);
389
+ const newAtk = calculateAttackBWXY(gen, attacker, defender, move, field, desc, isCritical);
390
+ const newDef = calculateDefenseBWXY(gen, attacker, defender, move, field, desc, isCritical);
391
+ // Check if lost -ate ability. Typing stays the same, only boost is lost
392
+ // Cannot be regained during multihit move and no Normal moves with stat drawbacks
393
+ hasAteAbilityTypeChange = hasAteAbilityTypeChange &&
394
+ attacker.hasAbility('Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate');
395
+
396
+ if ((move.dropsStats && move.timesUsed! > 1)) {
397
+ // Adaptability does not change between hits of a multihit, only between turns
398
+ stabMod = getStabMod(attacker, move, desc);
399
+ }
400
+
401
+ const newBasePower = calculateBasePowerBWXY(
402
+ gen,
403
+ attacker,
404
+ defender,
405
+ move,
406
+ field,
407
+ hasAteAbilityTypeChange,
408
+ desc
409
+ );
410
+ const newBaseDamage = getBaseDamage(attacker.level, newBasePower, newAtk, newDef);
411
+ const newFinalMods = calculateFinalModsBWXY(
412
+ gen,
413
+ attacker,
414
+ defender,
415
+ move,
416
+ field,
417
+ desc,
418
+ isCritical,
419
+ typeEffectiveness,
420
+ times
421
+ );
422
+ const newFinalMod = chainMods(newFinalMods, 41, 131072);
423
+
424
+ let damageMultiplier = 0;
425
+ damage = damage.map(affectedAmount => {
426
+ const newFinalDamage = getFinalDamage(
427
+ newBaseDamage,
428
+ damageMultiplier,
429
+ typeEffectiveness,
430
+ applyBurn,
431
+ stabMod,
432
+ newFinalMod
433
+ );
434
+ damageMultiplier++;
435
+ return affectedAmount + newFinalDamage;
436
+ });
437
+ }
438
+ desc.defenseBoost = origDefBoost;
439
+ desc.attackBoost = origAtkBoost;
440
+ }
441
+
442
+ result.damage = childDamage ? [damage, childDamage] : damage;
443
+
444
+ // #endregion
445
+
446
+ return result;
447
+ }
448
+
449
+ export function calculateBasePowerBWXY(
450
+ gen: Generation,
451
+ attacker: Pokemon,
452
+ defender: Pokemon,
453
+ move: Move,
454
+ field: Field,
455
+ hasAteAbilityTypeChange: boolean,
456
+ desc: RawDesc,
457
+ hit = 1,
458
+ ) {
459
+ let basePower: number;
460
+ const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last';
461
+
462
+ switch (move.name) {
463
+ case 'Payback':
464
+ basePower = move.bp * (turnOrder === 'last' ? 2 : 1);
465
+ desc.moveBP = basePower;
466
+ break;
467
+ case 'Pursuit':
468
+ const switching = field.defenderSide.isSwitching === 'out';
469
+ basePower = move.bp * (switching ? 2 : 1);
470
+ if (switching) desc.isSwitching = 'out';
471
+ desc.moveBP = basePower;
472
+ break;
473
+ case 'Electro Ball':
474
+ if (defender.stats.spe === 0) defender.stats.spe = 1;
475
+ const r = Math.floor(attacker.stats.spe / defender.stats.spe);
476
+ basePower = r >= 4 ? 150 : r >= 3 ? 120 : r >= 2 ? 80 : r >= 1 ? 60 : 40;
477
+ desc.moveBP = basePower;
478
+ break;
479
+ case 'Gyro Ball':
480
+ if (attacker.stats.spe === 0) attacker.stats.spe = 1;
481
+ basePower = Math.min(150, Math.floor((25 * defender.stats.spe) / attacker.stats.spe) + 1);
482
+ desc.moveBP = basePower;
483
+ break;
484
+ case 'Punishment':
485
+ basePower = Math.min(200, 60 + 20 * countBoosts(gen, defender.boosts));
486
+ desc.moveBP = basePower;
487
+ break;
488
+ case 'Low Kick':
489
+ case 'Grass Knot':
490
+ const w = getWeight(defender, desc, 'defender');
491
+ basePower = w >= 200 ? 120 : w >= 100 ? 100 : w >= 50 ? 80 : w >= 25 ? 60 : w >= 10 ? 40 : 20;
492
+ desc.moveBP = basePower;
493
+ break;
494
+ case 'Hex':
495
+ basePower = move.bp * (defender.status ? 2 : 1);
496
+ desc.moveBP = basePower;
497
+ break;
498
+ case 'Heavy Slam':
499
+ case 'Heat Crash':
500
+ const wr =
501
+ getWeight(attacker, desc, 'attacker') /
502
+ getWeight(defender, desc, 'defender');
503
+ basePower = wr >= 5 ? 120 : wr >= 4 ? 100 : wr >= 3 ? 80 : wr >= 2 ? 60 : 40;
504
+ desc.moveBP = basePower;
505
+ break;
506
+ case 'Stored Power':
507
+ case 'Power Trip':
508
+ basePower = 20 + 20 * countBoosts(gen, attacker.boosts);
509
+ desc.moveBP = basePower;
510
+ break;
511
+ case 'Acrobatics':
512
+ basePower = move.bp * (attacker.hasItem('Flying Gem') || !attacker.item ? 2 : 1);
513
+ desc.moveBP = basePower;
514
+ break;
515
+ case 'Assurance':
516
+ basePower = move.bp * (defender.hasAbility('Parental Bond (Child)') ? 2 : 1);
517
+ // NOTE: desc.attackerAbility = 'Parental Bond' will already reflect this boost
518
+ break;
519
+ case 'Wake-Up Slap':
520
+ basePower = move.bp * (defender.hasStatus('slp') ? 2 : 1);
521
+ desc.moveBP = basePower;
522
+ break;
523
+ case 'Smelling Salts':
524
+ basePower = move.bp * (defender.hasStatus('par') ? 2 : 1);
525
+ desc.moveBP = basePower;
526
+ break;
527
+ case 'Weather Ball':
528
+ basePower = move.bp * (field.weather && !field.hasWeather('Strong Winds') ? 2 : 1);
529
+ desc.moveBP = basePower;
530
+ break;
531
+ case 'Fling':
532
+ basePower = getFlingPower(attacker.item);
533
+ desc.moveBP = basePower;
534
+ desc.attackerItem = attacker.item;
535
+ break;
536
+ case 'Eruption':
537
+ case 'Water Spout':
538
+ basePower = Math.max(1, Math.floor((150 * attacker.curHP()) / attacker.maxHP()));
539
+ desc.moveBP = basePower;
540
+ break;
541
+ case 'Flail':
542
+ case 'Reversal':
543
+ const p = Math.floor((48 * attacker.curHP()) / attacker.maxHP());
544
+ basePower = p <= 1 ? 200 : p <= 4 ? 150 : p <= 9 ? 100 : p <= 16 ? 80 : p <= 32 ? 40 : 20;
545
+ desc.moveBP = basePower;
546
+ break;
547
+ case 'Nature Power':
548
+ if (gen.num === 5) {
549
+ move.category = 'Physical';
550
+ move.target = 'allAdjacent';
551
+ basePower = 100;
552
+ desc.moveName = 'Earthquake';
553
+ } else {
554
+ move.category = 'Special';
555
+ move.secondaries = true;
556
+ switch (field.terrain) {
557
+ case 'Electric':
558
+ basePower = 90;
559
+ desc.moveName = 'Thunderbolt';
560
+ break;
561
+ case 'Grassy':
562
+ basePower = 90;
563
+ desc.moveName = 'Energy Ball';
564
+ break;
565
+ case 'Misty':
566
+ basePower = 95;
567
+ desc.moveName = 'Moonblast';
568
+ break;
569
+ default:
570
+ basePower = 80;
571
+ desc.moveName = 'Tri Attack';
572
+ }
573
+ }
574
+ break;
575
+ // Triple Kick's damage increases after each consecutive hit (10, 20, 30)
576
+ case 'Triple Kick':
577
+ basePower = hit * 10;
578
+ desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10;
579
+ break;
580
+ case 'Crush Grip':
581
+ case 'Wring Out':
582
+ basePower = 100 * Math.floor((defender.curHP() * 4096) / defender.maxHP());
583
+ basePower = Math.floor(Math.floor((120 * basePower + 2048 - 1) / 4096) / 100) || 1;
584
+ desc.moveBP = basePower;
585
+ break;
586
+ default:
587
+ basePower = move.bp;
588
+ }
589
+
590
+ if (basePower === 0) {
591
+ return 0;
592
+ }
593
+
594
+ const bpMods = calculateBPModsBWXY(
595
+ gen,
596
+ attacker,
597
+ defender,
598
+ move,
599
+ field,
600
+ desc,
601
+ basePower,
602
+ hasAteAbilityTypeChange,
603
+ turnOrder
604
+ );
605
+
606
+ basePower = OF16(Math.max(1, pokeRound((basePower * chainMods(bpMods, 41, 2097152)) / 4096)));
607
+ return basePower;
608
+ }
609
+
610
+ export function calculateBPModsBWXY(
611
+ gen: Generation,
612
+ attacker: Pokemon,
613
+ defender: Pokemon,
614
+ move: Move,
615
+ field: Field,
616
+ desc: RawDesc,
617
+ basePower: number,
618
+ hasAteAbilityTypeChange: boolean,
619
+ turnOrder: string
620
+ ) {
621
+ const bpMods = [];
622
+
623
+ const defenderItem = (defender.item && defender.item !== '')
624
+ ? defender.item : defender.disabledItem;
625
+ let resistedKnockOffDamage =
626
+ !defenderItem ||
627
+ (defender.named('Giratina-Origin') && defenderItem === 'Griseous Orb') ||
628
+ (defender.name.includes('Arceus') && defenderItem.includes('Plate')) ||
629
+ (defender.name.includes('Genesect') && defenderItem.includes('Drive')) ||
630
+ (defender.named('Groudon', 'Groudon-Primal') && defenderItem === 'Red Orb') ||
631
+ (defender.named('Kyogre', 'Kyogre-Primal') && defenderItem === 'Blue Orb');
632
+
633
+ // The last case only applies when the Pokemon is holding the Mega Stone that matches its species
634
+ // (or when it's already a Mega-Evolution)
635
+ if (!resistedKnockOffDamage && defenderItem) {
636
+ const item = gen.items.get(toID(defenderItem))!;
637
+ resistedKnockOffDamage = !!(item.megaEvolves && defender.name.includes(item.megaEvolves));
638
+ }
639
+
640
+
641
+ // Use BasePower after moves with custom BP to determine if Technician should boost
642
+ if ((attacker.hasAbility('Technician') && basePower <= 60) ||
643
+ (attacker.hasAbility('Flare Boost') &&
644
+ attacker.hasStatus('brn') && move.category === 'Special') ||
645
+ (attacker.hasAbility('Toxic Boost') &&
646
+ attacker.hasStatus('psn', 'tox') && move.category === 'Physical')
647
+ ) {
648
+ bpMods.push(6144);
649
+ desc.attackerAbility = attacker.ability;
650
+ } else if (attacker.hasAbility('Analytic') && turnOrder !== 'first') {
651
+ bpMods.push(5325);
652
+ desc.attackerAbility = attacker.ability;
653
+ } else if (
654
+ attacker.hasAbility('Sand Force') &&
655
+ field.hasWeather('Sand') &&
656
+ move.hasType('Rock', 'Ground', 'Steel')
657
+ ) {
658
+ bpMods.push(5325);
659
+ desc.attackerAbility = attacker.ability;
660
+ desc.weather = field.weather;
661
+ } else if (
662
+ (attacker.hasAbility('Reckless') && (move.recoil || move.hasCrashDamage)) ||
663
+ (attacker.hasAbility('Iron Fist') && move.flags.punch)
664
+ ) {
665
+ bpMods.push(4915);
666
+ desc.attackerAbility = attacker.ability;
667
+ }
668
+
669
+ if (defender.hasAbility('Heatproof') && move.hasType('Fire')) {
670
+ bpMods.push(2048);
671
+ desc.defenderAbility = defender.ability;
672
+ } else if (defender.hasAbility('Dry Skin') && move.hasType('Fire')) {
673
+ bpMods.push(5120);
674
+ desc.defenderAbility = defender.ability;
675
+ }
676
+
677
+ if (attacker.hasAbility('Sheer Force') && move.secondaries) {
678
+ bpMods.push(5325);
679
+ desc.attackerAbility = attacker.ability;
680
+ }
681
+
682
+ if (attacker.hasAbility('Rivalry') && ![attacker.gender, defender.gender].includes('N')) {
683
+ if (attacker.gender === defender.gender) {
684
+ bpMods.push(5120);
685
+ desc.rivalry = 'buffed';
686
+ } else {
687
+ bpMods.push(3072);
688
+ desc.rivalry = 'nerfed';
689
+ }
690
+ desc.attackerAbility = attacker.ability;
691
+ }
692
+
693
+ if (attacker.item && getItemBoostType(attacker.item) === move.type) {
694
+ bpMods.push(4915);
695
+ desc.attackerItem = attacker.item;
696
+ } else if (
697
+ (attacker.hasItem('Muscle Band') && move.category === 'Physical') ||
698
+ (attacker.hasItem('Wise Glasses') && move.category === 'Special')
699
+ ) {
700
+ bpMods.push(4505);
701
+ desc.attackerItem = attacker.item;
702
+ } else if (
703
+ (attacker.hasItem('Adamant Orb') &&
704
+ attacker.named('Dialga') &&
705
+ move.hasType('Steel', 'Dragon')) ||
706
+ (attacker.hasItem('Lustrous Orb') &&
707
+ attacker.named('Palkia') &&
708
+ move.hasType('Water', 'Dragon')) ||
709
+ (attacker.hasItem('Griseous Orb') &&
710
+ attacker.named('Giratina-Origin') &&
711
+ move.hasType('Ghost', 'Dragon'))
712
+ ) {
713
+ bpMods.push(4915);
714
+ desc.attackerItem = attacker.item;
715
+ } else if (attacker.hasItem(`${move.type} Gem`)) {
716
+ bpMods.push(gen.num > 5 ? 5325 : 6144);
717
+ desc.attackerItem = attacker.item;
718
+ }
719
+
720
+ if ((move.named('Facade') && attacker.hasStatus('brn', 'par', 'psn', 'tox')) ||
721
+ (move.named('Brine') && defender.curHP() <= defender.maxHP() / 2) ||
722
+ (move.named('Venoshock') && defender.hasStatus('psn', 'tox'))) {
723
+ bpMods.push(8192);
724
+ desc.moveBP = basePower * 2;
725
+ } else if (gen.num > 5 && move.named('Knock Off') && !resistedKnockOffDamage) {
726
+ bpMods.push(6144);
727
+ desc.moveBP = basePower * 1.5;
728
+ } else if (move.named('Solar Beam') && field.hasWeather('Rain', 'Heavy Rain', 'Sand', 'Hail')) {
729
+ bpMods.push(2048);
730
+ desc.moveBP = basePower / 2;
731
+ desc.weather = field.weather;
732
+ }
733
+
734
+ if (field.attackerSide.isHelpingHand) {
735
+ bpMods.push(6144);
736
+ desc.isHelpingHand = true;
737
+ }
738
+
739
+ if (hasAteAbilityTypeChange) {
740
+ bpMods.push(5325);
741
+ desc.attackerAbility = attacker.ability;
742
+ } else if (
743
+ (attacker.hasAbility('Mega Launcher') && move.flags.pulse) ||
744
+ (attacker.hasAbility('Strong Jaw') && move.flags.bite)
745
+ ) {
746
+ bpMods.push(6144);
747
+ desc.attackerAbility = attacker.ability;
748
+ } else if (attacker.hasAbility('Tough Claws') && move.flags.contact) {
749
+ bpMods.push(5325);
750
+ desc.attackerAbility = attacker.ability;
751
+ }
752
+
753
+ const aura = `${move.type} Aura`;
754
+ const isAttackerAura = attacker.hasAbility(aura);
755
+ const isDefenderAura = defender.hasAbility(aura);
756
+ const isUserAuraBreak = attacker.hasAbility('Aura Break') || defender.hasAbility('Aura Break');
757
+ const isFieldAuraBreak = field.isAuraBreak;
758
+ const isFieldFairyAura = field.isFairyAura && move.type === 'Fairy';
759
+ const isFieldDarkAura = field.isDarkAura && move.type === 'Dark';
760
+ const auraActive = isAttackerAura || isDefenderAura || isFieldFairyAura || isFieldDarkAura;
761
+ const auraBreak = isFieldAuraBreak || isUserAuraBreak;
762
+ if (auraActive) {
763
+ if (auraBreak) {
764
+ bpMods.push(3072);
765
+ desc.attackerAbility = attacker.ability;
766
+ desc.defenderAbility = defender.ability;
767
+ } else {
768
+ bpMods.push(5448);
769
+ if (isAttackerAura) desc.attackerAbility = attacker.ability;
770
+ if (isDefenderAura) desc.defenderAbility = defender.ability;
771
+ }
772
+ }
773
+
774
+ // It's not actually clear if the terrain modifiers are base damage mods like weather or are
775
+ // base power mods like in Gen 7+, but the research doesn't exist for this yet so we match PS here
776
+ if (isGrounded(attacker, field)) {
777
+ if ((field.hasTerrain('Electric') && move.hasType('Electric')) ||
778
+ (field.hasTerrain('Grassy') && move.hasType('Grass'))
779
+ ) {
780
+ bpMods.push(6144);
781
+ desc.terrain = field.terrain;
782
+ }
783
+ }
784
+ if (isGrounded(defender, field)) {
785
+ if ((field.hasTerrain('Misty') && move.hasType('Dragon')) ||
786
+ (field.hasTerrain('Grassy') && move.named('Bulldoze', 'Earthquake'))
787
+ ) {
788
+ bpMods.push(2048);
789
+ desc.terrain = field.terrain;
790
+ }
791
+ }
792
+
793
+ return bpMods;
794
+ }
795
+
796
+ export function calculateAttackBWXY(
797
+ gen: Generation,
798
+ attacker: Pokemon,
799
+ defender: Pokemon,
800
+ move: Move,
801
+ field: Field,
802
+ desc: RawDesc,
803
+ isCritical = false
804
+ ) {
805
+ let attack: number;
806
+ const attackSource = move.named('Foul Play') ? defender : attacker;
807
+ const attackStat = move.category === 'Special' ? 'spa' : 'atk';
808
+ desc.attackEVs =
809
+ move.named('Foul Play')
810
+ ? getStatDescriptionText(gen, defender, attackStat, defender.nature)
811
+ : getStatDescriptionText(gen, attacker, attackStat, attacker.nature);
812
+
813
+ if (attackSource.boosts[attackStat] === 0 ||
814
+ (isCritical && attackSource.boosts[attackStat] < 0)) {
815
+ attack = attackSource.rawStats[attackStat];
816
+ } else if (defender.hasAbility('Unaware')) {
817
+ attack = attackSource.rawStats[attackStat];
818
+ desc.defenderAbility = defender.ability;
819
+ } else {
820
+ attack = getModifiedStat(attackSource.rawStats[attackStat]!, attackSource.boosts[attackStat]!);
821
+ desc.attackBoost = attackSource.boosts[attackStat];
822
+ }
823
+
824
+ // unlike all other attack modifiers, Hustle gets applied directly
825
+ if (attacker.hasAbility('Hustle') && move.category === 'Physical') {
826
+ attack = pokeRound((attack * 3) / 2);
827
+ desc.attackerAbility = attacker.ability;
828
+ }
829
+
830
+ const atMods = calculateAtModsBWXY(attacker, defender, move, field, desc);
831
+ attack = OF16(Math.max(1, pokeRound((attack * chainMods(atMods, 410, 131072)) / 4096)));
832
+ return attack;
833
+ }
834
+
835
+ export function calculateAtModsBWXY(
836
+ attacker: Pokemon,
837
+ defender: Pokemon,
838
+ move: Move,
839
+ field: Field,
840
+ desc: RawDesc
841
+ ) {
842
+ const atMods = [];
843
+ if (defender.hasAbility('Thick Fat') && move.hasType('Fire', 'Ice')) {
844
+ atMods.push(2048);
845
+ desc.defenderAbility = defender.ability;
846
+ }
847
+
848
+ if ((attacker.hasAbility('Guts') && attacker.status && move.category === 'Physical') ||
849
+ (attacker.curHP() <= attacker.maxHP() / 3 &&
850
+ ((attacker.hasAbility('Overgrow') && move.hasType('Grass')) ||
851
+ (attacker.hasAbility('Blaze') && move.hasType('Fire')) ||
852
+ (attacker.hasAbility('Torrent') && move.hasType('Water')) ||
853
+ (attacker.hasAbility('Swarm') && move.hasType('Bug')))) ||
854
+ (move.category === 'Special' && attacker.abilityOn && attacker.hasAbility('Plus', 'Minus'))
855
+ ) {
856
+ atMods.push(6144);
857
+ desc.attackerAbility = attacker.ability;
858
+ } else if (attacker.hasAbility('Flash Fire') && attacker.abilityOn && move.hasType('Fire')) {
859
+ atMods.push(6144);
860
+ desc.attackerAbility = 'Flash Fire';
861
+ } else if (
862
+ (attacker.hasAbility('Solar Power') &&
863
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
864
+ move.category === 'Special') ||
865
+ (attacker.named('Cherrim') &&
866
+ attacker.hasAbility('Flower Gift') &&
867
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
868
+ move.category === 'Physical')
869
+ ) {
870
+ atMods.push(6144);
871
+ desc.attackerAbility = attacker.ability;
872
+ desc.weather = field.weather;
873
+ } else if (
874
+ (attacker.hasAbility('Defeatist') && attacker.curHP() <= attacker.maxHP() / 2) ||
875
+ (attacker.hasAbility('Slow Start') && attacker.abilityOn && move.category === 'Physical')
876
+ ) {
877
+ atMods.push(2048);
878
+ desc.attackerAbility = attacker.ability;
879
+ } else if (attacker.hasAbility('Huge Power', 'Pure Power') && move.category === 'Physical') {
880
+ atMods.push(8192);
881
+ desc.attackerAbility = attacker.ability;
882
+ }
883
+
884
+ if (
885
+ field.attackerSide.isFlowerGift &&
886
+ !attacker.hasAbility('Flower Gift') &&
887
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
888
+ move.category === 'Physical') {
889
+ atMods.push(6144);
890
+ desc.weather = field.weather;
891
+ desc.isFlowerGiftAttacker = true;
892
+ }
893
+
894
+ if ((attacker.hasItem('Thick Club') &&
895
+ attacker.named('Cubone', 'Marowak', 'Marowak-Alola') &&
896
+ move.category === 'Physical') ||
897
+ (attacker.hasItem('Deep Sea Tooth') &&
898
+ attacker.named('Clamperl') &&
899
+ move.category === 'Special') ||
900
+ (attacker.hasItem('Light Ball') && attacker.name.startsWith('Pikachu') && !move.isZ)
901
+ ) {
902
+ atMods.push(8192);
903
+ desc.attackerItem = attacker.item;
904
+ } else if (
905
+ (attacker.hasItem('Soul Dew') &&
906
+ attacker.named('Latios', 'Latias', 'Latios-Mega', 'Latias-Mega') &&
907
+ move.category === 'Special') ||
908
+ (attacker.hasItem('Choice Band') && move.category === 'Physical') ||
909
+ (attacker.hasItem('Choice Specs') && move.category === 'Special')
910
+ ) {
911
+ atMods.push(6144);
912
+ desc.attackerItem = attacker.item;
913
+ }
914
+ return atMods;
915
+ }
916
+
917
+ export function calculateDefenseBWXY(
918
+ gen: Generation,
919
+ attacker: Pokemon,
920
+ defender: Pokemon,
921
+ move: Move,
922
+ field: Field,
923
+ desc: RawDesc,
924
+ isCritical = false
925
+ ) {
926
+ let defense: number;
927
+ const defenseStat = move.overrideDefensiveStat || move.category === 'Physical' ? 'def' : 'spd';
928
+ const hitsPhysical = defenseStat === 'def';
929
+ desc.defenseEVs = getStatDescriptionText(gen, defender, defenseStat, defender.nature);
930
+ if (defender.boosts[defenseStat] === 0 ||
931
+ (isCritical && defender.boosts[defenseStat] > 0) ||
932
+ move.ignoreDefensive) {
933
+ defense = defender.rawStats[defenseStat];
934
+ } else if (attacker.hasAbility('Unaware')) {
935
+ defense = defender.rawStats[defenseStat];
936
+ desc.attackerAbility = attacker.ability;
937
+ } else {
938
+ defense = getModifiedStat(defender.rawStats[defenseStat]!, defender.boosts[defenseStat]!);
939
+ desc.defenseBoost = defender.boosts[defenseStat];
940
+ }
941
+
942
+ // unlike all other defense modifiers, Sandstorm SpD boost gets applied directly
943
+ if (field.hasWeather('Sand') && defender.hasType('Rock') && !hitsPhysical) {
944
+ defense = pokeRound((defense * 3) / 2);
945
+ desc.weather = field.weather;
946
+ }
947
+
948
+ const dfMods = calculateDfModsBWXY(
949
+ gen,
950
+ defender,
951
+ field,
952
+ desc,
953
+ hitsPhysical
954
+ );
955
+ defense = OF16(Math.max(1, pokeRound((defense * chainMods(dfMods, 410, 131072)) / 4096)));
956
+ return defense;
957
+ }
958
+
959
+ export function calculateDfModsBWXY(
960
+ gen: Generation,
961
+ defender: Pokemon,
962
+ field: Field,
963
+ desc: RawDesc,
964
+ hitsPhysical = false
965
+ ) {
966
+ const dfMods = [];
967
+ if (defender.hasAbility('Marvel Scale') && defender.status && hitsPhysical) {
968
+ dfMods.push(6144);
969
+ desc.defenderAbility = defender.ability;
970
+ } else if (
971
+ defender.named('Cherrim') &&
972
+ defender.hasAbility('Flower Gift') &&
973
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
974
+ !hitsPhysical
975
+ ) {
976
+ dfMods.push(6144);
977
+ desc.defenderAbility = defender.ability;
978
+ desc.weather = field.weather;
979
+ } else if (
980
+ field.defenderSide.isFlowerGift &&
981
+ field.hasWeather('Sun', 'Harsh Sunshine') &&
982
+ !hitsPhysical) {
983
+ dfMods.push(6144);
984
+ desc.weather = field.weather;
985
+ desc.isFlowerGiftDefender = true;
986
+ }
987
+
988
+ if (field.hasTerrain('Grassy') && defender.hasAbility('Grass Pelt') && hitsPhysical) {
989
+ dfMods.push(6144);
990
+ desc.defenderAbility = defender.ability;
991
+ }
992
+
993
+ if ((!hitsPhysical && defender.hasItem('Soul Dew') &&
994
+ defender.named('Latios', 'Latias', 'Latios-Mega', 'Latias-Mega')) ||
995
+ (defender.hasItem('Eviolite') && gen.species.get(toID(defender.name))?.nfe) ||
996
+ (!hitsPhysical && defender.hasItem('Assault Vest'))) {
997
+ dfMods.push(6144);
998
+ desc.defenderItem = defender.item;
999
+ }
1000
+
1001
+ if ((defender.hasItem('Metal Powder') && defender.named('Ditto') && hitsPhysical) ||
1002
+ (defender.hasItem('Deep Sea Scale') && defender.named('Clamperl') && !hitsPhysical)) {
1003
+ dfMods.push(8192);
1004
+ desc.defenderItem = defender.item;
1005
+ }
1006
+
1007
+ if (defender.hasAbility('Fur Coat') && hitsPhysical) {
1008
+ dfMods.push(8192);
1009
+ desc.defenderAbility = defender.ability;
1010
+ }
1011
+ return dfMods;
1012
+ }
1013
+
1014
+
1015
+ function calculateBaseDamageBWXY(
1016
+ gen: Generation,
1017
+ attacker: Pokemon,
1018
+ basePower: number,
1019
+ attack: number,
1020
+ defense: number,
1021
+ move: Move,
1022
+ field: Field,
1023
+ desc: RawDesc,
1024
+ isCritical = false,
1025
+ ) {
1026
+ let baseDamage = getBaseDamage(attacker.level, basePower, attack, defense);
1027
+
1028
+ const isSpread = field.gameType !== 'Singles' &&
1029
+ ['allAdjacent', 'allAdjacentFoes'].includes(move.target);
1030
+ if (isSpread) {
1031
+ baseDamage = pokeRound(OF32(baseDamage * 3072) / 4096);
1032
+ }
1033
+
1034
+ if (attacker.hasAbility('Parental Bond (Child)')) {
1035
+ baseDamage = pokeRound(OF32(baseDamage * 2048) / 4096);
1036
+ }
1037
+
1038
+ if ((field.hasWeather('Sun', 'Harsh Sunshine') && move.hasType('Fire')) ||
1039
+ (field.hasWeather('Rain', 'Heavy Rain') && move.hasType('Water'))) {
1040
+ baseDamage = pokeRound(OF32(baseDamage * 6144) / 4096);
1041
+ desc.weather = field.weather;
1042
+ } else if (
1043
+ (field.hasWeather('Sun') && move.hasType('Water')) ||
1044
+ (field.hasWeather('Rain') && move.hasType('Fire'))
1045
+ ) {
1046
+ baseDamage = pokeRound(OF32(baseDamage * 2048) / 4096);
1047
+ desc.weather = field.weather;
1048
+ }
1049
+
1050
+ if (isCritical) {
1051
+ baseDamage = Math.floor(OF32(baseDamage * (gen.num > 5 ? 1.5 : 2)));
1052
+ desc.isCritical = isCritical;
1053
+ }
1054
+
1055
+ return baseDamage;
1056
+ }
1057
+
1058
+ function calculateFinalModsBWXY(
1059
+ gen: Generation,
1060
+ attacker: Pokemon,
1061
+ defender: Pokemon,
1062
+ move: Move,
1063
+ field: Field,
1064
+ desc: RawDesc,
1065
+ isCritical = false,
1066
+ typeEffectiveness: number,
1067
+ hitCount = 0
1068
+ ) {
1069
+ const finalMods = [];
1070
+
1071
+ if (field.defenderSide.isReflect && move.category === 'Physical' && !isCritical) {
1072
+ finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
1073
+ desc.isReflect = true;
1074
+ } else if (field.defenderSide.isLightScreen && move.category === 'Special' && !isCritical) {
1075
+ finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
1076
+ desc.isLightScreen = true;
1077
+ }
1078
+
1079
+ if (defender.hasAbility('Multiscale') && defender.curHP() === defender.maxHP() &&
1080
+ hitCount === 0 &&
1081
+ !field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying')) &&
1082
+ !attacker.hasAbility('Parental Bond (Child)')) {
1083
+ finalMods.push(2048);
1084
+ desc.defenderAbility = defender.ability;
1085
+ }
1086
+
1087
+ if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) {
1088
+ finalMods.push(8192);
1089
+ desc.attackerAbility = attacker.ability;
1090
+ }
1091
+
1092
+ if (field.defenderSide.isFriendGuard) {
1093
+ finalMods.push(3072);
1094
+ desc.isFriendGuard = true;
1095
+ }
1096
+
1097
+ if (attacker.hasAbility('Sniper') && isCritical) {
1098
+ finalMods.push(6144);
1099
+ desc.attackerAbility = attacker.ability;
1100
+ }
1101
+
1102
+ if (defender.hasAbility('Solid Rock', 'Filter') && typeEffectiveness > 1) {
1103
+ finalMods.push(3072);
1104
+ desc.defenderAbility = defender.ability;
1105
+ }
1106
+
1107
+ if (attacker.hasItem('Metronome') && move.timesUsedWithMetronome! >= 1) {
1108
+ const timesUsedWithMetronome = Math.floor(move.timesUsedWithMetronome!);
1109
+ if (timesUsedWithMetronome <= 4) {
1110
+ finalMods.push(4096 + timesUsedWithMetronome * 819);
1111
+ } else {
1112
+ finalMods.push(8192);
1113
+ }
1114
+ desc.attackerItem = attacker.item;
1115
+ }
1116
+
1117
+ if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1 && !move.isZ) {
1118
+ finalMods.push(4915);
1119
+ desc.attackerItem = attacker.item;
1120
+ } else if (attacker.hasItem('Life Orb')) {
1121
+ finalMods.push(5324);
1122
+ desc.attackerItem = attacker.item;
1123
+ }
1124
+
1125
+ if (move.hasType(getBerryResistType(defender.item)) &&
1126
+ (typeEffectiveness > 1 || move.hasType('Normal')) &&
1127
+ hitCount === 0 &&
1128
+ !attacker.hasAbility('Unnerve')) {
1129
+ finalMods.push(2048);
1130
+ desc.defenderItem = defender.item;
1131
+ }
1132
+
1133
+ return finalMods;
1134
+ }