@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,676 @@
1
+ import type {
2
+ Generation,
3
+ ID,
4
+ ItemName,
5
+ MoveCategory,
6
+ NatureName,
7
+ StatID,
8
+ StatsTable,
9
+ Terrain,
10
+ TypeName,
11
+ Weather,
12
+ } from '../data/interface';
13
+ import {toID} from '../util';
14
+ import type {Field, Side} from '../field';
15
+ import type {Move} from '../move';
16
+ import type {Pokemon} from '../pokemon';
17
+ import {Stats} from '../stats';
18
+ import type {RawDesc} from '../desc';
19
+
20
+ const EV_ITEMS = [
21
+ 'Macho Brace',
22
+ 'Power Anklet',
23
+ 'Power Band',
24
+ 'Power Belt',
25
+ 'Power Bracer',
26
+ 'Power Lens',
27
+ 'Power Weight',
28
+ ];
29
+
30
+ export function isGrounded(pokemon: Pokemon, field: Field) {
31
+ return (field.isGravity || pokemon.hasItem('Iron Ball') ||
32
+ (!pokemon.hasType('Flying') &&
33
+ !pokemon.hasAbility('Levitate') &&
34
+ !pokemon.hasItem('Air Balloon')));
35
+ }
36
+
37
+ export function getModifiedStat(stat: number, mod: number, gen?: Generation) {
38
+ if (gen && gen.num < 3) {
39
+ if (mod >= 0) {
40
+ const pastGenBoostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
41
+ stat = Math.floor(stat * pastGenBoostTable[mod]);
42
+ } else {
43
+ const numerators = [100, 66, 50, 40, 33, 28, 25];
44
+ stat = Math.floor((stat * numerators[-mod]) / 100);
45
+ }
46
+ return Math.min(999, Math.max(1, stat));
47
+ }
48
+
49
+ const numerator = 0;
50
+ const denominator = 1;
51
+ const modernGenBoostTable = [
52
+ [2, 8],
53
+ [2, 7],
54
+ [2, 6],
55
+ [2, 5],
56
+ [2, 4],
57
+ [2, 3],
58
+ [2, 2],
59
+ [3, 2],
60
+ [4, 2],
61
+ [5, 2],
62
+ [6, 2],
63
+ [7, 2],
64
+ [8, 2],
65
+ ];
66
+ stat = OF16(stat * modernGenBoostTable[6 + mod][numerator]);
67
+ stat = Math.floor(stat / modernGenBoostTable[6 + mod][denominator]);
68
+
69
+ return stat;
70
+ }
71
+
72
+ export function computeFinalStats(
73
+ gen: Generation,
74
+ attacker: Pokemon,
75
+ defender: Pokemon,
76
+ field: Field,
77
+ ...stats: StatID[]
78
+ ) {
79
+ const sides: Array<[Pokemon, Side]> =
80
+ [[attacker, field.attackerSide], [defender, field.defenderSide]];
81
+ for (const [pokemon, side] of sides) {
82
+ for (const stat of stats) {
83
+ if (stat === 'spe') {
84
+ pokemon.stats.spe = getFinalSpeed(gen, pokemon, field, side);
85
+ } else {
86
+ pokemon.stats[stat] = getModifiedStat(pokemon.rawStats[stat]!, pokemon.boosts[stat]!, gen);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ export function getFinalSpeed(gen: Generation, pokemon: Pokemon, field: Field, side: Side) {
93
+ const weather = field.weather || '';
94
+ const terrain = field.terrain;
95
+ let speed = getModifiedStat(pokemon.rawStats.spe, pokemon.boosts.spe, gen);
96
+ const speedMods = [];
97
+
98
+ if (side.isTailwind) speedMods.push(8192);
99
+ // Pledge swamp would get applied here when implemented
100
+ // speedMods.push(1024);
101
+
102
+ if ((pokemon.hasAbility('Unburden') && pokemon.abilityOn) ||
103
+ (pokemon.hasAbility('Chlorophyll') && weather.includes('Sun')) ||
104
+ (pokemon.hasAbility('Sand Rush') && weather === 'Sand') ||
105
+ (pokemon.hasAbility('Swift Swim') && weather.includes('Rain')) ||
106
+ (pokemon.hasAbility('Slush Rush') && ['Hail', 'Snow'].includes(weather)) ||
107
+ (pokemon.hasAbility('Surge Surfer') && terrain === 'Electric')
108
+ ) {
109
+ speedMods.push(8192);
110
+ } else if (pokemon.hasAbility('Quick Feet') && pokemon.status) {
111
+ speedMods.push(6144);
112
+ } else if (pokemon.hasAbility('Slow Start') && pokemon.abilityOn) {
113
+ speedMods.push(2048);
114
+ } else if (isQPActive(pokemon, field) && getQPBoostedStat(pokemon, gen) === 'spe') {
115
+ speedMods.push(6144);
116
+ }
117
+
118
+ if (pokemon.hasItem('Choice Scarf')) {
119
+ speedMods.push(6144);
120
+ } else if (pokemon.hasItem('Iron Ball', ...EV_ITEMS)) {
121
+ speedMods.push(2048);
122
+ } else if (pokemon.hasItem('Quick Powder') && pokemon.named('Ditto')) {
123
+ speedMods.push(8192);
124
+ }
125
+
126
+ speed = OF32(pokeRound((speed * chainMods(speedMods, 410, 131172)) / 4096));
127
+ if (pokemon.hasStatus('par') && !pokemon.hasAbility('Quick Feet')) {
128
+ speed = Math.floor(OF32(speed * (gen.num < 7 ? 25 : 50)) / 100);
129
+ }
130
+
131
+ speed = Math.min(gen.num <= 2 ? 999 : 10000, speed);
132
+ return Math.max(0, speed);
133
+ }
134
+
135
+ export function getMoveEffectiveness(
136
+ gen: Generation,
137
+ move: Move,
138
+ type: TypeName,
139
+ isGhostRevealed?: boolean,
140
+ isGravity?: boolean,
141
+ isRingTarget?: boolean,
142
+ ) {
143
+ if (isGhostRevealed && type === 'Ghost' && move.hasType('Normal', 'Fighting')) {
144
+ return 1;
145
+ } else if (isGravity && type === 'Flying' && move.hasType('Ground')) {
146
+ return 1;
147
+ } else if (move.named('Freeze-Dry') && type === 'Water') {
148
+ return 2;
149
+ } else {
150
+ let effectiveness = gen.types.get(toID(move.type))!.effectiveness[type]!;
151
+ if (effectiveness === 0 && isRingTarget) {
152
+ effectiveness = 1;
153
+ }
154
+ if (move.named('Flying Press')) {
155
+ // Can only do this because flying has no other interactions
156
+ effectiveness *= gen.types.get('flying' as ID)!.effectiveness[type]!;
157
+ }
158
+ return effectiveness;
159
+ }
160
+ }
161
+
162
+ export function checkAirLock(pokemon: Pokemon, field: Field) {
163
+ if (pokemon.hasAbility('Air Lock', 'Cloud Nine')) {
164
+ field.weather = undefined;
165
+ }
166
+ }
167
+
168
+ export function checkTeraformZero(pokemon: Pokemon, field: Field) {
169
+ if (pokemon.hasAbility('Teraform Zero') && pokemon.abilityOn) {
170
+ field.weather = undefined;
171
+ field.terrain = undefined;
172
+ }
173
+ }
174
+
175
+ export function checkForecast(pokemon: Pokemon, weather?: Weather) {
176
+ if (pokemon.hasAbility('Forecast') && pokemon.named('Castform')) {
177
+ switch (weather) {
178
+ case 'Sun':
179
+ case 'Harsh Sunshine':
180
+ pokemon.types = ['Fire'];
181
+ break;
182
+ case 'Rain':
183
+ case 'Heavy Rain':
184
+ pokemon.types = ['Water'];
185
+ break;
186
+ case 'Hail':
187
+ case 'Snow':
188
+ pokemon.types = ['Ice'];
189
+ break;
190
+ default:
191
+ pokemon.types = ['Normal'];
192
+ }
193
+ }
194
+ }
195
+
196
+ export function checkItem(pokemon: Pokemon, magicRoomActive?: boolean) {
197
+ // Pokemon with Klutz still get their speed dropped in generation 4
198
+ if (pokemon.gen.num === 4 && pokemon.hasItem('Iron Ball')) return;
199
+ if (
200
+ pokemon.hasAbility('Klutz') && !EV_ITEMS.includes(pokemon.item!) ||
201
+ magicRoomActive
202
+ ) {
203
+ pokemon.disabledItem = pokemon.item;
204
+ pokemon.item = '' as ItemName;
205
+ }
206
+ }
207
+
208
+ export function checkWonderRoom(pokemon: Pokemon, wonderRoomActive?: boolean) {
209
+ if (wonderRoomActive) {
210
+ [pokemon.rawStats.def, pokemon.rawStats.spd] = [pokemon.rawStats.spd, pokemon.rawStats.def];
211
+ }
212
+ }
213
+
214
+ export function checkIntimidate(gen: Generation, source: Pokemon, target: Pokemon) {
215
+ const blocked =
216
+ target.hasAbility('Clear Body', 'White Smoke', 'Hyper Cutter', 'Full Metal Body') ||
217
+ // More abilities now block Intimidate in Gen 8+ (DaWoblefet, Cloudy Mistral)
218
+ (gen.num >= 8 && target.hasAbility('Inner Focus', 'Own Tempo', 'Oblivious', 'Scrappy')) ||
219
+ target.hasItem('Clear Amulet');
220
+ if (source.hasAbility('Intimidate') && source.abilityOn && !blocked) {
221
+ if (target.hasAbility('Contrary', 'Defiant', 'Guard Dog')) {
222
+ target.boosts.atk = Math.min(6, target.boosts.atk + 1);
223
+ } else if (target.hasAbility('Simple')) {
224
+ target.boosts.atk = Math.max(-6, target.boosts.atk - 2);
225
+ } else {
226
+ target.boosts.atk = Math.max(-6, target.boosts.atk - 1);
227
+ }
228
+ if (target.hasAbility('Competitive')) {
229
+ target.boosts.spa = Math.min(6, target.boosts.spa + 2);
230
+ }
231
+ }
232
+ }
233
+
234
+ export function checkDownload(source: Pokemon, target: Pokemon, wonderRoomActive?: boolean) {
235
+ if (source.hasAbility('Download')) {
236
+ let def = target.stats.def;
237
+ let spd = target.stats.spd;
238
+ // We swap the defense stats again here since Download ignores Wonder Room
239
+ if (wonderRoomActive) [def, spd] = [spd, def];
240
+ if (spd <= def) {
241
+ source.boosts.spa = Math.min(6, source.boosts.spa + 1);
242
+ } else {
243
+ source.boosts.atk = Math.min(6, source.boosts.atk + 1);
244
+ }
245
+ }
246
+ }
247
+
248
+ export function checkIntrepidSword(source: Pokemon, gen: Generation) {
249
+ if (source.hasAbility('Intrepid Sword') && gen.num > 7) {
250
+ source.boosts.atk = Math.min(6, source.boosts.atk + 1);
251
+ }
252
+ }
253
+
254
+ export function checkDauntlessShield(source: Pokemon, gen: Generation) {
255
+ if (source.hasAbility('Dauntless Shield') && gen.num > 7) {
256
+ source.boosts.def = Math.min(6, source.boosts.def + 1);
257
+ }
258
+ }
259
+
260
+ export function checkWindRider(source: Pokemon, attackingSide: Side) {
261
+ if (source.hasAbility('Wind Rider') && attackingSide.isTailwind) {
262
+ source.boosts.atk = Math.min(6, source.boosts.atk + 1);
263
+ }
264
+ }
265
+
266
+ export function checkEmbody(source: Pokemon, gen: Generation) {
267
+ if (gen.num < 9) return;
268
+ switch (source.ability) {
269
+ case 'Embody Aspect (Cornerstone)':
270
+ source.boosts.def = Math.min(6, source.boosts.def + 1);
271
+ break;
272
+ case 'Embody Aspect (Hearthflame)':
273
+ source.boosts.atk = Math.min(6, source.boosts.atk + 1);
274
+ break;
275
+ case 'Embody Aspect (Teal)':
276
+ source.boosts.spe = Math.min(6, source.boosts.spe + 1);
277
+ break;
278
+ case 'Embody Aspect (Wellspring)':
279
+ source.boosts.spd = Math.min(6, source.boosts.spd + 1);
280
+ break;
281
+ }
282
+ }
283
+
284
+ export function checkInfiltrator(pokemon: Pokemon, affectedSide: Side) {
285
+ if (pokemon.hasAbility('Infiltrator')) {
286
+ affectedSide.isReflect = false;
287
+ affectedSide.isLightScreen = false;
288
+ affectedSide.isAuroraVeil = false;
289
+ }
290
+ }
291
+
292
+ export function checkSeedBoost(pokemon: Pokemon, field: Field) {
293
+ if (!pokemon.item) return;
294
+ if (field.terrain && pokemon.item.includes('Seed')) {
295
+ const terrainSeed = pokemon.item.substring(0, pokemon.item.indexOf(' ')) as Terrain;
296
+ if (field.hasTerrain(terrainSeed)) {
297
+ if (terrainSeed === 'Grassy' || terrainSeed === 'Electric') {
298
+ pokemon.boosts.def = pokemon.hasAbility('Contrary')
299
+ ? Math.max(-6, pokemon.boosts.def - 1)
300
+ : Math.min(6, pokemon.boosts.def + 1);
301
+ } else {
302
+ pokemon.boosts.spd = pokemon.hasAbility('Contrary')
303
+ ? Math.max(-6, pokemon.boosts.spd - 1)
304
+ : Math.min(6, pokemon.boosts.spd + 1);
305
+ }
306
+ pokemon.item = '' as ItemName;
307
+ }
308
+ }
309
+ }
310
+
311
+ // NOTE: We only need to handle guaranteed, damage-relevant boosts here for multi-hit accuracy
312
+ export function checkMultihitBoost(
313
+ gen: Generation,
314
+ attacker: Pokemon,
315
+ defender: Pokemon,
316
+ move: Move,
317
+ field: Field,
318
+ desc: RawDesc,
319
+ attackerUsedItem = false,
320
+ defenderUsedItem = false
321
+ ) {
322
+ // NOTE: attacker.ability must be Parental Bond for these moves to be multi-hit
323
+ if (move.named('Gyro Ball', 'Electro Ball') && defender.hasAbility('Gooey', 'Tangling Hair')) {
324
+ // Gyro Ball (etc) makes contact into Gooey (etc) whenever its inflicting multiple hits because
325
+ // this can only happen if the attacker ability is Parental Bond (and thus can't be Long Reach)
326
+ if (attacker.hasItem('White Herb') && !attackerUsedItem) {
327
+ desc.attackerItem = attacker.item;
328
+ attackerUsedItem = true;
329
+ } else {
330
+ attacker.boosts.spe = Math.max(attacker.boosts.spe - 1, -6);
331
+ attacker.stats.spe = getFinalSpeed(gen, attacker, field, field.attackerSide);
332
+ desc.defenderAbility = defender.ability;
333
+ }
334
+ // BUG: Technically Sitrus/Figy Berry + Unburden can also affect the defender's speed, but
335
+ // this goes far beyond what we care to implement (especially once Gluttony is considered) now
336
+ } else if (move.named('Power-Up Punch')) {
337
+ attacker.boosts.atk = Math.min(attacker.boosts.atk + 1, 6);
338
+ attacker.stats.atk = getModifiedStat(attacker.rawStats.atk, attacker.boosts.atk, gen);
339
+ }
340
+
341
+ const atkSimple = attacker.hasAbility('Simple') ? 2 : 1;
342
+ const defSimple = defender.hasAbility('Simple') ? 2 : 1;
343
+
344
+ if ((!defenderUsedItem) &&
345
+ (defender.hasItem('Luminous Moss') && move.hasType('Water')) ||
346
+ (defender.hasItem('Maranga Berry') && move.category === 'Special') ||
347
+ (defender.hasItem('Kee Berry') && move.category === 'Physical')) {
348
+ const defStat = defender.hasItem('Kee Berry') ? 'def' : 'spd';
349
+ if (attacker.hasAbility('Unaware')) {
350
+ desc.attackerAbility = attacker.ability;
351
+ } else {
352
+ if (defender.hasAbility('Contrary')) {
353
+ desc.defenderAbility = defender.ability;
354
+ if (defender.hasItem('White Herb') && !defenderUsedItem) {
355
+ desc.defenderItem = defender.item;
356
+ defenderUsedItem = true;
357
+ } else {
358
+ defender.boosts[defStat] = Math.max(-6, defender.boosts[defStat] - defSimple);
359
+ }
360
+ } else {
361
+ defender.boosts[defStat] = Math.min(6, defender.boosts[defStat] + defSimple);
362
+ }
363
+ if (defSimple === 2) desc.defenderAbility = defender.ability;
364
+ defender.stats[defStat] = getModifiedStat(defender.rawStats[defStat],
365
+ defender.boosts[defStat],
366
+ gen);
367
+ desc.defenderItem = defender.item;
368
+ defenderUsedItem = true;
369
+ }
370
+ }
371
+
372
+ if (defender.hasAbility('Seed Sower')) {
373
+ field.terrain = 'Grassy';
374
+ }
375
+ if (defender.hasAbility('Sand Spit')) {
376
+ field.weather = 'Sand';
377
+ }
378
+
379
+ if (defender.hasAbility('Stamina')) {
380
+ if (attacker.hasAbility('Unaware')) {
381
+ desc.attackerAbility = attacker.ability;
382
+ } else {
383
+ defender.boosts.def = Math.min(defender.boosts.def + 1, 6);
384
+ defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen);
385
+ desc.defenderAbility = defender.ability;
386
+ }
387
+ } else if (defender.hasAbility('Water Compaction') && move.hasType('Water')) {
388
+ if (attacker.hasAbility('Unaware')) {
389
+ desc.attackerAbility = attacker.ability;
390
+ } else {
391
+ defender.boosts.def = Math.min(defender.boosts.def + 2, 6);
392
+ defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen);
393
+ desc.defenderAbility = defender.ability;
394
+ }
395
+ } else if (defender.hasAbility('Weak Armor')) {
396
+ if (attacker.hasAbility('Unaware')) {
397
+ desc.attackerAbility = attacker.ability;
398
+ } else {
399
+ if (defender.hasItem('White Herb') && !defenderUsedItem && defender.boosts.def === 0) {
400
+ desc.defenderItem = defender.item;
401
+ defenderUsedItem = true;
402
+ } else {
403
+ defender.boosts.def = Math.max(defender.boosts.def - 1, -6);
404
+ defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen);
405
+ }
406
+ desc.defenderAbility = defender.ability;
407
+ }
408
+ defender.boosts.spe = Math.min(defender.boosts.spe + 2, 6);
409
+ defender.stats.spe = getFinalSpeed(gen, defender, field, field.defenderSide);
410
+ }
411
+
412
+ if (move.dropsStats) {
413
+ if (attacker.hasAbility('Unaware')) {
414
+ desc.attackerAbility = attacker.ability;
415
+ } else {
416
+ // No move with dropsStats has fancy logic regarding category here
417
+ const stat = move.category === 'Special' ? 'spa' : 'atk';
418
+
419
+ let boosts = attacker.boosts[stat];
420
+ if (attacker.hasAbility('Contrary')) {
421
+ boosts = Math.min(6, boosts + move.dropsStats);
422
+ desc.attackerAbility = attacker.ability;
423
+ } else {
424
+ boosts = Math.max(-6, boosts - move.dropsStats * atkSimple);
425
+ }
426
+ if (atkSimple === 2) desc.attackerAbility = attacker.ability;
427
+
428
+ if (attacker.hasItem('White Herb') && attacker.boosts[stat] < 0 && !attackerUsedItem) {
429
+ boosts += move.dropsStats * atkSimple;
430
+ desc.attackerItem = attacker.item;
431
+ attackerUsedItem = true;
432
+ }
433
+
434
+ attacker.boosts[stat] = boosts;
435
+ attacker.stats[stat] = getModifiedStat(attacker.rawStats[stat], defender.boosts[stat], gen);
436
+ }
437
+ }
438
+
439
+ // Do ability swap after all other effects
440
+ if (defender.hasAbility('Mummy', 'Wandering Spirit', 'Lingering Aroma') && move.flags.contact) {
441
+ const oldAttackerAbility = attacker.ability;
442
+ attacker.ability = defender.ability;
443
+ // If attacker ability is notable, then ability swap is notable.
444
+ if (desc.attackerAbility) {
445
+ desc.defenderAbility = defender.ability;
446
+ }
447
+ if (defender.hasAbility('Wandering Spirit')) {
448
+ defender.ability = oldAttackerAbility;
449
+ }
450
+ }
451
+
452
+ return [attackerUsedItem, defenderUsedItem];
453
+ }
454
+
455
+ export function chainMods(mods: number[], lowerBound: number, upperBound: number) {
456
+ let M = 4096;
457
+ for (const mod of mods) {
458
+ if (mod !== 4096) {
459
+ M = (M * mod + 2048) >> 12;
460
+ }
461
+ }
462
+ return Math.max(Math.min(M, upperBound), lowerBound);
463
+ }
464
+
465
+ export function getBaseDamage(level: number, basePower: number, attack: number, defense: number) {
466
+ return Math.floor(
467
+ OF32(
468
+ Math.floor(
469
+ OF32(OF32(Math.floor((2 * level) / 5 + 2) * basePower) * attack) / defense
470
+ ) / 50 + 2
471
+ )
472
+ );
473
+ }
474
+
475
+ /**
476
+ * Get which stat will be boosted by Quark Drive or Protosynthesis
477
+ * In the case that `pokemon.boostedStat` is set, it will always return that stat
478
+ * In the case that two stats have equal value, stat choices will be prioritized
479
+ * in the following order:
480
+ * Attack, Defense, Special Attack, Special Defense, and Speed
481
+ *
482
+ * @param modifiedStats
483
+ * @returns
484
+ */
485
+ export function getQPBoostedStat(
486
+ pokemon: Pokemon,
487
+ gen?: Generation
488
+ ): StatID {
489
+ if (pokemon.boostedStat && pokemon.boostedStat !== 'auto') {
490
+ return pokemon.boostedStat; // override.
491
+ }
492
+ let bestStat: StatID = 'atk';
493
+ for (const stat of ['def', 'spa', 'spd', 'spe'] as StatID[]) {
494
+ if (
495
+ // proto/quark ignore boosts when considering their boost
496
+ getModifiedStat(pokemon.rawStats[stat], pokemon.boosts[stat], gen) >
497
+ getModifiedStat(pokemon.rawStats[bestStat], pokemon.boosts[bestStat], gen)
498
+ ) {
499
+ bestStat = stat;
500
+ }
501
+ }
502
+ return bestStat;
503
+ }
504
+
505
+ export function isQPActive(
506
+ pokemon: Pokemon,
507
+ field: Field
508
+ ) {
509
+ if (!pokemon.boostedStat) {
510
+ return false;
511
+ }
512
+
513
+ const weather = field.weather || '';
514
+ const terrain = field.terrain;
515
+
516
+ return (
517
+ (pokemon.hasAbility('Protosynthesis') &&
518
+ (weather.includes('Sun') || pokemon.hasItem('Booster Energy'))) ||
519
+ (pokemon.hasAbility('Quark Drive') &&
520
+ (terrain === 'Electric' || pokemon.hasItem('Booster Energy'))) ||
521
+ (pokemon.boostedStat !== 'auto')
522
+ );
523
+ }
524
+
525
+ export function getFinalDamage(
526
+ baseAmount: number,
527
+ i: number,
528
+ effectiveness: number,
529
+ isBurned: boolean,
530
+ stabMod: number,
531
+ finalMod: number,
532
+ protect?: boolean
533
+ ) {
534
+ let damageAmount = Math.floor(OF32(baseAmount * (85 + i)) / 100);
535
+ // If the stabMod would not accomplish anything we avoid applying it because it could cause
536
+ // us to calculate damage overflow incorrectly (DaWoblefet)
537
+ if (stabMod !== 4096) damageAmount = OF32(damageAmount * stabMod) / 4096;
538
+ damageAmount = Math.floor(OF32(pokeRound(damageAmount) * effectiveness));
539
+
540
+ if (isBurned) damageAmount = Math.floor(damageAmount / 2);
541
+ if (protect) damageAmount = pokeRound(OF32(damageAmount * 1024) / 4096);
542
+ return OF16(pokeRound(Math.max(1, OF32(damageAmount * finalMod) / 4096)));
543
+ }
544
+
545
+ /**
546
+ * Determines which move category Shell Side Arm should behave as.
547
+ *
548
+ * A simplified formula can be used here compared to what the research
549
+ * suggests as we do not want to implement the random tiebreak element of
550
+ * move - instead we simply default to 'Special' and allow the user to override
551
+ * this by manually adjusting the move's category.
552
+ *
553
+ * See also:
554
+ * {@link https://github.com/smogon/pokemon-showdown/commit/65d2bb5d}
555
+ *
556
+ * @param source Attacking pokemon (after stat modifications)
557
+ * @param target Target pokemon (after stat modifications)
558
+ * @returns 'Physical' | 'Special'
559
+ */
560
+ export function getShellSideArmCategory(source: Pokemon, target: Pokemon): MoveCategory {
561
+ const physicalDamage = source.stats.atk / target.stats.def;
562
+ const specialDamage = source.stats.spa / target.stats.spd;
563
+ return physicalDamage > specialDamage ? 'Physical' : 'Special';
564
+ }
565
+
566
+ export function getWeight(pokemon: Pokemon, desc: RawDesc, role: 'defender' | 'attacker') {
567
+ let weightHG = pokemon.weightkg * 10;
568
+ const abilityFactor = pokemon.hasAbility('Heavy Metal') ? 2
569
+ : pokemon.hasAbility('Light Metal') ? 0.5
570
+ : 1;
571
+ if (abilityFactor !== 1) {
572
+ weightHG = Math.max(Math.trunc(weightHG * abilityFactor), 1);
573
+ desc[`${role}Ability`] = pokemon.ability;
574
+ }
575
+
576
+ if (pokemon.hasItem('Float Stone')) {
577
+ weightHG = Math.max(Math.trunc(weightHG * 0.5), 1);
578
+ desc[`${role}Item`] = pokemon.item;
579
+ }
580
+
581
+ // convert back to kg
582
+ return weightHG / 10;
583
+ }
584
+
585
+ export function getStabMod(pokemon: Pokemon, move: Move, desc: RawDesc) {
586
+ let stabMod = 4096;
587
+ if (pokemon.hasOriginalType(move.type)) {
588
+ stabMod += 2048;
589
+ } else if (pokemon.hasAbility('Protean', 'Libero') && !pokemon.teraType) {
590
+ stabMod += 2048;
591
+ desc.attackerAbility = pokemon.ability;
592
+ }
593
+ const teraType = pokemon.teraType;
594
+ if (teraType === move.type && teraType !== 'Stellar') {
595
+ stabMod += 2048;
596
+ desc.attackerTera = teraType;
597
+ }
598
+ if (pokemon.hasAbility('Adaptability') && pokemon.hasType(move.type)) {
599
+ stabMod += teraType && pokemon.hasOriginalType(teraType) ? 1024 : 2048;
600
+ desc.attackerAbility = pokemon.ability;
601
+ }
602
+ return stabMod;
603
+ }
604
+
605
+ export function getStellarStabMod(pokemon: Pokemon, move: Move, stabMod = 1, turns = 0) {
606
+ const isStellarBoosted =
607
+ pokemon.teraType === 'Stellar' &&
608
+ ((move.isStellarFirstUse && turns === 0) || pokemon.named('Terapagos-Stellar'));
609
+ if (isStellarBoosted) {
610
+ if (pokemon.hasOriginalType(move.type)) {
611
+ stabMod += 2048;
612
+ } else {
613
+ stabMod = 4915;
614
+ }
615
+ }
616
+ return stabMod;
617
+ }
618
+
619
+ export function countBoosts(gen: Generation, boosts: StatsTable) {
620
+ let sum = 0;
621
+
622
+ const STATS: StatID[] = gen.num === 1
623
+ ? ['atk', 'def', 'spa', 'spe']
624
+ : ['atk', 'def', 'spa', 'spd', 'spe'];
625
+
626
+ for (const stat of STATS) {
627
+ // Only positive boosts are counted
628
+ const boost = boosts[stat];
629
+ if (boost && boost > 0) sum += boost;
630
+ }
631
+ return sum;
632
+ }
633
+
634
+ export function getStatDescriptionText(
635
+ gen: Generation,
636
+ pokemon: Pokemon,
637
+ stat: StatID,
638
+ natureName?: NatureName
639
+ ): string {
640
+ const nature = gen.natures.get(toID(natureName))!;
641
+ let desc = pokemon.evs[stat] +
642
+ (stat === 'hp' || nature.plus === nature.minus ? ''
643
+ : nature.plus === stat ? '+'
644
+ : nature.minus === stat ? '-'
645
+ : '') + ' ' +
646
+ Stats.displayStat(stat);
647
+ const iv = pokemon.ivs[stat];
648
+ if (iv !== 31) desc += ` ${iv} IVs`;
649
+ return desc;
650
+ }
651
+
652
+ export function handleFixedDamageMoves(attacker: Pokemon, move: Move) {
653
+ if (move.named('Seismic Toss', 'Night Shade')) {
654
+ return attacker.level;
655
+ } else if (move.named('Dragon Rage')) {
656
+ return 40;
657
+ } else if (move.named('Sonic Boom')) {
658
+ return 20;
659
+ }
660
+ return 0;
661
+ }
662
+
663
+ // Game Freak rounds DOWN on .5
664
+ export function pokeRound(num: number) {
665
+ return num % 1 > 0.5 ? Math.ceil(num) : Math.floor(num);
666
+ }
667
+
668
+ // 16-bit Overflow
669
+ export function OF16(n: number) {
670
+ return n > 65535 ? n % 65536 : n;
671
+ }
672
+
673
+ // 32-bit Overflow
674
+ export function OF32(n: number) {
675
+ return n > 4294967295 ? n % 4294967296 : n;
676
+ }