@robsonbittencourt/calc 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc +12 -0
- package/bundle +107 -0
- package/dist/adaptable.d.ts +6 -0
- package/dist/adaptable.js +28 -0
- package/dist/adaptable.js.map +1 -0
- package/dist/calc.d.ts +6 -0
- package/dist/calc.js +26 -0
- package/dist/calc.js.map +1 -0
- package/dist/data/abilities.d.ts +15 -0
- package/dist/data/abilities.js +448 -0
- package/dist/data/abilities.js.map +1 -0
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +30 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/interface.d.ts +150 -0
- package/dist/data/interface.js +3 -0
- package/dist/data/interface.js.map +1 -0
- package/dist/data/items.d.ts +24 -0
- package/dist/data/items.js +708 -0
- package/dist/data/items.js.map +1 -0
- package/dist/data/moves.d.ts +86 -0
- package/dist/data/moves.js +5014 -0
- package/dist/data/moves.js.map +1 -0
- package/dist/data/natures.d.ts +17 -0
- package/dist/data/natures.js +127 -0
- package/dist/data/natures.js.map +1 -0
- package/dist/data/production.min.js +1 -0
- package/dist/data/species.d.ts +48 -0
- package/dist/data/species.js +10126 -0
- package/dist/data/species.js.map +1 -0
- package/dist/data/types.d.ts +23 -0
- package/dist/data/types.js +538 -0
- package/dist/data/types.js.map +1 -0
- package/dist/desc.d.ts +65 -0
- package/dist/desc.js +866 -0
- package/dist/desc.js.map +1 -0
- package/dist/field.d.ts +49 -0
- package/dist/field.js +111 -0
- package/dist/field.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/items.d.ts +13 -0
- package/dist/items.js +434 -0
- package/dist/items.js.map +1 -0
- package/dist/mechanics/gen12.d.ts +6 -0
- package/dist/mechanics/gen12.js +271 -0
- package/dist/mechanics/gen12.js.map +1 -0
- package/dist/mechanics/gen3.d.ts +11 -0
- package/dist/mechanics/gen3.js +371 -0
- package/dist/mechanics/gen3.js.map +1 -0
- package/dist/mechanics/gen4.d.ts +11 -0
- package/dist/mechanics/gen4.js +596 -0
- package/dist/mechanics/gen4.js.map +1 -0
- package/dist/mechanics/gen56.d.ts +13 -0
- package/dist/mechanics/gen56.js +836 -0
- package/dist/mechanics/gen56.js.map +1 -0
- package/dist/mechanics/gen789.d.ts +14 -0
- package/dist/mechanics/gen789.js +1325 -0
- package/dist/mechanics/gen789.js.map +1 -0
- package/dist/mechanics/util.d.ts +39 -0
- package/dist/mechanics/util.js +675 -0
- package/dist/mechanics/util.js.map +1 -0
- package/dist/move.d.ts +50 -0
- package/dist/move.js +324 -0
- package/dist/move.js.map +1 -0
- package/dist/pokemon.d.ts +55 -0
- package/dist/pokemon.js +240 -0
- package/dist/pokemon.js.map +1 -0
- package/dist/production.min.js +1 -0
- package/dist/result.d.ts +34 -0
- package/dist/result.js +94 -0
- package/dist/result.js.map +1 -0
- package/dist/state.d.ts +77 -0
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -0
- package/dist/stats.d.ts +26 -0
- package/dist/stats.js +183 -0
- package/dist/stats.js.map +1 -0
- package/dist/test/calc.test.d.ts +1 -0
- package/dist/test/calc.test.js +1297 -0
- package/dist/test/calc.test.js.map +1 -0
- package/dist/test/data.test.d.ts +1 -0
- package/dist/test/data.test.js +368 -0
- package/dist/test/data.test.js.map +1 -0
- package/dist/test/gen.d.ts +135 -0
- package/dist/test/gen.js +636 -0
- package/dist/test/gen.js.map +1 -0
- package/dist/test/helper.d.ts +55 -0
- package/dist/test/helper.js +174 -0
- package/dist/test/helper.js.map +1 -0
- package/dist/test/move.test.d.ts +1 -0
- package/dist/test/move.test.js +14 -0
- package/dist/test/move.test.js.map +1 -0
- package/dist/test/pokemon.test.d.ts +1 -0
- package/dist/test/pokemon.test.js +102 -0
- package/dist/test/pokemon.test.js.map +1 -0
- package/dist/test/stats.test.d.ts +1 -0
- package/dist/test/stats.test.js +64 -0
- package/dist/test/stats.test.js.map +1 -0
- package/dist/test/utils.test.d.ts +1 -0
- package/dist/test/utils.test.js +19 -0
- package/dist/test/utils.test.js.map +1 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.js +115 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +40 -0
- package/src/adaptable.ts +12 -0
- package/src/calc.ts +40 -0
- package/src/data/abilities.ts +383 -0
- package/src/data/index.ts +36 -0
- package/src/data/interface.ts +176 -0
- package/src/data/items.ts +632 -0
- package/src/data/moves.ts +5028 -0
- package/src/data/natures.ts +65 -0
- package/src/data/species.ts +10098 -0
- package/src/data/types.ts +478 -0
- package/src/desc.ts +1063 -0
- package/src/field.ts +124 -0
- package/src/index.ts +156 -0
- package/src/items.ts +423 -0
- package/src/mechanics/gen12.ts +297 -0
- package/src/mechanics/gen3.ts +444 -0
- package/src/mechanics/gen4.ts +702 -0
- package/src/mechanics/gen56.ts +1134 -0
- package/src/mechanics/gen789.ts +1788 -0
- package/src/mechanics/util.ts +676 -0
- package/src/move.ts +337 -0
- package/src/pokemon.ts +244 -0
- package/src/result.ts +106 -0
- package/src/state.ts +81 -0
- package/src/stats.ts +213 -0
- package/src/test/calc.test.ts +1588 -0
- package/src/test/data.test.ts +129 -0
- package/src/test/gen.ts +514 -0
- package/src/test/helper.ts +185 -0
- package/src/test/move.test.ts +13 -0
- package/src/test/pokemon.test.ts +121 -0
- package/src/test/stats.test.ts +84 -0
- package/src/test/utils.test.ts +18 -0
- package/src/util.ts +153 -0
- package/tsconfig.json +10 -0
|
@@ -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
|
+
}
|