@pkmn/engine 0.1.0-dev.545e0ef3 → 0.1.0-dev.5bbdaae3
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/README.md +98 -59
- package/build/pkg/addon.d.ts +7 -0
- package/build/pkg/addon.js +112 -0
- package/build/pkg/addon.js.map +1 -0
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +25 -3
- package/build/pkg/data/protocol.json +69 -12
- package/build/pkg/data.d.ts +3 -4
- package/build/pkg/data.js +3 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +5 -4
- package/build/pkg/gen1.js +47 -31
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +15 -12
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +60 -39
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +400 -0
- package/build/test/display.js.map +1 -0
- package/build/tools/debug.js +133 -0
- package/build/tools/debug.js.map +1 -0
- package/build.zig +104 -90
- package/package.json +65 -36
- package/src/bin/install-pkmn-engine +68 -36
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +47 -142
- package/src/lib/binding/wasm.zig +6 -4
- package/src/lib/common/battle.zig +17 -0
- package/src/lib/common/data.zig +15 -27
- package/src/lib/common/debug.zig +21 -11
- package/src/lib/common/js.zig +118 -0
- package/src/lib/common/optional.zig +67 -0
- package/src/lib/common/options.zig +12 -3
- package/src/lib/common/protocol.zig +752 -237
- package/src/lib/common/rational.zig +353 -0
- package/src/lib/common/rng.zig +15 -15
- package/src/lib/common/util.zig +12 -0
- package/src/lib/gen1/README.md +270 -154
- package/src/lib/gen1/calc.zig +426 -0
- package/src/lib/gen1/chance.zig +1060 -0
- package/src/lib/gen1/data/moves.zig +192 -180
- package/src/lib/gen1/data/species.zig +2 -2
- package/src/lib/gen1/data/types.zig +5 -5
- package/src/lib/gen1/data.zig +78 -64
- package/src/lib/gen1/helpers.zig +577 -32
- package/src/lib/gen1/mechanics.zig +1019 -654
- package/src/lib/gen2/README.md +106 -85
- package/src/lib/gen2/data/items.zig +12 -18
- package/src/lib/gen2/data/moves.zig +654 -563
- package/src/lib/gen2/data/species.zig +1 -1
- package/src/lib/gen2/data/types.zig +130 -28
- package/src/lib/gen2/data.zig +131 -87
- package/src/lib/gen2/helpers.zig +16 -22
- package/src/lib/gen2/mechanics.zig +1781 -11
- package/src/lib/pkmn.zig +68 -9
- package/src/test/display/pkmn.html.tmpl +368 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
|
|
3
|
+
const pkmn = @import("../pkmn.zig");
|
|
4
|
+
|
|
3
5
|
const common = @import("../common/data.zig");
|
|
4
6
|
const DEBUG = @import("../common/debug.zig").print;
|
|
5
|
-
const options = @import("../common/options.zig");
|
|
6
7
|
const protocol = @import("../common/protocol.zig");
|
|
7
8
|
const rng = @import("../common/rng.zig");
|
|
8
9
|
|
|
@@ -17,7 +18,7 @@ const ID = common.ID;
|
|
|
17
18
|
const Player = common.Player;
|
|
18
19
|
const Result = common.Result;
|
|
19
20
|
|
|
20
|
-
const showdown = options.showdown;
|
|
21
|
+
const showdown = pkmn.options.showdown;
|
|
21
22
|
|
|
22
23
|
const Boost = protocol.Boost;
|
|
23
24
|
const Damage = protocol.Damage;
|
|
@@ -56,35 +57,37 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
57
|
|
|
57
58
|
const MAX_STAT_VALUE = 999;
|
|
58
59
|
|
|
59
|
-
pub fn update(battle: anytype, c1: Choice, c2: Choice,
|
|
60
|
+
pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
|
|
60
61
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
|
-
if (battle.turn == 0) return start(battle,
|
|
62
|
+
if (battle.turn == 0) return start(battle, options);
|
|
62
63
|
|
|
64
|
+
var s1 = false;
|
|
65
|
+
var s2 = false;
|
|
63
66
|
var f1: ?Move = null;
|
|
64
67
|
var f2: ?Move = null;
|
|
65
68
|
|
|
66
|
-
if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
|
|
67
|
-
if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
|
|
68
|
-
|
|
69
|
-
if (turnOrder(battle, c1, c2) == .P1) {
|
|
70
|
-
if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
|
|
71
|
-
} else {
|
|
72
|
-
if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
|
|
73
|
-
}
|
|
69
|
+
if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
|
|
70
|
+
if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
|
|
74
71
|
|
|
75
72
|
var p1 = battle.side(.P1);
|
|
76
|
-
if (p1.active.volatiles.attacks == 0) {
|
|
77
|
-
p1.active.volatiles.Binding = false;
|
|
78
|
-
}
|
|
79
73
|
var p2 = battle.side(.P2);
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
|
|
75
|
+
var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
76
|
+
var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
77
|
+
|
|
78
|
+
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
79
|
+
if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
|
|
80
|
+
} else {
|
|
81
|
+
if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
85
|
+
if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
|
|
86
|
+
|
|
87
|
+
return endTurn(battle, options);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
fn start(battle: anytype,
|
|
90
|
+
fn start(battle: anytype, options: anytype) !Result {
|
|
88
91
|
const p1 = battle.side(.P1);
|
|
89
92
|
const p2 = battle.side(.P2);
|
|
90
93
|
|
|
@@ -96,10 +99,10 @@ fn start(battle: anytype, log: anytype) !Result {
|
|
|
96
99
|
assert(!showdown or p2_slot == 1);
|
|
97
100
|
if (p2_slot == 0) return Result.Win;
|
|
98
101
|
|
|
99
|
-
try switchIn(battle, .P1, p1_slot, true,
|
|
100
|
-
try switchIn(battle, .P2, p2_slot, true,
|
|
102
|
+
try switchIn(battle, .P1, p1_slot, true, options);
|
|
103
|
+
try switchIn(battle, .P2, p2_slot, true, options);
|
|
101
104
|
|
|
102
|
-
return endTurn(battle,
|
|
105
|
+
return endTurn(battle, options);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
fn findFirstAlive(side: *const Side) u8 {
|
|
@@ -112,6 +115,7 @@ fn selectMove(
|
|
|
112
115
|
player: Player,
|
|
113
116
|
choice: Choice,
|
|
114
117
|
foe_choice: Choice,
|
|
118
|
+
skip_turn: *bool,
|
|
115
119
|
from: *?Move,
|
|
116
120
|
) ?Result {
|
|
117
121
|
if (choice.type == .Pass) return null;
|
|
@@ -121,20 +125,29 @@ fn selectMove(
|
|
|
121
125
|
const stored = side.stored();
|
|
122
126
|
|
|
123
127
|
assert(!isForced(side.active) or
|
|
124
|
-
(choice.type == .Move and choice.data == @
|
|
128
|
+
(choice.type == .Move and choice.data == @intFromBool(showdown)));
|
|
125
129
|
|
|
126
130
|
// pre-battle menu
|
|
127
|
-
if (volatiles.Recharging)
|
|
131
|
+
if (volatiles.Recharging) {
|
|
132
|
+
if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
128
135
|
if (volatiles.Rage) {
|
|
129
136
|
from.* = side.last_used_move;
|
|
130
|
-
if (showdown)
|
|
137
|
+
if (showdown) {
|
|
138
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
139
|
+
saveMove(battle, player, null);
|
|
140
|
+
}
|
|
131
141
|
return null;
|
|
132
142
|
}
|
|
133
143
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
134
144
|
if (!showdown) volatiles.Flinch = false;
|
|
135
145
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
136
146
|
from.* = side.last_selected_move;
|
|
137
|
-
if (showdown)
|
|
147
|
+
if (showdown) {
|
|
148
|
+
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
149
|
+
saveMove(battle, player, null);
|
|
150
|
+
}
|
|
138
151
|
return null;
|
|
139
152
|
}
|
|
140
153
|
|
|
@@ -144,7 +157,10 @@ fn selectMove(
|
|
|
144
157
|
// pre-move select
|
|
145
158
|
if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
|
|
146
159
|
assert(showdown or choice.data == 0);
|
|
147
|
-
if (showdown)
|
|
160
|
+
if (showdown) {
|
|
161
|
+
if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
162
|
+
saveMove(battle, player, choice);
|
|
163
|
+
}
|
|
148
164
|
return null;
|
|
149
165
|
}
|
|
150
166
|
if (volatiles.Binding) {
|
|
@@ -162,11 +178,7 @@ fn selectMove(
|
|
|
162
178
|
assert(choice.data == 0);
|
|
163
179
|
// GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
|
|
164
180
|
if (foe_choice.type == .Switch) {
|
|
165
|
-
const
|
|
166
|
-
battle.last_selected_indexes.p1
|
|
167
|
-
else
|
|
168
|
-
battle.last_selected_indexes.p2;
|
|
169
|
-
const last = side.active.move(@intCast(u4, slot));
|
|
181
|
+
const last = side.active.move(battle.lastMove(player).index);
|
|
170
182
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
171
183
|
if (last.id == .MirrorMove) return Result.Error;
|
|
172
184
|
}
|
|
@@ -175,6 +187,7 @@ fn selectMove(
|
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
190
|
+
skip_turn.* = true;
|
|
178
191
|
if (showdown) {
|
|
179
192
|
saveMove(battle, player, choice);
|
|
180
193
|
} else {
|
|
@@ -185,6 +198,7 @@ fn selectMove(
|
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
// move select
|
|
201
|
+
volatiles.state = 0;
|
|
188
202
|
if (choice.data == 0) {
|
|
189
203
|
const struggle = ok: {
|
|
190
204
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -192,12 +206,9 @@ fn selectMove(
|
|
|
192
206
|
}
|
|
193
207
|
break :ok true;
|
|
194
208
|
};
|
|
195
|
-
|
|
196
209
|
assert(struggle);
|
|
197
|
-
saveMove(battle, player, choice);
|
|
198
|
-
} else {
|
|
199
|
-
saveMove(battle, player, choice);
|
|
200
210
|
}
|
|
211
|
+
saveMove(battle, player, choice);
|
|
201
212
|
|
|
202
213
|
return null;
|
|
203
214
|
}
|
|
@@ -217,16 +228,12 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
217
228
|
assert(showdown or move.pp != 0);
|
|
218
229
|
|
|
219
230
|
side.last_selected_move = move.id;
|
|
220
|
-
|
|
221
|
-
battle.last_selected_indexes.p1 = @intCast(u4, c.data);
|
|
222
|
-
} else {
|
|
223
|
-
battle.last_selected_indexes.p2 = @intCast(u4, c.data);
|
|
224
|
-
}
|
|
231
|
+
battle.lastMove(player).index = @intCast(c.data);
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
}
|
|
228
235
|
|
|
229
|
-
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool,
|
|
236
|
+
fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: anytype) !void {
|
|
230
237
|
var side = battle.side(player);
|
|
231
238
|
var foe = battle.foe(player);
|
|
232
239
|
var active = &side.active;
|
|
@@ -239,11 +246,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
239
246
|
side.order[0] = side.order[slot - 1];
|
|
240
247
|
side.order[slot - 1] = out;
|
|
241
248
|
|
|
242
|
-
|
|
243
|
-
battle.last_selected_indexes.p1 = 1;
|
|
244
|
-
} else {
|
|
245
|
-
battle.last_selected_indexes.p2 = 1;
|
|
246
|
-
}
|
|
249
|
+
battle.lastMove(player).index = 1;
|
|
247
250
|
|
|
248
251
|
side.last_used_move = .None;
|
|
249
252
|
foe.last_used_move = .None;
|
|
@@ -259,17 +262,18 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
|
|
|
259
262
|
|
|
260
263
|
foe.active.volatiles.Binding = false;
|
|
261
264
|
|
|
262
|
-
try log.switched(battle.active(player), incoming);
|
|
265
|
+
try options.log.switched(battle.active(player), incoming);
|
|
266
|
+
options.chance.switched(player, side.order[0], out);
|
|
263
267
|
|
|
264
268
|
if (showdown and incoming.status == Status.TOX) {
|
|
265
269
|
incoming.status = Status.init(.PSN);
|
|
266
270
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
267
271
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
268
|
-
try log.status(battle.active(player), incoming.status, .Silent);
|
|
272
|
+
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
|
|
272
|
-
fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
276
|
+
fn turnOrder(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Player {
|
|
273
277
|
assert(c1.type != .Pass or c2.type != .Pass);
|
|
274
278
|
|
|
275
279
|
if (c1.type == .Pass) return .P2;
|
|
@@ -299,13 +303,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
299
303
|
const spe2 = battle.side(.P2).active.stats.spe;
|
|
300
304
|
if (spe1 == spe2) {
|
|
301
305
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
|
-
if (showdown and m1 == .Counter and m2 == .Counter)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
306
|
+
if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
|
|
307
|
+
battle.rng.advance(1);
|
|
308
|
+
}
|
|
308
309
|
|
|
310
|
+
const p1 = try Rolls.speedTie(battle, options);
|
|
309
311
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
312
|
|
|
311
313
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +317,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
317
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
318
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
319
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
320
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
321
|
// won the original coin flip
|
|
320
322
|
if (t1 and t2) return .P1;
|
|
321
323
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -328,34 +330,59 @@ fn doTurn(
|
|
|
328
330
|
battle: anytype,
|
|
329
331
|
player: Player,
|
|
330
332
|
player_choice: Choice,
|
|
333
|
+
player_rewrap: bool,
|
|
334
|
+
player_skip: bool,
|
|
331
335
|
player_from: ?Move,
|
|
332
336
|
foe_player: Player,
|
|
333
337
|
foe_choice: Choice,
|
|
338
|
+
foe_rewrap: bool,
|
|
339
|
+
foe_skip: bool,
|
|
334
340
|
foe_from: ?Move,
|
|
335
|
-
|
|
341
|
+
options: anytype,
|
|
336
342
|
) !?Result {
|
|
337
343
|
assert(player_choice.type != .Pass);
|
|
338
344
|
|
|
339
345
|
var residual = true;
|
|
340
346
|
var replace = battle.side(player).stored().hp == 0;
|
|
341
|
-
if (try executeMove(
|
|
347
|
+
if (try executeMove(
|
|
348
|
+
battle,
|
|
349
|
+
player,
|
|
350
|
+
player_choice,
|
|
351
|
+
player_rewrap,
|
|
352
|
+
player_skip,
|
|
353
|
+
player_from,
|
|
354
|
+
&residual,
|
|
355
|
+
options,
|
|
356
|
+
)) |r| return r;
|
|
342
357
|
if (!replace) {
|
|
343
358
|
if (player_choice.type != .Switch) {
|
|
344
|
-
if (try checkFaint(battle, foe_player,
|
|
359
|
+
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
360
|
}
|
|
346
|
-
if (residual) try handleResidual(battle, player,
|
|
347
|
-
if (try checkFaint(battle, player,
|
|
361
|
+
if (residual) try handleResidual(battle, player, options);
|
|
362
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
363
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
364
|
|
|
365
|
+
options.chance.clearPending();
|
|
366
|
+
|
|
350
367
|
residual = true;
|
|
351
368
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
352
|
-
|
|
369
|
+
const calc = pkmn.options.calc and foe_choice.type == .Pass;
|
|
370
|
+
if (if (calc) null else try executeMove(
|
|
371
|
+
battle,
|
|
372
|
+
foe_player,
|
|
373
|
+
foe_choice,
|
|
374
|
+
foe_rewrap,
|
|
375
|
+
foe_skip,
|
|
376
|
+
foe_from,
|
|
377
|
+
&residual,
|
|
378
|
+
options,
|
|
379
|
+
)) |r| return r;
|
|
353
380
|
if (!replace) {
|
|
354
|
-
if (foe_choice.type != .Switch) {
|
|
355
|
-
if (try checkFaint(battle, player,
|
|
381
|
+
if (!calc and foe_choice.type != .Switch) {
|
|
382
|
+
if (try checkFaint(battle, player, options)) |r| return r;
|
|
356
383
|
}
|
|
357
|
-
if (residual) try handleResidual(battle, foe_player,
|
|
358
|
-
if (try checkFaint(battle, foe_player,
|
|
384
|
+
if (residual) try handleResidual(battle, foe_player, options);
|
|
385
|
+
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
359
386
|
}
|
|
360
387
|
|
|
361
388
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -372,29 +399,32 @@ fn executeMove(
|
|
|
372
399
|
battle: anytype,
|
|
373
400
|
player: Player,
|
|
374
401
|
choice: Choice,
|
|
402
|
+
rewrap: bool,
|
|
403
|
+
skip: bool,
|
|
375
404
|
from: ?Move,
|
|
376
405
|
residual: *bool,
|
|
377
|
-
|
|
406
|
+
options: anytype,
|
|
378
407
|
) !?Result {
|
|
379
408
|
var side = battle.side(player);
|
|
380
409
|
|
|
381
410
|
if (choice.type == .Switch) {
|
|
382
|
-
try switchIn(battle, player,
|
|
411
|
+
try switchIn(battle, player, choice.data, false, options);
|
|
383
412
|
return null;
|
|
384
413
|
}
|
|
385
414
|
|
|
415
|
+
// This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
|
|
416
|
+
// Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
|
|
417
|
+
// additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
|
|
386
418
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
387
|
-
// Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
|
|
388
|
-
// Binding instead gets handled in beforeMove after sleep and freeze
|
|
389
419
|
assert(!showdown);
|
|
390
420
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
391
|
-
try log.cant(battle.active(player), .Bound);
|
|
421
|
+
try options.log.cant(battle.active(player), .Bound);
|
|
392
422
|
}
|
|
393
423
|
return null;
|
|
394
424
|
}
|
|
395
425
|
|
|
396
426
|
assert(choice.type == .Move);
|
|
397
|
-
var mslot = @intCast(
|
|
427
|
+
var mslot: u4 = @intCast(choice.data);
|
|
398
428
|
// Sadly, we can't even check `Move.get(side.last_selected_move).effect == .Binding` here
|
|
399
429
|
// because Pokémon Showdown's Mirror Move implementation clobbers side.last_selected_move
|
|
400
430
|
var auto = showdown and side.last_selected_move != .None;
|
|
@@ -403,10 +433,7 @@ fn executeMove(
|
|
|
403
433
|
if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
|
|
404
434
|
// choice.data == 0 only happens with Struggle on Pokémon Showdown
|
|
405
435
|
assert(!showdown);
|
|
406
|
-
mslot = @intCast(
|
|
407
|
-
battle.last_selected_indexes.p1
|
|
408
|
-
else
|
|
409
|
-
battle.last_selected_indexes.p2);
|
|
436
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
410
437
|
const stored = side.stored();
|
|
411
438
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
412
439
|
auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
|
|
@@ -427,15 +454,12 @@ fn executeMove(
|
|
|
427
454
|
} else if (showdown and side.active.volatiles.Charging) {
|
|
428
455
|
// Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
|
|
429
456
|
assert(mslot == 1);
|
|
430
|
-
mslot = @intCast(
|
|
431
|
-
battle.last_selected_indexes.p1
|
|
432
|
-
else
|
|
433
|
-
battle.last_selected_indexes.p2);
|
|
457
|
+
mslot = @intCast(battle.lastMove(player).index);
|
|
434
458
|
}
|
|
435
459
|
|
|
436
460
|
var skip_can = false;
|
|
437
461
|
var skip_pp = false;
|
|
438
|
-
switch (try beforeMove(battle, player, from, residual,
|
|
462
|
+
switch (try beforeMove(battle, player, skip, from, residual, options)) {
|
|
439
463
|
.done => return null,
|
|
440
464
|
.skip_can => skip_can = true,
|
|
441
465
|
.skip_pp => skip_pp = true,
|
|
@@ -443,22 +467,24 @@ fn executeMove(
|
|
|
443
467
|
.err => return @as(?Result, Result.Error),
|
|
444
468
|
}
|
|
445
469
|
|
|
446
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual,
|
|
470
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
|
|
447
471
|
return null;
|
|
448
472
|
}
|
|
449
473
|
|
|
450
|
-
return doMove(battle, player, mslot, auto, residual,
|
|
474
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
451
475
|
}
|
|
452
476
|
|
|
453
|
-
const BeforeMove =
|
|
477
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
454
478
|
|
|
455
479
|
fn beforeMove(
|
|
456
480
|
battle: anytype,
|
|
457
481
|
player: Player,
|
|
482
|
+
skip: bool,
|
|
458
483
|
from: ?Move,
|
|
459
484
|
residual: *bool,
|
|
460
|
-
|
|
485
|
+
options: anytype,
|
|
461
486
|
) !BeforeMove {
|
|
487
|
+
var log = options.log;
|
|
462
488
|
var side = battle.side(player);
|
|
463
489
|
const foe = battle.foe(player);
|
|
464
490
|
var active = &side.active;
|
|
@@ -467,11 +493,21 @@ fn beforeMove(
|
|
|
467
493
|
var volatiles = &active.volatiles;
|
|
468
494
|
|
|
469
495
|
if (Status.is(stored.status, .SLP)) {
|
|
470
|
-
const
|
|
496
|
+
const before = stored.status;
|
|
471
497
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
498
|
+
if (options.calc.modify(player, .sleep)) |extend| {
|
|
499
|
+
if (!extend) stored.status = 0;
|
|
500
|
+
} else {
|
|
501
|
+
stored.status -= 1;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const duration = Status.duration(stored.status);
|
|
505
|
+
if (!Status.is(stored.status, .EXT)) {
|
|
506
|
+
try options.chance.sleep(player, duration);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (duration == 0) {
|
|
510
|
+
try log.curestatus(ident, before, .Message);
|
|
475
511
|
stored.status = 0; // clears EXT if present
|
|
476
512
|
} else {
|
|
477
513
|
try log.cant(ident, .Sleep);
|
|
@@ -486,13 +522,15 @@ fn beforeMove(
|
|
|
486
522
|
return .done;
|
|
487
523
|
}
|
|
488
524
|
|
|
489
|
-
if (foe.active.volatiles.Binding) {
|
|
525
|
+
if (skip or foe.active.volatiles.Binding) {
|
|
490
526
|
try log.cant(ident, .Bound);
|
|
491
527
|
return .done;
|
|
492
528
|
}
|
|
493
529
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
530
|
+
if (volatiles.Flinch) {
|
|
531
|
+
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
532
|
+
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
533
|
+
if (!showdown) volatiles.Flinch = false;
|
|
496
534
|
try log.cant(ident, .Flinch);
|
|
497
535
|
return .done;
|
|
498
536
|
}
|
|
@@ -504,21 +542,20 @@ fn beforeMove(
|
|
|
504
542
|
}
|
|
505
543
|
|
|
506
544
|
if (volatiles.disabled_duration > 0) {
|
|
507
|
-
|
|
545
|
+
if (options.calc.modify(player, .disable)) |extend| {
|
|
546
|
+
if (!extend) volatiles.disabled_duration = 0;
|
|
547
|
+
} else {
|
|
548
|
+
volatiles.disabled_duration -= 1;
|
|
549
|
+
}
|
|
550
|
+
try options.chance.disable(player, volatiles.disabled_duration);
|
|
551
|
+
|
|
508
552
|
if (volatiles.disabled_duration == 0) {
|
|
509
553
|
volatiles.disabled_move = 0;
|
|
510
554
|
try log.end(ident, .Disable);
|
|
511
555
|
}
|
|
512
556
|
}
|
|
513
557
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
514
|
-
if (showdown and try disabled(side, ident,
|
|
515
|
-
|
|
516
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
517
|
-
if (showdown and volatiles.Flinch) {
|
|
518
|
-
volatiles.Flinch = false;
|
|
519
|
-
try log.cant(ident, .Flinch);
|
|
520
|
-
return .done;
|
|
521
|
-
}
|
|
558
|
+
if (showdown and try disabled(side, ident, options)) return .done;
|
|
522
559
|
|
|
523
560
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
524
561
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
@@ -530,19 +567,20 @@ fn beforeMove(
|
|
|
530
567
|
if (volatiles.Confusion) {
|
|
531
568
|
assert(volatiles.confusion > 0);
|
|
532
569
|
|
|
533
|
-
|
|
570
|
+
if (options.calc.modify(player, .confusion)) |extend| {
|
|
571
|
+
if (!extend) volatiles.confusion = 0;
|
|
572
|
+
} else {
|
|
573
|
+
volatiles.confusion -= 1;
|
|
574
|
+
}
|
|
575
|
+
try options.chance.confusion(player, volatiles.confusion);
|
|
576
|
+
|
|
534
577
|
if (volatiles.confusion == 0) {
|
|
535
578
|
volatiles.Confusion = false;
|
|
536
579
|
try log.end(ident, .Confusion);
|
|
537
580
|
} else {
|
|
538
581
|
try log.activate(ident, .Confusion);
|
|
539
582
|
|
|
540
|
-
|
|
541
|
-
!battle.rng.chance(u8, 128, 256)
|
|
542
|
-
else
|
|
543
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
-
|
|
545
|
-
if (confused) {
|
|
583
|
+
if (try Rolls.confused(battle, player, options)) {
|
|
546
584
|
assert(!volatiles.MultiHit);
|
|
547
585
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
548
586
|
volatiles.Bide = false;
|
|
@@ -552,6 +590,7 @@ fn beforeMove(
|
|
|
552
590
|
volatiles.Charging = false;
|
|
553
591
|
volatiles.Binding = false;
|
|
554
592
|
volatiles.Invulnerable = false;
|
|
593
|
+
options.chance.clearDurations(player, null);
|
|
555
594
|
{
|
|
556
595
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
557
596
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -568,42 +607,53 @@ fn beforeMove(
|
|
|
568
607
|
player
|
|
569
608
|
else
|
|
570
609
|
player.foe();
|
|
610
|
+
|
|
611
|
+
const uncapped = battle.last_damage;
|
|
571
612
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
572
|
-
_ = try applyDamage(battle, player, target, .Confusion,
|
|
613
|
+
_ = try applyDamage(battle, player, target, .Confusion, options);
|
|
614
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
615
|
+
if (showdown) battle.last_damage = uncapped;
|
|
573
616
|
|
|
574
617
|
return .done;
|
|
575
618
|
}
|
|
576
619
|
}
|
|
577
620
|
}
|
|
578
621
|
|
|
579
|
-
if (!showdown and try disabled(side, ident,
|
|
580
|
-
|
|
581
|
-
if (Status.is(stored.status, .PAR)) {
|
|
582
|
-
const paralyzed = if (showdown)
|
|
583
|
-
battle.rng.chance(u8, 63, 256)
|
|
584
|
-
else
|
|
585
|
-
battle.rng.next() < Gen12.percent(25);
|
|
622
|
+
if (!showdown and try disabled(side, ident, options)) return .done;
|
|
586
623
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
624
|
+
if (Status.is(stored.status, .PAR) and try Rolls.paralyzed(battle, player, options)) {
|
|
625
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
626
|
+
volatiles.Bide = false;
|
|
627
|
+
volatiles.Thrashing = false;
|
|
628
|
+
volatiles.Charging = false;
|
|
629
|
+
volatiles.Binding = false;
|
|
630
|
+
options.chance.clearDurations(player, null);
|
|
631
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
632
|
+
try log.cant(ident, .Paralysis);
|
|
633
|
+
return .done;
|
|
597
634
|
}
|
|
598
635
|
|
|
599
636
|
if (volatiles.Bide) {
|
|
600
637
|
assert(!volatiles.Thrashing and !volatiles.Rage);
|
|
601
638
|
|
|
602
|
-
|
|
639
|
+
if (showdown) {
|
|
640
|
+
// Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
|
|
641
|
+
// OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
|
|
642
|
+
// addition because 65535 is sufficient to faint any Pokémon anyway
|
|
643
|
+
volatiles.state +|= battle.last_damage;
|
|
644
|
+
} else {
|
|
645
|
+
volatiles.state +%= battle.last_damage;
|
|
646
|
+
}
|
|
603
647
|
|
|
604
648
|
assert(volatiles.attacks > 0);
|
|
605
649
|
|
|
606
|
-
|
|
650
|
+
if (options.calc.modify(player, .attacking)) |extend| {
|
|
651
|
+
if (!extend) volatiles.attacks = 0;
|
|
652
|
+
} else {
|
|
653
|
+
volatiles.attacks -= 1;
|
|
654
|
+
}
|
|
655
|
+
try options.chance.attacking(player, volatiles.attacks);
|
|
656
|
+
|
|
607
657
|
if (volatiles.attacks != 0) {
|
|
608
658
|
try log.activate(ident, .Bide);
|
|
609
659
|
return .done;
|
|
@@ -620,15 +670,16 @@ fn beforeMove(
|
|
|
620
670
|
return .done;
|
|
621
671
|
}
|
|
622
672
|
|
|
623
|
-
|
|
624
|
-
|
|
673
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
674
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
675
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
625
676
|
|
|
626
677
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
627
678
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
628
679
|
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
629
680
|
if (showdown) {
|
|
630
681
|
residual.* = false;
|
|
631
|
-
try handleResidual(battle, player,
|
|
682
|
+
try handleResidual(battle, player, options);
|
|
632
683
|
}
|
|
633
684
|
|
|
634
685
|
return .done;
|
|
@@ -637,20 +688,39 @@ fn beforeMove(
|
|
|
637
688
|
if (volatiles.Thrashing) {
|
|
638
689
|
assert(volatiles.attacks > 0);
|
|
639
690
|
assert(from != null);
|
|
640
|
-
|
|
641
|
-
if (
|
|
691
|
+
|
|
692
|
+
if (options.calc.modify(player, .attacking)) |extend| {
|
|
693
|
+
if (!extend) volatiles.attacks = 0;
|
|
694
|
+
} else {
|
|
695
|
+
volatiles.attacks -= 1;
|
|
696
|
+
}
|
|
697
|
+
try options.chance.attacking(player, volatiles.attacks);
|
|
698
|
+
|
|
699
|
+
if (!showdown and handleThrashing(battle, active, player, options)) {
|
|
642
700
|
try log.start(battle.active(player), .ConfusionSilent);
|
|
643
701
|
}
|
|
644
702
|
try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
|
|
645
|
-
if (showdown
|
|
646
|
-
|
|
703
|
+
if (showdown) {
|
|
704
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
705
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
706
|
+
side.last_used_move = side.last_selected_move;
|
|
707
|
+
if (handleThrashing(battle, active, player, options)) {
|
|
708
|
+
try log.start(battle.active(player), .ConfusionSilent);
|
|
709
|
+
}
|
|
647
710
|
}
|
|
648
711
|
return .skip_can;
|
|
649
712
|
}
|
|
650
713
|
|
|
651
714
|
if (volatiles.Binding) {
|
|
652
715
|
assert(volatiles.attacks > 0);
|
|
653
|
-
|
|
716
|
+
|
|
717
|
+
if (options.calc.modify(player, .binding)) |extend| {
|
|
718
|
+
if (!extend) volatiles.attacks = 0;
|
|
719
|
+
} else {
|
|
720
|
+
volatiles.attacks -= 1;
|
|
721
|
+
}
|
|
722
|
+
try options.chance.binding(player, volatiles.attacks);
|
|
723
|
+
|
|
654
724
|
try log.move(
|
|
655
725
|
battle.active(player),
|
|
656
726
|
side.last_selected_move,
|
|
@@ -658,7 +728,11 @@ fn beforeMove(
|
|
|
658
728
|
side.last_selected_move,
|
|
659
729
|
);
|
|
660
730
|
if (showdown or battle.last_damage != 0) {
|
|
661
|
-
|
|
731
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
732
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
733
|
+
if (battle.foe(player).stored().hp > 0 and !sub) {
|
|
734
|
+
try buildRage(battle, player.foe(), options);
|
|
735
|
+
}
|
|
662
736
|
}
|
|
663
737
|
return .done;
|
|
664
738
|
}
|
|
@@ -674,7 +748,7 @@ fn canMove(
|
|
|
674
748
|
skip_pp: bool,
|
|
675
749
|
from: ?Move,
|
|
676
750
|
residual: *bool,
|
|
677
|
-
|
|
751
|
+
options: anytype,
|
|
678
752
|
) !bool {
|
|
679
753
|
var side = battle.side(player);
|
|
680
754
|
const player_ident = battle.active(player);
|
|
@@ -689,8 +763,9 @@ fn canMove(
|
|
|
689
763
|
side.active.volatiles.Charging = false;
|
|
690
764
|
side.active.volatiles.Invulnerable = false;
|
|
691
765
|
} else if (move.effect == .Charge) {
|
|
692
|
-
try log.move(player_ident, side.last_selected_move, .{}, from);
|
|
693
|
-
|
|
766
|
+
try options.log.move(player_ident, side.last_selected_move, .{}, from);
|
|
767
|
+
setCounterable(battle, player, side, move);
|
|
768
|
+
try Effects.charge(battle, player, options);
|
|
694
769
|
return false;
|
|
695
770
|
}
|
|
696
771
|
|
|
@@ -705,23 +780,35 @@ fn canMove(
|
|
|
705
780
|
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
706
781
|
// regrettable, but this information is more "nice to have" than required
|
|
707
782
|
const f = if (from != null and from.? == .None) null else from;
|
|
708
|
-
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
783
|
+
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
784
|
+
setCounterable(battle, player, side, move);
|
|
709
785
|
|
|
710
786
|
if (move.effect.onBegin()) {
|
|
711
|
-
try onBegin(battle, player, move, mslot, residual,
|
|
787
|
+
try onBegin(battle, player, move, mslot, residual, options);
|
|
712
788
|
return false;
|
|
713
789
|
}
|
|
714
790
|
|
|
715
791
|
if (move.effect == .Thrashing) {
|
|
716
|
-
Effects.thrashing(battle, player);
|
|
792
|
+
Effects.thrashing(battle, player, options);
|
|
717
793
|
} else if (!showdown and move.effect == .Binding) {
|
|
718
794
|
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
719
|
-
Effects.binding(battle, player);
|
|
795
|
+
try Effects.binding(battle, player, false, options);
|
|
720
796
|
}
|
|
721
797
|
|
|
722
798
|
return true;
|
|
723
799
|
}
|
|
724
800
|
|
|
801
|
+
fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
|
|
802
|
+
// The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
|
|
803
|
+
// in cases where an early return happens the data for a players last selected move does not get
|
|
804
|
+
// reloaded and HandleCounterMove actually bases its success/failure off of stale information.
|
|
805
|
+
// This boolean state we track here doesn't exist on the cartridge because it instead manifests
|
|
806
|
+
// as the desync results from actually having two separate battle states that subtly disagree
|
|
807
|
+
const counterable = side.last_selected_move != .Counter and move.bp > 0 and
|
|
808
|
+
(move.type == .Normal or move.type == .Fighting);
|
|
809
|
+
battle.lastMove(player).counterable = @intFromBool(counterable);
|
|
810
|
+
}
|
|
811
|
+
|
|
725
812
|
fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
726
813
|
if (side.last_selected_move == .Struggle) return;
|
|
727
814
|
|
|
@@ -732,11 +819,11 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
732
819
|
if (volatiles.Bide) return;
|
|
733
820
|
|
|
734
821
|
assert(active.move(mslot).pp > 0 or auto);
|
|
735
|
-
active.move(mslot).pp = @
|
|
822
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
|
|
736
823
|
if (volatiles.Transform) return;
|
|
737
824
|
|
|
738
825
|
assert(side.stored().move(mslot).pp > 0 or auto);
|
|
739
|
-
side.stored().move(mslot).pp = @
|
|
826
|
+
side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
|
|
740
827
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
741
828
|
}
|
|
742
829
|
|
|
@@ -744,12 +831,12 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
744
831
|
var active = &side.active;
|
|
745
832
|
const volatiles = &active.volatiles;
|
|
746
833
|
|
|
747
|
-
active.move(mslot).pp = @
|
|
834
|
+
active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) +% 1;
|
|
748
835
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
749
836
|
if (showdown and volatiles.Transform) return;
|
|
750
837
|
|
|
751
838
|
assert(mslot > 0 and mslot <= 4);
|
|
752
|
-
side.stored().moves[mslot - 1].pp = @
|
|
839
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
|
|
753
840
|
}
|
|
754
841
|
|
|
755
842
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -757,10 +844,12 @@ fn doMove(
|
|
|
757
844
|
battle: anytype,
|
|
758
845
|
player: Player,
|
|
759
846
|
mslot: u4,
|
|
847
|
+
rewrap: bool,
|
|
760
848
|
auto: bool,
|
|
761
849
|
residual: *bool,
|
|
762
|
-
|
|
850
|
+
options: anytype,
|
|
763
851
|
) !?Result {
|
|
852
|
+
var log = options.log;
|
|
764
853
|
var side = battle.side(player);
|
|
765
854
|
const foe = battle.foe(player);
|
|
766
855
|
|
|
@@ -775,37 +864,44 @@ fn doMove(
|
|
|
775
864
|
var hits: u4 = 1;
|
|
776
865
|
var effectiveness = Effectiveness.neutral;
|
|
777
866
|
|
|
867
|
+
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
868
|
+
if (showdown and !counter) battle.last_damage = 0;
|
|
869
|
+
|
|
778
870
|
// The cartridge handles set damage moves in applyDamage but we short circuit to simplify things
|
|
779
871
|
if (move.effect == .SuperFang or move.effect == .SpecialDamage) {
|
|
780
|
-
return specialDamage(battle, player, move,
|
|
872
|
+
return specialDamage(battle, player, move, options);
|
|
781
873
|
}
|
|
782
874
|
|
|
783
875
|
// Pokémon Showdown runs invulnerability / immunity checks before checking accuracy - simply
|
|
784
876
|
// calling moveHit early covers most of that but we also need to check type immunity first
|
|
785
877
|
var miss = showdown and miss: {
|
|
786
878
|
immune = move.target != .Self and !status and !counter and
|
|
787
|
-
(@
|
|
788
|
-
@
|
|
879
|
+
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
880
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
789
881
|
if (immune and move.effect != .Binding) break :miss true;
|
|
790
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
791
|
-
|
|
882
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
883
|
+
battle.last_damage = 0;
|
|
884
|
+
break :miss true;
|
|
885
|
+
}
|
|
886
|
+
break :miss (move.target != .Self and
|
|
887
|
+
!moveHit(battle, player, move, &immune, &mist, options));
|
|
792
888
|
};
|
|
793
889
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
794
890
|
|
|
795
|
-
var late = showdown;
|
|
891
|
+
var late = showdown and move.effect != .Explode;
|
|
796
892
|
const skip = status or immune;
|
|
797
893
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
798
894
|
if (showdown and move.effect.isMulti()) {
|
|
799
|
-
Effects.multiHit(battle, player, move);
|
|
895
|
+
try Effects.multiHit(battle, player, move, options);
|
|
800
896
|
hits = side.active.volatiles.attacks;
|
|
801
897
|
late = false;
|
|
802
898
|
}
|
|
803
899
|
|
|
804
900
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
805
901
|
const check = !showdown or (!counter and move.effect != .OHKO);
|
|
806
|
-
if (check) crit = checkCriticalHit(battle, player, move);
|
|
902
|
+
if (check) crit = try checkCriticalHit(battle, player, move, options);
|
|
807
903
|
|
|
808
|
-
if (counter) return counterDamage(battle, player, move,
|
|
904
|
+
if (counter) return counterDamage(battle, player, move, options);
|
|
809
905
|
|
|
810
906
|
battle.last_damage = 0;
|
|
811
907
|
|
|
@@ -827,17 +923,15 @@ fn doMove(
|
|
|
827
923
|
effectiveness = adjustDamage(battle, player);
|
|
828
924
|
immune = effectiveness == 0;
|
|
829
925
|
}
|
|
830
|
-
randomizeDamage(battle);
|
|
926
|
+
try randomizeDamage(battle, player, options);
|
|
831
927
|
}
|
|
832
928
|
}
|
|
833
929
|
|
|
834
|
-
|
|
835
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
836
|
-
|
|
930
|
+
var zero = battle.last_damage == 0;
|
|
837
931
|
miss = if (showdown or skip)
|
|
838
932
|
miss
|
|
839
933
|
else
|
|
840
|
-
(!moveHit(battle, player, move, &immune, &mist) or
|
|
934
|
+
(!moveHit(battle, player, move, &immune, &mist, options) or zero);
|
|
841
935
|
|
|
842
936
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
843
937
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -845,11 +939,11 @@ fn doMove(
|
|
|
845
939
|
|
|
846
940
|
if (!showdown or !miss) {
|
|
847
941
|
if (move.effect == .MirrorMove) {
|
|
848
|
-
return mirrorMove(battle, player, mslot, auto, residual,
|
|
942
|
+
return mirrorMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
849
943
|
} else if (move.effect == .Metronome) {
|
|
850
|
-
return metronome(battle, player, mslot, auto, residual,
|
|
944
|
+
return metronome(battle, player, mslot, rewrap, auto, residual, options);
|
|
851
945
|
} else if (move.effect.onEnd()) {
|
|
852
|
-
try onEnd(battle, player, move,
|
|
946
|
+
try onEnd(battle, player, move, options);
|
|
853
947
|
return null;
|
|
854
948
|
}
|
|
855
949
|
}
|
|
@@ -862,38 +956,46 @@ fn doMove(
|
|
|
862
956
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
863
957
|
if (ohko) {
|
|
864
958
|
try log.immune(foe_ident, .OHKO);
|
|
865
|
-
} else if (immune and !invulnerable) {
|
|
959
|
+
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
866
960
|
try log.immune(foe_ident, .None);
|
|
867
961
|
} else if (mist) {
|
|
868
962
|
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
869
963
|
try log.fail(foe_ident, .None);
|
|
870
964
|
} else {
|
|
965
|
+
if (showdown or !zero) try options.chance.commit(player, .miss);
|
|
871
966
|
try log.lastmiss();
|
|
872
967
|
try log.miss(battle.active(player));
|
|
873
968
|
}
|
|
874
969
|
if (move.effect == .JumpKick) {
|
|
875
970
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
876
|
-
assert(battle.last_damage == 0);
|
|
971
|
+
assert(showdown or battle.last_damage == 0);
|
|
877
972
|
battle.last_damage = 1;
|
|
878
|
-
_ = try applyDamage(battle, player, player.foe(), .None,
|
|
973
|
+
_ = try applyDamage(battle, player, player.foe(), .None, options);
|
|
879
974
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
880
975
|
} else if (move.effect == .Explode) {
|
|
881
976
|
try Effects.explode(battle, player);
|
|
882
|
-
try buildRage(battle, player.foe(),
|
|
977
|
+
try buildRage(battle, player.foe(), options);
|
|
883
978
|
} else if (showdown and move.effect == .Disable) {
|
|
884
|
-
try buildRage(battle, player.foe(),
|
|
979
|
+
try buildRage(battle, player.foe(), options);
|
|
980
|
+
} else if (!showdown and immune and move.effect == .Binding) {
|
|
981
|
+
try options.chance.commit(player, .binding);
|
|
885
982
|
}
|
|
886
983
|
return null;
|
|
984
|
+
} else {
|
|
985
|
+
try options.chance.commit(player, .hit);
|
|
887
986
|
}
|
|
888
987
|
|
|
889
988
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
890
989
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
891
990
|
// it up above before damage calculation).
|
|
892
991
|
if (!showdown and move.effect.isMulti()) {
|
|
893
|
-
Effects.multiHit(battle, player, move);
|
|
992
|
+
try Effects.multiHit(battle, player, move, options);
|
|
894
993
|
hits = side.active.volatiles.attacks;
|
|
895
994
|
}
|
|
896
995
|
|
|
996
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
997
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
998
|
+
|
|
897
999
|
var nullified = false;
|
|
898
1000
|
var hit: u4 = 0;
|
|
899
1001
|
while (hit < hits) {
|
|
@@ -905,11 +1007,13 @@ fn doMove(
|
|
|
905
1007
|
try log.resisted(battle.active(player.foe()));
|
|
906
1008
|
}
|
|
907
1009
|
}
|
|
908
|
-
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None,
|
|
1010
|
+
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
909
1011
|
if (hit == 0 and ohko) try log.ohko();
|
|
910
1012
|
hit += 1;
|
|
911
1013
|
if (foe.stored().hp == 0) break;
|
|
912
|
-
if (!late
|
|
1014
|
+
if (!late and (!sub or move.effect == .Explode)) {
|
|
1015
|
+
try buildRage(battle, player.foe(), options);
|
|
1016
|
+
}
|
|
913
1017
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
914
1018
|
if (nullified) break;
|
|
915
1019
|
}
|
|
@@ -919,17 +1023,23 @@ fn doMove(
|
|
|
919
1023
|
assert(nullified or foe.stored().hp == 0 or side.active.volatiles.attacks - hit == 0);
|
|
920
1024
|
side.active.volatiles.attacks = 0;
|
|
921
1025
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
922
|
-
try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1026
|
+
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
923
1027
|
}
|
|
924
1028
|
try log.hitcount(battle.active(player.foe()), hit);
|
|
925
1029
|
} else if (showdown) {
|
|
926
1030
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
927
1031
|
if (move.effect == .Binding) {
|
|
928
|
-
Effects.binding(battle, player);
|
|
1032
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
929
1033
|
if (immune) {
|
|
930
1034
|
battle.last_damage = 0;
|
|
1035
|
+
assert(foe.stored().hp > 0);
|
|
931
1036
|
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
932
|
-
|
|
1037
|
+
if (sub) {
|
|
1038
|
+
try log.activate(battle.active(player.foe()), .Substitute);
|
|
1039
|
+
} else {
|
|
1040
|
+
try log.damage(battle.active(player.foe()), foe.stored(), .None);
|
|
1041
|
+
try buildRage(battle, player.foe(), options);
|
|
1042
|
+
}
|
|
933
1043
|
return null;
|
|
934
1044
|
}
|
|
935
1045
|
}
|
|
@@ -942,12 +1052,12 @@ fn doMove(
|
|
|
942
1052
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
943
1053
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
944
1054
|
// setup before the loop means we can avoid having to waste time doing no-op handler searches.
|
|
945
|
-
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual,
|
|
1055
|
+
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
946
1056
|
|
|
947
1057
|
if (foe.stored().hp == 0) return null;
|
|
948
1058
|
|
|
949
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
950
|
-
if (late and move.effect != .Disable) try buildRage(battle, player.foe(),
|
|
1059
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
1060
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), options);
|
|
951
1061
|
|
|
952
1062
|
if (!move.effect.isSpecial()) {
|
|
953
1063
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -958,30 +1068,29 @@ fn doMove(
|
|
|
958
1068
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
959
1069
|
// puts the |-status| message before |-hitcount| instead of after.
|
|
960
1070
|
if (move.effect == .Twineedle) {
|
|
961
|
-
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting),
|
|
1071
|
+
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
962
1072
|
} else if (move.effect == .Disable) {
|
|
963
|
-
const result = try Effects.disable(battle, player, move,
|
|
964
|
-
if (showdown) try buildRage(battle, player.foe(),
|
|
1073
|
+
const result = try Effects.disable(battle, player, move, options);
|
|
1074
|
+
if (showdown) try buildRage(battle, player.foe(), options);
|
|
965
1075
|
return result;
|
|
966
1076
|
} else {
|
|
967
|
-
try moveEffect(battle, player, move,
|
|
1077
|
+
try moveEffect(battle, player, move, options);
|
|
968
1078
|
}
|
|
969
1079
|
}
|
|
970
1080
|
|
|
971
1081
|
return null;
|
|
972
1082
|
}
|
|
973
1083
|
|
|
974
|
-
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
1084
|
+
fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
975
1085
|
const side = battle.side(player);
|
|
976
1086
|
|
|
977
1087
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
978
|
-
var
|
|
979
|
-
// GLITCH: Focus Energy reduces critical hit
|
|
980
|
-
|
|
981
|
-
|
|
1088
|
+
var rate = @as(u16, Species.chance(side.stored().species));
|
|
1089
|
+
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1090
|
+
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1091
|
+
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
982
1092
|
|
|
983
|
-
|
|
984
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
1093
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), options);
|
|
985
1094
|
}
|
|
986
1095
|
|
|
987
1096
|
fn calcDamage(
|
|
@@ -1047,7 +1156,7 @@ fn calcDamage(
|
|
|
1047
1156
|
d = @min(997, d);
|
|
1048
1157
|
d += 2;
|
|
1049
1158
|
|
|
1050
|
-
battle.last_damage = @intCast(
|
|
1159
|
+
battle.last_damage = @intCast(d);
|
|
1051
1160
|
|
|
1052
1161
|
return true;
|
|
1053
1162
|
}
|
|
@@ -1061,9 +1170,9 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1061
1170
|
var d = battle.last_damage;
|
|
1062
1171
|
if (side.active.types.includes(move.type)) d +%= d / 2;
|
|
1063
1172
|
|
|
1064
|
-
const neutral = @
|
|
1065
|
-
const eff1: u16 = @
|
|
1066
|
-
const eff2: u16 = @
|
|
1173
|
+
const neutral = @intFromEnum(Effectiveness.Neutral);
|
|
1174
|
+
const eff1: u16 = @intFromEnum(move.type.effectiveness(types.type1));
|
|
1175
|
+
const eff2: u16 = @intFromEnum(move.type.effectiveness(types.type2));
|
|
1067
1176
|
|
|
1068
1177
|
// Type effectiveness matchup precedence only matters with (NVE, SE)
|
|
1069
1178
|
if (!showdown and (eff1 + eff2) == Effectiveness.mismatch and
|
|
@@ -1083,101 +1192,87 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1083
1192
|
return if (types.type1 == types.type2) eff1 * neutral else eff1 * eff2;
|
|
1084
1193
|
}
|
|
1085
1194
|
|
|
1086
|
-
fn randomizeDamage(battle: anytype) void {
|
|
1195
|
+
fn randomizeDamage(battle: anytype, player: Player, options: anytype) !void {
|
|
1087
1196
|
if (battle.last_damage <= 1) return;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
-
if (r >= 217) break :loop r;
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1197
|
+
const random = try Rolls.damage(battle, player, options);
|
|
1198
|
+
options.calc.base(player.foe(), battle.last_damage);
|
|
1199
|
+
battle.last_damage = @intCast(@as(u32, battle.last_damage) *% random / 255);
|
|
1200
|
+
options.calc.final(player.foe(), battle.last_damage);
|
|
1099
1201
|
}
|
|
1100
1202
|
|
|
1101
|
-
fn specialDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1203
|
+
fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1102
1204
|
const side = battle.side(player);
|
|
1205
|
+
const foe = battle.foe(player);
|
|
1103
1206
|
|
|
1104
|
-
if (!try checkHit(battle, player, move,
|
|
1207
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1208
|
+
try options.chance.commit(player, .hit);
|
|
1105
1209
|
|
|
1106
1210
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1107
|
-
.SuperFang => @max(
|
|
1211
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1108
1212
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1109
1213
|
.SonicBoom => 20,
|
|
1110
1214
|
.DragonRage => 40,
|
|
1111
1215
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1112
1216
|
.Psywave => power: {
|
|
1113
|
-
const max = @intCast(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
-
if (max <= 1) return Result.Error;
|
|
1119
|
-
while (true) {
|
|
1120
|
-
const r = battle.rng.next();
|
|
1121
|
-
if (r < max) break :power r;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1217
|
+
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1218
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1219
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1220
|
+
break :power try Rolls.psywave(battle, player, max, options);
|
|
1124
1221
|
},
|
|
1125
1222
|
else => unreachable,
|
|
1126
1223
|
};
|
|
1127
1224
|
|
|
1128
1225
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1129
1226
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1227
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1228
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1229
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1132
1230
|
|
|
1133
1231
|
return null;
|
|
1134
1232
|
}
|
|
1135
1233
|
|
|
1136
|
-
fn counterDamage(battle: anytype, player: Player, move: Move.Data,
|
|
1234
|
+
fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1137
1235
|
const foe = battle.foe(player);
|
|
1138
1236
|
|
|
1139
1237
|
if (battle.last_damage == 0) {
|
|
1140
|
-
try log.fail(battle.active(player), .None);
|
|
1238
|
+
try options.log.fail(battle.active(player), .None);
|
|
1141
1239
|
return null;
|
|
1142
1240
|
}
|
|
1143
1241
|
|
|
1144
|
-
// Pretend
|
|
1145
|
-
const foe_last_used_move =
|
|
1146
|
-
Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
|
|
1242
|
+
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1147
1243
|
const foe_last_selected_move =
|
|
1148
1244
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1149
|
-
.
|
|
1245
|
+
.Splash
|
|
1150
1246
|
else
|
|
1151
1247
|
foe.last_selected_move);
|
|
1152
1248
|
|
|
1153
|
-
const used =
|
|
1154
|
-
foe.last_used_move != .Counter and
|
|
1155
|
-
(foe_last_used_move.type == .Normal or
|
|
1156
|
-
foe_last_used_move.type == .Fighting);
|
|
1157
|
-
|
|
1249
|
+
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1158
1250
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1159
1251
|
foe.last_selected_move != .Counter and
|
|
1160
1252
|
(foe_last_selected_move.type == .Normal or
|
|
1161
1253
|
foe_last_selected_move.type == .Fighting);
|
|
1162
1254
|
|
|
1163
1255
|
if (!used and !selected) {
|
|
1164
|
-
try log.fail(battle.active(player), .None);
|
|
1256
|
+
try options.log.fail(battle.active(player), .None);
|
|
1165
1257
|
return null;
|
|
1166
1258
|
}
|
|
1167
1259
|
|
|
1168
1260
|
if (!used or !selected) {
|
|
1169
1261
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1170
1262
|
if (!showdown) return Result.Error;
|
|
1171
|
-
try log.fail(battle.active(player), .None);
|
|
1263
|
+
try options.log.fail(battle.active(player), .None);
|
|
1172
1264
|
return null;
|
|
1173
1265
|
}
|
|
1174
1266
|
|
|
1175
|
-
battle.last_damage
|
|
1267
|
+
battle.last_damage *|= 2;
|
|
1176
1268
|
|
|
1177
1269
|
// Pokémon Showdown calls checkHit before Counter
|
|
1178
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
1270
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1271
|
+
try options.chance.commit(player, .hit);
|
|
1179
1272
|
|
|
1180
|
-
|
|
1273
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1274
|
+
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1275
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), options);
|
|
1181
1276
|
return null;
|
|
1182
1277
|
}
|
|
1183
1278
|
|
|
@@ -1186,7 +1281,7 @@ fn applyDamage(
|
|
|
1186
1281
|
target_player: Player,
|
|
1187
1282
|
sub_player: Player,
|
|
1188
1283
|
reason: Damage,
|
|
1189
|
-
|
|
1284
|
+
options: anytype,
|
|
1190
1285
|
) !bool {
|
|
1191
1286
|
assert(showdown or battle.last_damage != 0);
|
|
1192
1287
|
|
|
@@ -1201,19 +1296,20 @@ fn applyDamage(
|
|
|
1201
1296
|
subbed.active.volatiles.substitute = 0;
|
|
1202
1297
|
subbed.active.volatiles.Substitute = false;
|
|
1203
1298
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1204
|
-
try log.end(battle.active(sub_player), .Substitute);
|
|
1299
|
+
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1300
|
+
options.calc.capped(target_player);
|
|
1205
1301
|
return true;
|
|
1206
1302
|
} else {
|
|
1207
1303
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1208
|
-
subbed.active.volatiles.substitute -= @intCast(
|
|
1209
|
-
try log.activate(battle.active(sub_player), .Substitute);
|
|
1304
|
+
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1305
|
+
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1210
1306
|
return false;
|
|
1211
1307
|
}
|
|
1212
1308
|
}
|
|
1213
1309
|
|
|
1214
1310
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1215
1311
|
target.stored().hp -= battle.last_damage;
|
|
1216
|
-
try log.damage(battle.active(target_player), target.stored(), reason);
|
|
1312
|
+
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1217
1313
|
return false;
|
|
1218
1314
|
}
|
|
1219
1315
|
|
|
@@ -1221,73 +1317,78 @@ fn mirrorMove(
|
|
|
1221
1317
|
battle: anytype,
|
|
1222
1318
|
player: Player,
|
|
1223
1319
|
mslot: u4,
|
|
1320
|
+
rewrap: bool,
|
|
1224
1321
|
auto: bool,
|
|
1225
1322
|
residual: *bool,
|
|
1226
|
-
|
|
1227
|
-
) !?Result {
|
|
1323
|
+
options: anytype,
|
|
1324
|
+
) anyerror!?Result {
|
|
1228
1325
|
var side = battle.side(player);
|
|
1229
1326
|
const foe = battle.foe(player);
|
|
1230
1327
|
|
|
1231
1328
|
side.last_selected_move = foe.last_used_move;
|
|
1232
1329
|
|
|
1233
1330
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1234
|
-
try log.fail(battle.active(player), .None);
|
|
1331
|
+
try options.log.fail(battle.active(player), .None);
|
|
1235
1332
|
return null;
|
|
1333
|
+
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1334
|
+
incrementPP(side, mslot);
|
|
1236
1335
|
}
|
|
1237
1336
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
return doMove(battle, player, mslot, auto, residual,
|
|
1337
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1242
1341
|
}
|
|
1243
1342
|
|
|
1244
1343
|
fn metronome(
|
|
1245
1344
|
battle: anytype,
|
|
1246
1345
|
player: Player,
|
|
1247
1346
|
mslot: u4,
|
|
1347
|
+
rewrap: bool,
|
|
1248
1348
|
auto: bool,
|
|
1249
1349
|
residual: *bool,
|
|
1250
|
-
|
|
1251
|
-
) !?Result {
|
|
1350
|
+
options: anytype,
|
|
1351
|
+
) anyerror!?Result {
|
|
1252
1352
|
var side = battle.side(player);
|
|
1253
1353
|
|
|
1254
|
-
side.last_selected_move =
|
|
1255
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
-
} else loop: {
|
|
1259
|
-
while (true) {
|
|
1260
|
-
const r = battle.rng.next();
|
|
1261
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
-
break :loop @intToEnum(Move, r);
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
|
|
1354
|
+
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1267
1355
|
incrementPP(side, mslot);
|
|
1268
1356
|
|
|
1269
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual,
|
|
1270
|
-
|
|
1357
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
1271
1361
|
}
|
|
1272
1362
|
|
|
1273
|
-
fn checkHit(battle: anytype, player: Player, move: Move.Data,
|
|
1363
|
+
fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !bool {
|
|
1274
1364
|
var immune = false;
|
|
1275
1365
|
var mist = false;
|
|
1276
|
-
|
|
1366
|
+
|
|
1367
|
+
if (moveHit(battle, player, move, &immune, &mist, options)) return true;
|
|
1368
|
+
|
|
1277
1369
|
assert(!immune);
|
|
1278
1370
|
if (mist) {
|
|
1279
1371
|
assert(!showdown);
|
|
1280
1372
|
const foe_ident = battle.active(player.foe());
|
|
1281
|
-
try log.activate(foe_ident, .Mist);
|
|
1282
|
-
try log.fail(foe_ident, .None);
|
|
1373
|
+
try options.log.activate(foe_ident, .Mist);
|
|
1374
|
+
try options.log.fail(foe_ident, .None);
|
|
1283
1375
|
} else {
|
|
1284
|
-
try
|
|
1285
|
-
try log.
|
|
1376
|
+
try options.chance.commit(player, .miss);
|
|
1377
|
+
try options.log.lastmiss();
|
|
1378
|
+
try options.log.miss(battle.active(player));
|
|
1286
1379
|
}
|
|
1380
|
+
|
|
1287
1381
|
return false;
|
|
1288
1382
|
}
|
|
1289
1383
|
|
|
1290
|
-
fn moveHit(
|
|
1384
|
+
fn moveHit(
|
|
1385
|
+
battle: anytype,
|
|
1386
|
+
player: Player,
|
|
1387
|
+
move: Move.Data,
|
|
1388
|
+
immune: *bool,
|
|
1389
|
+
mist: *bool,
|
|
1390
|
+
options: anytype,
|
|
1391
|
+
) bool {
|
|
1291
1392
|
var side = battle.side(player);
|
|
1292
1393
|
const foe = battle.foe(player);
|
|
1293
1394
|
|
|
@@ -1321,23 +1422,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1321
1422
|
}
|
|
1322
1423
|
|
|
1323
1424
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1425
|
+
const state = side.active.volatiles.state;
|
|
1426
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1427
|
+
const overwritten = overwrite and state > 0;
|
|
1428
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1429
|
+
|
|
1430
|
+
var accuracy = if (overwritten) state else @as(u16, move.accuracy);
|
|
1431
|
+
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1329
1432
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1330
|
-
boost = BOOSTS[@
|
|
1433
|
+
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
1331
1434
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1332
1435
|
accuracy = @min(255, @max(1, accuracy));
|
|
1333
1436
|
|
|
1334
|
-
|
|
1437
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1438
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1439
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1335
1440
|
|
|
1336
1441
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1337
|
-
break :miss
|
|
1338
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
-
else
|
|
1340
|
-
battle.rng.next() >= accuracy;
|
|
1442
|
+
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1341
1443
|
};
|
|
1342
1444
|
|
|
1343
1445
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1347,7 +1449,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1347
1449
|
}
|
|
1348
1450
|
|
|
1349
1451
|
if (!miss) return true;
|
|
1350
|
-
battle.last_damage = 0;
|
|
1452
|
+
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1351
1453
|
side.active.volatiles.Binding = false;
|
|
1352
1454
|
return false;
|
|
1353
1455
|
}
|
|
@@ -1355,8 +1457,8 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1355
1457
|
fn checkFaint(
|
|
1356
1458
|
battle: anytype,
|
|
1357
1459
|
player: Player,
|
|
1358
|
-
|
|
1359
|
-
) @TypeOf(log).Error!?Result {
|
|
1460
|
+
options: anytype,
|
|
1461
|
+
) @TypeOf(options.log).Error!?Result {
|
|
1360
1462
|
const side = battle.side(player);
|
|
1361
1463
|
if (side.stored().hp > 0) return null;
|
|
1362
1464
|
|
|
@@ -1368,29 +1470,29 @@ fn checkFaint(
|
|
|
1368
1470
|
const tie = player_out and foe_out;
|
|
1369
1471
|
const more = tie or player_out or foe_out;
|
|
1370
1472
|
|
|
1371
|
-
if (try faint(battle, player,
|
|
1372
|
-
if (foe_fainted) if (try faint(battle, player.foe(),
|
|
1473
|
+
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1474
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1373
1475
|
|
|
1374
1476
|
assert(!side.active.volatiles.MultiHit);
|
|
1375
1477
|
assert(!foe.active.volatiles.MultiHit);
|
|
1376
1478
|
|
|
1377
1479
|
if (tie) {
|
|
1378
|
-
try log.tie();
|
|
1480
|
+
try options.log.tie();
|
|
1379
1481
|
return Result.Tie;
|
|
1380
1482
|
} else if (player_out) {
|
|
1381
|
-
try log.win(player.foe());
|
|
1483
|
+
try options.log.win(player.foe());
|
|
1382
1484
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1383
1485
|
} else if (foe_out) {
|
|
1384
|
-
try log.win(player);
|
|
1486
|
+
try options.log.win(player);
|
|
1385
1487
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1386
1488
|
}
|
|
1387
1489
|
|
|
1388
1490
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
1389
|
-
if (player == .P1) return
|
|
1390
|
-
return
|
|
1491
|
+
if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
|
|
1492
|
+
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1391
1493
|
}
|
|
1392
1494
|
|
|
1393
|
-
fn faint(battle: anytype, player: Player,
|
|
1495
|
+
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1394
1496
|
var side = battle.side(player);
|
|
1395
1497
|
var foe = battle.foe(player);
|
|
1396
1498
|
assert(side.stored().hp == 0);
|
|
@@ -1423,32 +1525,31 @@ fn faint(battle: anytype, player: Player, log: anytype, done: bool) !?Result {
|
|
|
1423
1525
|
side.stored().stats.spe;
|
|
1424
1526
|
}
|
|
1425
1527
|
|
|
1426
|
-
try log.faint(battle.active(player), done);
|
|
1528
|
+
try options.log.faint(battle.active(player), done);
|
|
1529
|
+
options.calc.capped(player);
|
|
1427
1530
|
return null;
|
|
1428
1531
|
}
|
|
1429
1532
|
|
|
1430
|
-
fn handleResidual(battle: anytype, player: Player,
|
|
1533
|
+
fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
1431
1534
|
var side = battle.side(player);
|
|
1432
1535
|
var stored = side.stored();
|
|
1433
1536
|
const ident = battle.active(player);
|
|
1434
1537
|
var volatiles = &side.active.volatiles;
|
|
1435
1538
|
|
|
1436
1539
|
const brn = Status.is(stored.status, .BRN);
|
|
1437
|
-
if (brn or Status.is(stored.status, .PSN))
|
|
1540
|
+
if (brn or Status.is(stored.status, .PSN)) {
|
|
1438
1541
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1439
|
-
|
|
1440
1542
|
if (volatiles.Toxic) {
|
|
1441
1543
|
volatiles.toxic += 1;
|
|
1442
1544
|
damage *= volatiles.toxic;
|
|
1443
1545
|
}
|
|
1546
|
+
damage = @min(damage, stored.hp);
|
|
1444
1547
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1451
|
-
try log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1548
|
+
if (!showdown or damage > 0) {
|
|
1549
|
+
stored.hp -= damage;
|
|
1550
|
+
// Pokémon Showdown uses damageOf here but its not relevant in Generation I
|
|
1551
|
+
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1552
|
+
}
|
|
1452
1553
|
}
|
|
1453
1554
|
|
|
1454
1555
|
if (volatiles.LeechSeed) {
|
|
@@ -1469,49 +1570,46 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1469
1570
|
damage *= volatiles.toxic;
|
|
1470
1571
|
}
|
|
1471
1572
|
|
|
1472
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
-
if (showdown) battle.last_damage = damage;
|
|
1474
|
-
|
|
1475
1573
|
const amount = @min(damage, stored.hp);
|
|
1476
1574
|
stored.hp -= amount;
|
|
1477
1575
|
|
|
1478
1576
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1479
|
-
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1577
|
+
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1480
1578
|
|
|
1481
1579
|
const before = foe_stored.hp;
|
|
1482
1580
|
// Uncapped damage is added back to the foe
|
|
1483
1581
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1484
1582
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1485
|
-
if (foe_stored.hp > before) try log.heal(foe_ident, foe_stored, .Silent);
|
|
1583
|
+
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1486
1584
|
}
|
|
1487
1585
|
}
|
|
1488
1586
|
|
|
1489
|
-
fn endTurn(battle: anytype,
|
|
1587
|
+
fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result {
|
|
1490
1588
|
assert(!battle.side(.P1).active.volatiles.MultiHit);
|
|
1491
1589
|
assert(!battle.side(.P2).active.volatiles.MultiHit);
|
|
1492
1590
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1591
|
+
battle.turn += 1;
|
|
1592
|
+
|
|
1593
|
+
if (showdown and pkmn.options.ebc and checkEBC(battle)) {
|
|
1594
|
+
try options.log.tie();
|
|
1495
1595
|
return Result.Tie;
|
|
1496
1596
|
}
|
|
1497
1597
|
|
|
1498
|
-
battle.turn += 1;
|
|
1499
|
-
|
|
1500
1598
|
if (showdown and battle.turn >= 1000) {
|
|
1501
|
-
try log.tie();
|
|
1599
|
+
try options.log.tie();
|
|
1502
1600
|
return Result.Tie;
|
|
1503
1601
|
} else if (battle.turn >= 65535) {
|
|
1504
1602
|
return Result.Error;
|
|
1505
1603
|
}
|
|
1506
1604
|
|
|
1507
|
-
try log.turn(battle.turn);
|
|
1605
|
+
try options.log.turn(battle.turn);
|
|
1508
1606
|
|
|
1509
1607
|
return Result.Default;
|
|
1510
1608
|
}
|
|
1511
1609
|
|
|
1512
1610
|
fn checkEBC(battle: anytype) bool {
|
|
1513
1611
|
for (battle.sides, 0..) |side, i| {
|
|
1514
|
-
const foe = battle.sides[~@
|
|
1612
|
+
const foe = battle.sides[~@as(u1, @intCast(i))];
|
|
1515
1613
|
|
|
1516
1614
|
var foe_all_ghosts = true;
|
|
1517
1615
|
var foe_all_transform = true;
|
|
@@ -1563,48 +1661,84 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1563
1661
|
return false;
|
|
1564
1662
|
}
|
|
1565
1663
|
|
|
1664
|
+
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1665
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1666
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1667
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1668
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1669
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1670
|
+
m
|
|
1671
|
+
else
|
|
1672
|
+
side.last_selected_move;
|
|
1673
|
+
if (m != .None and m == last) {
|
|
1674
|
+
side.active.volatiles.Charging = false;
|
|
1675
|
+
try options.log.disabled(ident, last);
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1683
|
+
const side = battle.side(who);
|
|
1684
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1685
|
+
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1690
|
+
var volatiles = &active.volatiles;
|
|
1691
|
+
assert(volatiles.Thrashing);
|
|
1692
|
+
if (volatiles.attacks > 0) return false;
|
|
1693
|
+
|
|
1694
|
+
volatiles.Thrashing = false;
|
|
1695
|
+
volatiles.Confusion = true;
|
|
1696
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1566
1700
|
inline fn onBegin(
|
|
1567
1701
|
battle: anytype,
|
|
1568
1702
|
player: Player,
|
|
1569
1703
|
move: Move.Data,
|
|
1570
1704
|
mslot: u8,
|
|
1571
1705
|
residual: *bool,
|
|
1572
|
-
|
|
1706
|
+
options: anytype,
|
|
1573
1707
|
) !void {
|
|
1574
1708
|
assert(move.effect.onBegin());
|
|
1575
1709
|
return switch (move.effect) {
|
|
1576
|
-
.Confusion => Effects.confusion(battle, player, move,
|
|
1577
|
-
.Conversion => Effects.conversion(battle, player,
|
|
1578
|
-
.FocusEnergy => Effects.focusEnergy(battle, player,
|
|
1579
|
-
.Haze => Effects.haze(battle, player,
|
|
1580
|
-
.Heal => Effects.heal(battle, player,
|
|
1581
|
-
.LeechSeed => Effects.leechSeed(battle, player, move,
|
|
1582
|
-
.LightScreen => Effects.lightScreen(battle, player,
|
|
1583
|
-
.Mimic => Effects.mimic(battle, player, move, mslot,
|
|
1584
|
-
.Mist => Effects.mist(battle, player,
|
|
1585
|
-
.Paralyze => Effects.paralyze(battle, player, move,
|
|
1586
|
-
.Poison => Effects.poison(battle, player, move,
|
|
1587
|
-
.Reflect => Effects.reflect(battle, player,
|
|
1588
|
-
.Splash => Effects.splash(battle, player,
|
|
1589
|
-
.Substitute => Effects.substitute(battle, player, residual,
|
|
1590
|
-
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move,
|
|
1591
|
-
.Transform => Effects.transform(battle, player,
|
|
1710
|
+
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1711
|
+
.Conversion => Effects.conversion(battle, player, options),
|
|
1712
|
+
.FocusEnergy => Effects.focusEnergy(battle, player, options),
|
|
1713
|
+
.Haze => Effects.haze(battle, player, options),
|
|
1714
|
+
.Heal => Effects.heal(battle, player, options),
|
|
1715
|
+
.LeechSeed => Effects.leechSeed(battle, player, move, options),
|
|
1716
|
+
.LightScreen => Effects.lightScreen(battle, player, options),
|
|
1717
|
+
.Mimic => Effects.mimic(battle, player, move, mslot, options),
|
|
1718
|
+
.Mist => Effects.mist(battle, player, options),
|
|
1719
|
+
.Paralyze => Effects.paralyze(battle, player, move, options),
|
|
1720
|
+
.Poison => Effects.poison(battle, player, move, options),
|
|
1721
|
+
.Reflect => Effects.reflect(battle, player, options),
|
|
1722
|
+
.Splash => Effects.splash(battle, player, options),
|
|
1723
|
+
.Substitute => Effects.substitute(battle, player, residual, options),
|
|
1724
|
+
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, options),
|
|
1725
|
+
.Transform => Effects.transform(battle, player, options),
|
|
1592
1726
|
else => unreachable,
|
|
1593
1727
|
};
|
|
1594
1728
|
}
|
|
1595
1729
|
|
|
1596
|
-
inline fn onEnd(battle: anytype, player: Player, move: Move.Data,
|
|
1730
|
+
inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1597
1731
|
assert(move.effect.onEnd());
|
|
1598
1732
|
return switch (move.effect) {
|
|
1599
|
-
.Bide => Effects.bide(battle, player,
|
|
1733
|
+
.Bide => Effects.bide(battle, player, options),
|
|
1600
1734
|
// zig fmt: off
|
|
1601
1735
|
.AttackUp1, .AttackUp2, .DefenseUp1, .DefenseUp2,
|
|
1602
1736
|
.EvasionUp1, .SpecialUp1, .SpecialUp2, .SpeedUp2 =>
|
|
1603
|
-
Effects.boost(battle, player, move,
|
|
1737
|
+
Effects.boost(battle, player, move, options),
|
|
1604
1738
|
.AccuracyDown1, .AttackDown1, .DefenseDown1, .DefenseDown2, .SpeedDown1 =>
|
|
1605
|
-
Effects.unboost(battle, player, move,
|
|
1739
|
+
Effects.unboost(battle, player, move, options),
|
|
1606
1740
|
// zig fmt: on
|
|
1607
|
-
.Sleep => Effects.sleep(battle, player, move,
|
|
1741
|
+
.Sleep => Effects.sleep(battle, player, move, options),
|
|
1608
1742
|
else => unreachable,
|
|
1609
1743
|
};
|
|
1610
1744
|
}
|
|
@@ -1614,54 +1748,51 @@ inline fn alwaysHappens(
|
|
|
1614
1748
|
player: Player,
|
|
1615
1749
|
move: Move.Data,
|
|
1616
1750
|
residual: *bool,
|
|
1617
|
-
|
|
1751
|
+
options: anytype,
|
|
1618
1752
|
) !void {
|
|
1619
1753
|
assert(move.effect.alwaysHappens());
|
|
1620
1754
|
return switch (move.effect) {
|
|
1621
|
-
.DrainHP, .DreamEater => Effects.drainHP(battle, player,
|
|
1755
|
+
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1622
1756
|
.Explode => Effects.explode(battle, player),
|
|
1623
|
-
.PayDay => Effects.payDay(battle, player,
|
|
1757
|
+
.PayDay => Effects.payDay(battle, player, options),
|
|
1624
1758
|
.Rage => Effects.rage(battle, player),
|
|
1625
|
-
.Recoil => Effects.recoil(battle, player, residual,
|
|
1759
|
+
.Recoil => Effects.recoil(battle, player, residual, options),
|
|
1626
1760
|
.JumpKick, .Binding => {},
|
|
1627
1761
|
else => unreachable,
|
|
1628
1762
|
};
|
|
1629
1763
|
}
|
|
1630
1764
|
|
|
1631
|
-
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data,
|
|
1765
|
+
inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1632
1766
|
return switch (move.effect) {
|
|
1633
|
-
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move,
|
|
1634
|
-
.ConfusionChance => Effects.confusion(battle, player, move,
|
|
1635
|
-
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move),
|
|
1636
|
-
.FreezeChance => Effects.freezeChance(battle, player, move,
|
|
1637
|
-
.HyperBeam => Effects.hyperBeam(battle, player,
|
|
1767
|
+
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1768
|
+
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
1769
|
+
.FlinchChance1, .FlinchChance2 => Effects.flinchChance(battle, player, move, options),
|
|
1770
|
+
.FreezeChance => Effects.freezeChance(battle, player, move, options),
|
|
1771
|
+
.HyperBeam => Effects.hyperBeam(battle, player, options),
|
|
1638
1772
|
.MultiHit, .DoubleHit, .Twineedle => unreachable,
|
|
1639
|
-
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move,
|
|
1640
|
-
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move,
|
|
1773
|
+
.ParalyzeChance1, .ParalyzeChance2 => Effects.paralyzeChance(battle, player, move, options),
|
|
1774
|
+
.PoisonChance1, .PoisonChance2 => Effects.poison(battle, player, move, options),
|
|
1641
1775
|
// zig fmt: off
|
|
1642
1776
|
.AttackDownChance, .DefenseDownChance, .SpecialDownChance, .SpeedDownChance =>
|
|
1643
|
-
Effects.unboost(battle, player, move,
|
|
1777
|
+
Effects.unboost(battle, player, move, options),
|
|
1644
1778
|
// zig fmt: on
|
|
1645
1779
|
else => {},
|
|
1646
1780
|
};
|
|
1647
1781
|
}
|
|
1648
1782
|
|
|
1649
1783
|
pub const Effects = struct {
|
|
1650
|
-
fn bide(battle: anytype, player: Player,
|
|
1784
|
+
fn bide(battle: anytype, player: Player, options: anytype) !void {
|
|
1651
1785
|
var side = battle.side(player);
|
|
1652
1786
|
|
|
1653
1787
|
side.active.volatiles.Bide = true;
|
|
1654
1788
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1655
1789
|
side.active.volatiles.state = 0;
|
|
1656
|
-
side.active.volatiles.attacks =
|
|
1657
|
-
battle.rng.range(u4, 2, 4)
|
|
1658
|
-
else
|
|
1659
|
-
(battle.rng.next() & 1) + 2);
|
|
1790
|
+
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1660
1791
|
|
|
1661
|
-
try log.start(battle.active(player), .Bide);
|
|
1792
|
+
try options.log.start(battle.active(player), .Bide);
|
|
1662
1793
|
}
|
|
1663
1794
|
|
|
1664
|
-
fn burnChance(battle: anytype, player: Player, move: Move.Data,
|
|
1795
|
+
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1665
1796
|
var foe = battle.foe(player);
|
|
1666
1797
|
var foe_stored = foe.stored();
|
|
1667
1798
|
|
|
@@ -1672,96 +1803,97 @@ pub const Effects = struct {
|
|
|
1672
1803
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1673
1804
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1674
1805
|
assert(move.type == .Fire);
|
|
1675
|
-
try log.curestatus(
|
|
1806
|
+
try options.log.curestatus(
|
|
1807
|
+
battle.active(player.foe()),
|
|
1808
|
+
foe_stored.status,
|
|
1809
|
+
.Message,
|
|
1810
|
+
);
|
|
1676
1811
|
foe_stored.status = 0;
|
|
1677
1812
|
}
|
|
1678
1813
|
return;
|
|
1679
1814
|
}
|
|
1680
1815
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
Gen12.percent(10)
|
|
1686
|
-
else
|
|
1687
|
-
Gen12.percent(30));
|
|
1688
|
-
if (!chance) return;
|
|
1816
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1817
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .BurnChance1, options)) {
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1689
1820
|
|
|
1690
1821
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1822
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1692
1823
|
|
|
1693
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1824
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1694
1825
|
}
|
|
1695
1826
|
|
|
1696
|
-
fn charge(battle: anytype, player: Player,
|
|
1827
|
+
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
1697
1828
|
var side = battle.side(player);
|
|
1698
1829
|
var volatiles = &side.active.volatiles;
|
|
1699
1830
|
|
|
1700
1831
|
volatiles.Charging = true;
|
|
1701
1832
|
const move = side.last_selected_move;
|
|
1702
1833
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1703
|
-
try log.laststill();
|
|
1704
|
-
try log.prepare(battle.active(player), move);
|
|
1834
|
+
try options.log.laststill();
|
|
1835
|
+
try options.log.prepare(battle.active(player), move);
|
|
1705
1836
|
}
|
|
1706
1837
|
|
|
1707
|
-
fn confusion(battle: anytype, player: Player, move: Move.Data,
|
|
1838
|
+
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1708
1839
|
var foe = battle.foe(player);
|
|
1709
1840
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1841
|
|
|
1711
1842
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
|
|
1713
|
-
battle.rng.chance(u8, 25, 256)
|
|
1714
|
-
else
|
|
1715
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
-
if (!chance) return;
|
|
1843
|
+
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1717
1844
|
} else {
|
|
1718
1845
|
if (showdown) {
|
|
1719
|
-
if (!try checkHit(battle, player, move,
|
|
1846
|
+
if (!try checkHit(battle, player, move, options)) {
|
|
1720
1847
|
return;
|
|
1721
1848
|
} else if (sub) {
|
|
1722
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1849
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1723
1850
|
}
|
|
1724
1851
|
} else {
|
|
1725
1852
|
if (sub) {
|
|
1726
|
-
return log.fail(battle.active(player.foe()), .None);
|
|
1727
|
-
} else if (!try checkHit(battle, player, move,
|
|
1853
|
+
return options.log.fail(battle.active(player.foe()), .None);
|
|
1854
|
+
} else if (!try checkHit(battle, player, move, options)) {
|
|
1728
1855
|
return;
|
|
1729
1856
|
}
|
|
1730
1857
|
}
|
|
1858
|
+
try options.chance.commit(player, .hit);
|
|
1731
1859
|
}
|
|
1732
1860
|
|
|
1733
1861
|
if (foe.active.volatiles.Confusion) return;
|
|
1734
1862
|
foe.active.volatiles.Confusion = true;
|
|
1735
|
-
foe.active.volatiles.confusion =
|
|
1736
|
-
battle.rng.range(u8, 2, 6)
|
|
1737
|
-
else
|
|
1738
|
-
(battle.rng.next() & 3) + 2);
|
|
1863
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1739
1864
|
|
|
1740
|
-
try log.start(battle.active(player.foe()), .Confusion);
|
|
1865
|
+
try options.log.start(battle.active(player.foe()), .Confusion);
|
|
1741
1866
|
}
|
|
1742
1867
|
|
|
1743
|
-
fn conversion(battle: anytype, player: Player,
|
|
1868
|
+
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1744
1869
|
const foe = battle.foe(player);
|
|
1745
1870
|
|
|
1746
1871
|
if (foe.active.volatiles.Invulnerable) {
|
|
1747
|
-
try log.lastmiss();
|
|
1748
|
-
return log.miss(battle.active(player));
|
|
1872
|
+
try options.log.lastmiss();
|
|
1873
|
+
return options.log.miss(battle.active(player));
|
|
1749
1874
|
}
|
|
1750
1875
|
|
|
1751
1876
|
battle.side(player).active.types = foe.active.types;
|
|
1752
|
-
return log.typechange(
|
|
1877
|
+
return options.log.typechange(
|
|
1878
|
+
battle.active(player),
|
|
1879
|
+
foe.active.types,
|
|
1880
|
+
battle.active(player.foe()),
|
|
1881
|
+
);
|
|
1753
1882
|
}
|
|
1754
1883
|
|
|
1755
|
-
fn disable(battle: anytype, player: Player, move: Move.Data,
|
|
1884
|
+
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
1756
1885
|
var foe = battle.foe(player);
|
|
1757
1886
|
var volatiles = &foe.active.volatiles;
|
|
1758
1887
|
const foe_ident = battle.active(player.foe());
|
|
1759
1888
|
|
|
1760
1889
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1761
|
-
if (!showdown
|
|
1890
|
+
if (!showdown) {
|
|
1891
|
+
if (!try checkHit(battle, player, move, options)) return null;
|
|
1892
|
+
try options.chance.commit(player, .hit);
|
|
1893
|
+
}
|
|
1762
1894
|
|
|
1763
1895
|
if (volatiles.disabled_move != 0) {
|
|
1764
|
-
try log.fail(foe_ident, .None);
|
|
1896
|
+
try options.log.fail(foe_ident, .None);
|
|
1765
1897
|
return null;
|
|
1766
1898
|
}
|
|
1767
1899
|
|
|
@@ -1777,7 +1909,7 @@ pub const Effects = struct {
|
|
|
1777
1909
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1778
1910
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1779
1911
|
if (n == 0) {
|
|
1780
|
-
try log.fail(foe_ident, .None);
|
|
1912
|
+
try options.log.fail(foe_ident, .None);
|
|
1781
1913
|
return null;
|
|
1782
1914
|
} else if (err) {
|
|
1783
1915
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
@@ -1785,23 +1917,22 @@ pub const Effects = struct {
|
|
|
1785
1917
|
return Result.Error;
|
|
1786
1918
|
}
|
|
1787
1919
|
|
|
1788
|
-
volatiles.disabled_move =
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
else
|
|
1792
|
-
(battle.rng.next() & 7) + 1);
|
|
1920
|
+
volatiles.disabled_move =
|
|
1921
|
+
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1922
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
|
|
1793
1923
|
|
|
1794
|
-
|
|
1924
|
+
const id = foe.active.move(volatiles.disabled_move).id;
|
|
1925
|
+
try options.log.startEffect(foe_ident, .Disable, id);
|
|
1795
1926
|
return null;
|
|
1796
1927
|
}
|
|
1797
1928
|
|
|
1798
|
-
fn drainHP(battle: anytype, player: Player,
|
|
1929
|
+
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1799
1930
|
var side = battle.side(player);
|
|
1800
1931
|
var stored = side.stored();
|
|
1801
1932
|
|
|
1802
1933
|
if (battle.last_damage == 0) {
|
|
1803
1934
|
assert(showdown);
|
|
1804
|
-
return;
|
|
1935
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1805
1936
|
}
|
|
1806
1937
|
|
|
1807
1938
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1810,7 +1941,7 @@ pub const Effects = struct {
|
|
|
1810
1941
|
if (stored.hp == stored.stats.hp) return;
|
|
1811
1942
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1812
1943
|
|
|
1813
|
-
try log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1944
|
+
try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
|
|
1814
1945
|
}
|
|
1815
1946
|
|
|
1816
1947
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1824,34 +1955,28 @@ pub const Effects = struct {
|
|
|
1824
1955
|
side.active.volatiles.LeechSeed = false;
|
|
1825
1956
|
}
|
|
1826
1957
|
|
|
1827
|
-
fn flinchChance(battle: anytype, player: Player, move: Move.Data) void {
|
|
1958
|
+
fn flinchChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1828
1959
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1829
1960
|
|
|
1830
1961
|
if (volatiles.Substitute) return;
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
else
|
|
1835
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
-
Gen12.percent(10)
|
|
1837
|
-
else
|
|
1838
|
-
Gen12.percent(30));
|
|
1839
|
-
if (!chance) return;
|
|
1962
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .FlinchChance1, options)) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1840
1965
|
|
|
1841
1966
|
volatiles.Flinch = true;
|
|
1842
1967
|
volatiles.Recharging = false;
|
|
1843
1968
|
}
|
|
1844
1969
|
|
|
1845
|
-
fn focusEnergy(battle: anytype, player: Player,
|
|
1970
|
+
fn focusEnergy(battle: anytype, player: Player, options: anytype) !void {
|
|
1846
1971
|
var side = battle.side(player);
|
|
1847
1972
|
|
|
1848
1973
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1849
1974
|
side.active.volatiles.FocusEnergy = true;
|
|
1850
1975
|
|
|
1851
|
-
try log.start(battle.active(player), .FocusEnergy);
|
|
1976
|
+
try options.log.start(battle.active(player), .FocusEnergy);
|
|
1852
1977
|
}
|
|
1853
1978
|
|
|
1854
|
-
fn freezeChance(battle: anytype, player: Player, move: Move.Data,
|
|
1979
|
+
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1855
1980
|
var foe = battle.foe(player);
|
|
1856
1981
|
var foe_stored = foe.stored();
|
|
1857
1982
|
const foe_ident = battle.active(player.foe());
|
|
@@ -1862,22 +1987,19 @@ pub const Effects = struct {
|
|
|
1862
1987
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1863
1988
|
}
|
|
1864
1989
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
else
|
|
1868
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
-
if (!chance) return;
|
|
1870
|
-
|
|
1990
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1991
|
+
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1871
1992
|
// Freeze Clause Mod
|
|
1872
1993
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1873
1994
|
|
|
1874
1995
|
foe_stored.status = Status.init(.FRZ);
|
|
1875
1996
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1876
1997
|
|
|
1877
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
1998
|
+
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
1878
1999
|
}
|
|
1879
2000
|
|
|
1880
|
-
fn haze(battle: anytype, player: Player,
|
|
2001
|
+
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
2002
|
+
var log = options.log;
|
|
1881
2003
|
var side = battle.side(player);
|
|
1882
2004
|
var foe = battle.foe(player);
|
|
1883
2005
|
|
|
@@ -1899,7 +2021,7 @@ pub const Effects = struct {
|
|
|
1899
2021
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1900
2022
|
if (showdown) {
|
|
1901
2023
|
for (&battle.sides, 0..) |*s, i| {
|
|
1902
|
-
const p = @
|
|
2024
|
+
const p: Player = @enumFromInt(i);
|
|
1903
2025
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1904
2026
|
if (p != player and Status.any(s.stored().status)) {
|
|
1905
2027
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
@@ -1908,7 +2030,7 @@ pub const Effects = struct {
|
|
|
1908
2030
|
s.stored().status = Status.init(.PSN);
|
|
1909
2031
|
try log.status(battle.active(p), s.stored().status, .None);
|
|
1910
2032
|
}
|
|
1911
|
-
try clearVolatiles(battle, p,
|
|
2033
|
+
try clearVolatiles(battle, p, p != player, options);
|
|
1912
2034
|
}
|
|
1913
2035
|
} else {
|
|
1914
2036
|
if (Status.any(foe_stored.status)) {
|
|
@@ -1918,71 +2040,76 @@ pub const Effects = struct {
|
|
|
1918
2040
|
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
1919
2041
|
foe_stored.status = 0;
|
|
1920
2042
|
}
|
|
1921
|
-
try clearVolatiles(battle, player,
|
|
1922
|
-
try clearVolatiles(battle, player.foe(),
|
|
2043
|
+
try clearVolatiles(battle, player, false, options);
|
|
2044
|
+
try clearVolatiles(battle, player.foe(), true, options);
|
|
1923
2045
|
}
|
|
1924
2046
|
}
|
|
1925
2047
|
|
|
1926
|
-
fn heal(battle: anytype, player: Player,
|
|
2048
|
+
fn heal(battle: anytype, player: Player, options: anytype) !void {
|
|
1927
2049
|
var side = battle.side(player);
|
|
1928
2050
|
var stored = side.stored();
|
|
1929
2051
|
const ident = battle.active(player);
|
|
1930
2052
|
|
|
1931
2053
|
// GLITCH: HP recovery move failure glitches
|
|
1932
2054
|
const delta = stored.stats.hp - stored.hp;
|
|
1933
|
-
if (delta == 0 or delta & 255 == 255
|
|
2055
|
+
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2056
|
+
return try options.log.fail(ident, .None);
|
|
2057
|
+
}
|
|
1934
2058
|
|
|
1935
2059
|
const rest = side.last_selected_move == .Rest;
|
|
1936
2060
|
if (rest) {
|
|
1937
2061
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
1938
2062
|
if (showdown) battle.rng.advance(1);
|
|
1939
2063
|
stored.status = Status.slf(2);
|
|
1940
|
-
try log.statusFrom(ident, stored.status, Move.Rest);
|
|
2064
|
+
try options.log.statusFrom(ident, stored.status, Move.Rest);
|
|
1941
2065
|
stored.hp = stored.stats.hp;
|
|
1942
2066
|
} else {
|
|
1943
2067
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
1944
2068
|
}
|
|
1945
|
-
try log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2069
|
+
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
1946
2070
|
}
|
|
1947
2071
|
|
|
1948
|
-
fn hyperBeam(battle: anytype, player: Player,
|
|
2072
|
+
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
1949
2073
|
battle.side(player).active.volatiles.Recharging = true;
|
|
1950
|
-
try log.mustrecharge(battle.active(player));
|
|
2074
|
+
try options.log.mustrecharge(battle.active(player));
|
|
1951
2075
|
}
|
|
1952
2076
|
|
|
1953
|
-
fn leechSeed(battle: anytype, player: Player, move: Move.Data,
|
|
2077
|
+
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1954
2078
|
var foe = battle.foe(player);
|
|
1955
2079
|
|
|
1956
2080
|
if (showdown) {
|
|
1957
2081
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
1958
2082
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
1959
|
-
return log.immune(battle.active(player.foe()), .None);
|
|
2083
|
+
return options.log.immune(battle.active(player.foe()), .None);
|
|
1960
2084
|
}
|
|
1961
|
-
if (!try checkHit(battle, player, move,
|
|
2085
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1962
2086
|
if (foe.active.volatiles.LeechSeed) return;
|
|
1963
2087
|
} else {
|
|
1964
|
-
if (!try checkHit(battle, player, move,
|
|
2088
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1965
2089
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
1966
|
-
try log.lastmiss();
|
|
1967
|
-
return log.miss(battle.active(player));
|
|
2090
|
+
try options.log.lastmiss();
|
|
2091
|
+
return options.log.miss(battle.active(player));
|
|
1968
2092
|
}
|
|
1969
2093
|
}
|
|
1970
2094
|
|
|
2095
|
+
try options.chance.commit(player, .hit);
|
|
1971
2096
|
foe.active.volatiles.LeechSeed = true;
|
|
1972
2097
|
|
|
1973
|
-
try log.start(battle.active(player.foe()), .LeechSeed);
|
|
2098
|
+
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
1974
2099
|
}
|
|
1975
2100
|
|
|
1976
|
-
fn lightScreen(battle: anytype, player: Player,
|
|
2101
|
+
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
1977
2102
|
var side = battle.side(player);
|
|
1978
2103
|
|
|
1979
|
-
if (side.active.volatiles.LightScreen)
|
|
2104
|
+
if (side.active.volatiles.LightScreen) {
|
|
2105
|
+
return options.log.fail(battle.active(player), .None);
|
|
2106
|
+
}
|
|
1980
2107
|
side.active.volatiles.LightScreen = true;
|
|
1981
2108
|
|
|
1982
|
-
try log.start(battle.active(player), .LightScreen);
|
|
2109
|
+
try options.log.start(battle.active(player), .LightScreen);
|
|
1983
2110
|
}
|
|
1984
2111
|
|
|
1985
|
-
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8,
|
|
2112
|
+
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
1986
2113
|
var side = battle.side(player);
|
|
1987
2114
|
var foe = battle.foe(player);
|
|
1988
2115
|
|
|
@@ -1998,45 +2125,51 @@ pub const Effects = struct {
|
|
|
1998
2125
|
const has_mimic = has_mimic: {
|
|
1999
2126
|
for (side.active.moves, 0..) |m, i| {
|
|
2000
2127
|
if (m.id == .Mimic) {
|
|
2001
|
-
oslot = @intCast(
|
|
2128
|
+
oslot = @intCast(i + 1);
|
|
2002
2129
|
break :has_mimic true;
|
|
2003
2130
|
}
|
|
2004
2131
|
}
|
|
2005
2132
|
break :has_mimic false;
|
|
2006
2133
|
};
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2134
|
+
if (!has_mimic) {
|
|
2135
|
+
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2136
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2137
|
+
try options.chance.commit(player, .hit);
|
|
2138
|
+
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2011
2139
|
}
|
|
2012
2140
|
}
|
|
2013
|
-
if (!try checkHit(battle, player, move,
|
|
2141
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2142
|
+
try options.chance.commit(player, .hit);
|
|
2014
2143
|
|
|
2015
|
-
const rslot =
|
|
2144
|
+
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2016
2145
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2017
2146
|
|
|
2018
|
-
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2147
|
+
try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
2019
2148
|
}
|
|
2020
2149
|
|
|
2021
|
-
fn mist(battle: anytype, player: Player,
|
|
2150
|
+
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
2022
2151
|
var side = battle.side(player);
|
|
2023
2152
|
|
|
2024
2153
|
if (side.active.volatiles.Mist) return;
|
|
2025
2154
|
side.active.volatiles.Mist = true;
|
|
2026
2155
|
|
|
2027
|
-
try log.start(battle.active(player), .Mist);
|
|
2156
|
+
try options.log.start(battle.active(player), .Mist);
|
|
2028
2157
|
}
|
|
2029
2158
|
|
|
2030
|
-
fn multiHit(battle: anytype, player: Player, move: Move.Data) void {
|
|
2159
|
+
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2031
2160
|
var side = battle.side(player);
|
|
2032
2161
|
|
|
2033
2162
|
assert(!side.active.volatiles.MultiHit);
|
|
2034
2163
|
side.active.volatiles.MultiHit = true;
|
|
2035
2164
|
|
|
2036
|
-
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2165
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit)
|
|
2166
|
+
try Rolls.distribution(battle, .MultiHit, player, options)
|
|
2167
|
+
else
|
|
2168
|
+
2;
|
|
2037
2169
|
}
|
|
2038
2170
|
|
|
2039
|
-
fn paralyze(battle: anytype, player: Player, move: Move.Data,
|
|
2171
|
+
fn paralyze(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2172
|
+
var log = options.log;
|
|
2040
2173
|
var foe = battle.foe(player);
|
|
2041
2174
|
var foe_stored = foe.stored();
|
|
2042
2175
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2047,7 +2180,7 @@ pub const Effects = struct {
|
|
|
2047
2180
|
if (showdown) {
|
|
2048
2181
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2049
2182
|
if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
|
|
2050
|
-
if (!try checkHit(battle, player, move,
|
|
2183
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2051
2184
|
}
|
|
2052
2185
|
if (Status.any(foe_stored.status)) {
|
|
2053
2186
|
return log.fail(
|
|
@@ -2057,8 +2190,9 @@ pub const Effects = struct {
|
|
|
2057
2190
|
}
|
|
2058
2191
|
if (!showdown) {
|
|
2059
2192
|
if (immune) return log.immune(foe_ident, .None);
|
|
2060
|
-
if (!try checkHit(battle, player, move,
|
|
2193
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
2061
2194
|
}
|
|
2195
|
+
try options.chance.commit(player, .hit);
|
|
2062
2196
|
|
|
2063
2197
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2198
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2066,7 +2200,7 @@ pub const Effects = struct {
|
|
|
2066
2200
|
try log.status(foe_ident, foe_stored.status, .None);
|
|
2067
2201
|
}
|
|
2068
2202
|
|
|
2069
|
-
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data,
|
|
2203
|
+
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2070
2204
|
var foe = battle.foe(player);
|
|
2071
2205
|
var foe_stored = foe.stored();
|
|
2072
2206
|
|
|
@@ -2077,32 +2211,31 @@ pub const Effects = struct {
|
|
|
2077
2211
|
}
|
|
2078
2212
|
|
|
2079
2213
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
Gen12.percent(10)
|
|
2085
|
-
else
|
|
2086
|
-
Gen12.percent(30));
|
|
2087
|
-
if (!chance) return;
|
|
2214
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2215
|
+
if (!try Rolls.secondaryChance(battle, player, move.effect == .ParalyzeChance1, options)) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2088
2218
|
|
|
2089
2219
|
foe_stored.status = Status.init(.PAR);
|
|
2090
2220
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2091
2221
|
|
|
2092
|
-
try log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2222
|
+
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2093
2223
|
}
|
|
2094
2224
|
|
|
2095
|
-
fn payDay(battle: anytype, player: Player,
|
|
2096
|
-
if (!showdown or !battle.foe(player).active.volatiles.Substitute)
|
|
2225
|
+
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2226
|
+
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2227
|
+
try options.log.fieldactivate();
|
|
2228
|
+
}
|
|
2097
2229
|
}
|
|
2098
2230
|
|
|
2099
|
-
fn poison(battle: anytype, player: Player, move: Move.Data,
|
|
2231
|
+
fn poison(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2232
|
+
var log = options.log;
|
|
2100
2233
|
var foe = battle.foe(player);
|
|
2101
2234
|
var foe_stored = foe.stored();
|
|
2102
2235
|
const foe_ident = battle.active(player.foe());
|
|
2103
2236
|
const toxic = battle.side(player).last_selected_move == .Toxic;
|
|
2104
2237
|
|
|
2105
|
-
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move,
|
|
2238
|
+
if (showdown and move.effect == .Poison and !try checkHit(battle, player, move, options)) {
|
|
2106
2239
|
return;
|
|
2107
2240
|
} else if (foe.active.volatiles.Substitute) {
|
|
2108
2241
|
if (move.effect != .Poison) return;
|
|
@@ -2126,16 +2259,12 @@ pub const Effects = struct {
|
|
|
2126
2259
|
}
|
|
2127
2260
|
|
|
2128
2261
|
if (move.effect == .Poison) {
|
|
2129
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2262
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2263
|
+
try options.chance.commit(player, .hit);
|
|
2130
2264
|
} else {
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
-
Gen12.percent(20)
|
|
2136
|
-
else
|
|
2137
|
-
Gen12.percent(40));
|
|
2138
|
-
if (!chance) return;
|
|
2265
|
+
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2139
2268
|
}
|
|
2140
2269
|
|
|
2141
2270
|
foe_stored.status = Status.init(.PSN);
|
|
@@ -2150,34 +2279,40 @@ pub const Effects = struct {
|
|
|
2150
2279
|
|
|
2151
2280
|
fn rage(battle: anytype, player: Player) !void {
|
|
2152
2281
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2153
|
-
if (volatiles.Rage) return;
|
|
2154
|
-
volatiles.Rage = true;
|
|
2155
2282
|
assert(!volatiles.Bide);
|
|
2156
|
-
volatiles.
|
|
2283
|
+
volatiles.Rage = true;
|
|
2157
2284
|
}
|
|
2158
2285
|
|
|
2159
|
-
fn recoil(battle: anytype, player: Player, residual: *bool,
|
|
2286
|
+
fn recoil(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2160
2287
|
var side = battle.side(player);
|
|
2161
2288
|
var stored = side.stored();
|
|
2162
2289
|
|
|
2163
|
-
|
|
2290
|
+
assert(showdown or battle.last_damage > 0);
|
|
2291
|
+
if (showdown and battle.last_damage == 0) return;
|
|
2292
|
+
|
|
2293
|
+
const damage: i16 = @intCast(@max(battle.last_damage /
|
|
2164
2294
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2165
|
-
stored.hp = @intCast(
|
|
2295
|
+
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2166
2296
|
|
|
2167
|
-
try log.damageOf(
|
|
2297
|
+
try options.log.damageOf(
|
|
2298
|
+
battle.active(player),
|
|
2299
|
+
stored,
|
|
2300
|
+
.RecoilOf,
|
|
2301
|
+
battle.active(player.foe()),
|
|
2302
|
+
);
|
|
2168
2303
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2169
2304
|
}
|
|
2170
2305
|
|
|
2171
|
-
fn reflect(battle: anytype, player: Player,
|
|
2306
|
+
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2172
2307
|
var side = battle.side(player);
|
|
2173
2308
|
|
|
2174
|
-
if (side.active.volatiles.Reflect) return log.fail(battle.active(player), .None);
|
|
2309
|
+
if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
|
|
2175
2310
|
side.active.volatiles.Reflect = true;
|
|
2176
2311
|
|
|
2177
|
-
try log.start(battle.active(player), .Reflect);
|
|
2312
|
+
try options.log.start(battle.active(player), .Reflect);
|
|
2178
2313
|
}
|
|
2179
2314
|
|
|
2180
|
-
fn sleep(battle: anytype, player: Player, move: Move.Data,
|
|
2315
|
+
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2181
2316
|
var foe = battle.foe(player);
|
|
2182
2317
|
var foe_stored = foe.stored();
|
|
2183
2318
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2188,13 +2323,14 @@ pub const Effects = struct {
|
|
|
2188
2323
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2189
2324
|
} else {
|
|
2190
2325
|
if (Status.any(foe_stored.status)) {
|
|
2191
|
-
return log.fail(
|
|
2326
|
+
return options.log.fail(
|
|
2192
2327
|
foe_ident,
|
|
2193
2328
|
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2194
2329
|
);
|
|
2195
2330
|
}
|
|
2196
2331
|
// If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2197
|
-
if (!showdown and !try checkHit(battle, player, move,
|
|
2332
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2333
|
+
try options.chance.commit(player, .hit);
|
|
2198
2334
|
}
|
|
2199
2335
|
|
|
2200
2336
|
// Sleep Clause Mod
|
|
@@ -2205,40 +2341,32 @@ pub const Effects = struct {
|
|
|
2205
2341
|
}
|
|
2206
2342
|
foe.active.volatiles.Recharging = false;
|
|
2207
2343
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
while (true) {
|
|
2212
|
-
const r = battle.rng.next() & 7;
|
|
2213
|
-
if (r != 0) break :loop r;
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
foe_stored.status = Status.slp(duration);
|
|
2218
|
-
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2344
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2345
|
+
const last = battle.side(player).last_selected_move;
|
|
2346
|
+
try options.log.statusFrom(foe_ident, foe_stored.status, last);
|
|
2219
2347
|
}
|
|
2220
2348
|
|
|
2221
|
-
fn splash(battle: anytype, player: Player,
|
|
2222
|
-
try log.activate(battle.active(player), .Splash);
|
|
2349
|
+
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2350
|
+
try options.log.activate(battle.active(player), .Splash);
|
|
2223
2351
|
}
|
|
2224
2352
|
|
|
2225
|
-
fn substitute(battle: anytype, player: Player, residual: *bool,
|
|
2353
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2226
2354
|
var side = battle.side(player);
|
|
2227
2355
|
if (side.active.volatiles.Substitute) {
|
|
2228
|
-
try log.fail(battle.active(player), .Substitute);
|
|
2356
|
+
try options.log.fail(battle.active(player), .Substitute);
|
|
2229
2357
|
return;
|
|
2230
2358
|
}
|
|
2231
2359
|
|
|
2232
2360
|
assert(side.stored().stats.hp <= 1023);
|
|
2233
2361
|
// Will be 0 if HP is <= 3 meaning that the user gets a 1 HP Substitute for "free"
|
|
2234
|
-
const hp = @intCast(
|
|
2362
|
+
const hp: u8 = @intCast(side.stored().stats.hp / 4);
|
|
2235
2363
|
// Pokénon Showdown incorrectly checks for 1/4 HP based on `target.maxhp / 4` which returns
|
|
2236
2364
|
// a floating point value and thus only correctly implements the Substitute 1/4 glitch when
|
|
2237
2365
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2238
2366
|
// avoid having to convert to floating point)
|
|
2239
2367
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2240
2368
|
if (side.stored().hp < required_hp) {
|
|
2241
|
-
try log.fail(battle.active(player), .Weak);
|
|
2369
|
+
try options.log.fail(battle.active(player), .Weak);
|
|
2242
2370
|
return;
|
|
2243
2371
|
}
|
|
2244
2372
|
|
|
@@ -2246,35 +2374,37 @@ pub const Effects = struct {
|
|
|
2246
2374
|
side.stored().hp -= hp;
|
|
2247
2375
|
side.active.volatiles.substitute = hp + 1;
|
|
2248
2376
|
side.active.volatiles.Substitute = true;
|
|
2249
|
-
try log.start(battle.active(player), .Substitute);
|
|
2377
|
+
try options.log.start(battle.active(player), .Substitute);
|
|
2250
2378
|
if (hp > 0) {
|
|
2251
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2379
|
+
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2252
2380
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2253
2381
|
}
|
|
2254
2382
|
}
|
|
2255
2383
|
|
|
2256
|
-
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data,
|
|
2257
|
-
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2258
|
-
|
|
2384
|
+
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2259
2385
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2260
|
-
|
|
2261
|
-
|
|
2386
|
+
if (showdown) {
|
|
2387
|
+
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2388
|
+
if (try checkHit(battle, player, move, options)) {
|
|
2389
|
+
try options.chance.commit(player, .hit);
|
|
2390
|
+
}
|
|
2391
|
+
battle.last_damage = 0;
|
|
2392
|
+
} else {
|
|
2393
|
+
try options.log.fail(battle.active(player), .None);
|
|
2394
|
+
try options.log.laststill();
|
|
2395
|
+
}
|
|
2262
2396
|
}
|
|
2263
2397
|
|
|
2264
|
-
fn thrashing(battle: anytype, player: Player) void {
|
|
2398
|
+
fn thrashing(battle: anytype, player: Player, options: anytype) void {
|
|
2265
2399
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2266
2400
|
assert(!volatiles.Thrashing);
|
|
2267
2401
|
assert(!volatiles.Bide);
|
|
2268
2402
|
|
|
2269
2403
|
volatiles.Thrashing = true;
|
|
2270
|
-
volatiles.
|
|
2271
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
-
battle.rng.range(u8, 2, 4)
|
|
2273
|
-
else
|
|
2274
|
-
(battle.rng.next() & 1) + 2);
|
|
2404
|
+
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2275
2405
|
}
|
|
2276
2406
|
|
|
2277
|
-
fn transform(battle: anytype, player: Player,
|
|
2407
|
+
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
2278
2408
|
var side = battle.side(player);
|
|
2279
2409
|
const foe = battle.foe(player);
|
|
2280
2410
|
const foe_ident = battle.active(player.foe());
|
|
@@ -2300,10 +2430,10 @@ pub const Effects = struct {
|
|
|
2300
2430
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2301
2431
|
}
|
|
2302
2432
|
|
|
2303
|
-
try log.transform(battle.active(player), foe_ident);
|
|
2433
|
+
try options.log.transform(battle.active(player), foe_ident);
|
|
2304
2434
|
}
|
|
2305
2435
|
|
|
2306
|
-
fn binding(battle: anytype, player: Player) void {
|
|
2436
|
+
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
2307
2437
|
var side = battle.side(player);
|
|
2308
2438
|
var foe = battle.foe(player);
|
|
2309
2439
|
|
|
@@ -2311,12 +2441,16 @@ pub const Effects = struct {
|
|
|
2311
2441
|
side.active.volatiles.Binding = true;
|
|
2312
2442
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2313
2443
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2314
|
-
if (!showdown)
|
|
2444
|
+
if (!showdown) {
|
|
2445
|
+
foe.active.volatiles.Recharging = false;
|
|
2446
|
+
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2315
2447
|
|
|
2316
|
-
side.active.volatiles.attacks =
|
|
2448
|
+
side.active.volatiles.attacks =
|
|
2449
|
+
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2317
2450
|
}
|
|
2318
2451
|
|
|
2319
|
-
fn boost(battle: anytype, player: Player, move: Move.Data,
|
|
2452
|
+
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2453
|
+
var log = options.log;
|
|
2320
2454
|
var side = battle.side(player);
|
|
2321
2455
|
const ident = battle.active(player);
|
|
2322
2456
|
|
|
@@ -2328,7 +2462,7 @@ pub const Effects = struct {
|
|
|
2328
2462
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2329
2463
|
if (boosts.atk == 6) return try log.fail(ident, .None);
|
|
2330
2464
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2331
|
-
boosts.atk = @
|
|
2465
|
+
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2332
2466
|
const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
|
|
2333
2467
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2334
2468
|
boosts.atk -= 1;
|
|
@@ -2339,16 +2473,18 @@ pub const Effects = struct {
|
|
|
2339
2473
|
}
|
|
2340
2474
|
return try log.fail(ident, .None);
|
|
2341
2475
|
}
|
|
2342
|
-
var mod = BOOSTS[@
|
|
2343
|
-
const stat = unmodifiedStats(battle,
|
|
2476
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2477
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2344
2478
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2345
2479
|
try log.boost(ident, reason, n);
|
|
2480
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2481
|
+
if (showdown and move.effect == .Rage) return;
|
|
2346
2482
|
},
|
|
2347
2483
|
.DefenseUp1, .DefenseUp2 => {
|
|
2348
2484
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2349
2485
|
if (boosts.def == 6) return try log.fail(ident, .None);
|
|
2350
2486
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2351
|
-
boosts.def = @intCast(
|
|
2487
|
+
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2352
2488
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2353
2489
|
boosts.def -= 1;
|
|
2354
2490
|
if (showdown) {
|
|
@@ -2357,15 +2493,15 @@ pub const Effects = struct {
|
|
|
2357
2493
|
}
|
|
2358
2494
|
return try log.fail(ident, .None);
|
|
2359
2495
|
}
|
|
2360
|
-
var mod = BOOSTS[@
|
|
2361
|
-
const stat = unmodifiedStats(battle,
|
|
2496
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2497
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2362
2498
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2363
2499
|
try log.boost(ident, .Defense, n);
|
|
2364
2500
|
},
|
|
2365
2501
|
.SpeedUp2 => {
|
|
2366
2502
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2367
2503
|
if (boosts.spe == 6) return try log.fail(ident, .None);
|
|
2368
|
-
boosts.spe = @intCast(
|
|
2504
|
+
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2369
2505
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2370
2506
|
boosts.spe -= 1;
|
|
2371
2507
|
if (showdown) {
|
|
@@ -2374,8 +2510,8 @@ pub const Effects = struct {
|
|
|
2374
2510
|
}
|
|
2375
2511
|
return try log.fail(ident, .None);
|
|
2376
2512
|
}
|
|
2377
|
-
var mod = BOOSTS[@
|
|
2378
|
-
const stat = unmodifiedStats(battle,
|
|
2513
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2514
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2379
2515
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2380
2516
|
try log.boost(ident, .Speed, 2);
|
|
2381
2517
|
},
|
|
@@ -2383,7 +2519,7 @@ pub const Effects = struct {
|
|
|
2383
2519
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2384
2520
|
if (boosts.spc == 6) return try log.fail(ident, .None);
|
|
2385
2521
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2386
|
-
boosts.spc = @intCast(
|
|
2522
|
+
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2387
2523
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2388
2524
|
boosts.spc -= 1;
|
|
2389
2525
|
if (showdown) {
|
|
@@ -2394,8 +2530,8 @@ pub const Effects = struct {
|
|
|
2394
2530
|
}
|
|
2395
2531
|
return try log.fail(ident, .None);
|
|
2396
2532
|
}
|
|
2397
|
-
var mod = BOOSTS[@
|
|
2398
|
-
const stat = unmodifiedStats(battle,
|
|
2533
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2534
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2399
2535
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2400
2536
|
try log.boost(ident, .SpecialAttack, n);
|
|
2401
2537
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2403,7 +2539,7 @@ pub const Effects = struct {
|
|
|
2403
2539
|
.EvasionUp1 => {
|
|
2404
2540
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2405
2541
|
if (boosts.evasion == 6) return try log.fail(ident, .None);
|
|
2406
|
-
boosts.evasion = @intCast(
|
|
2542
|
+
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2407
2543
|
try log.boost(ident, .Evasion, 1);
|
|
2408
2544
|
},
|
|
2409
2545
|
else => unreachable,
|
|
@@ -2413,32 +2549,32 @@ pub const Effects = struct {
|
|
|
2413
2549
|
statusModify(battle.foe(player).stored().status, &battle.foe(player).active.stats);
|
|
2414
2550
|
}
|
|
2415
2551
|
|
|
2416
|
-
fn unboost(battle: anytype, player: Player, move: Move.Data,
|
|
2552
|
+
fn unboost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
2553
|
+
var log = options.log;
|
|
2417
2554
|
var foe = battle.foe(player);
|
|
2418
2555
|
const foe_ident = battle.active(player.foe());
|
|
2556
|
+
const secondary = move.effect.isStatDownChance();
|
|
2419
2557
|
|
|
2420
|
-
if (foe.active.volatiles.Substitute)
|
|
2421
|
-
return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
|
|
2422
|
-
}
|
|
2558
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2423
2559
|
|
|
2424
|
-
if (
|
|
2425
|
-
const
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
if (!
|
|
2430
|
-
|
|
2431
|
-
return; // checkHit already checks for Invulnerable
|
|
2560
|
+
if (secondary) {
|
|
2561
|
+
const proc = try Rolls.unboost(battle, player, options);
|
|
2562
|
+
if (!proc or foe.active.volatiles.Invulnerable) return;
|
|
2563
|
+
} else {
|
|
2564
|
+
// checkHit already checks for Invulnerable
|
|
2565
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2566
|
+
try options.chance.commit(player, .hit);
|
|
2432
2567
|
}
|
|
2433
2568
|
|
|
2434
2569
|
var stats = &foe.active.stats;
|
|
2435
2570
|
var boosts = &foe.active.boosts;
|
|
2571
|
+
const fail = showdown or !secondary;
|
|
2436
2572
|
|
|
2437
2573
|
switch (move.effect) {
|
|
2438
2574
|
.AttackDown1, .AttackDownChance => {
|
|
2439
2575
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2440
|
-
if (boosts.atk == -6) return try log.fail(foe_ident, .None);
|
|
2441
|
-
boosts.atk = @intCast(
|
|
2576
|
+
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2577
|
+
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2442
2578
|
if (stats.atk == 1) {
|
|
2443
2579
|
boosts.atk += 1;
|
|
2444
2580
|
if (showdown) {
|
|
@@ -2447,16 +2583,16 @@ pub const Effects = struct {
|
|
|
2447
2583
|
}
|
|
2448
2584
|
return try log.fail(foe_ident, .None);
|
|
2449
2585
|
}
|
|
2450
|
-
var mod = BOOSTS[@
|
|
2451
|
-
const stat = unmodifiedStats(battle,
|
|
2586
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2587
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2452
2588
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2453
2589
|
try log.boost(foe_ident, .Attack, -1);
|
|
2454
2590
|
},
|
|
2455
2591
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2456
2592
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2457
|
-
if (boosts.def == -6) return try log.fail(foe_ident, .None);
|
|
2593
|
+
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2458
2594
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2459
|
-
boosts.def = @intCast(
|
|
2595
|
+
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2460
2596
|
if (stats.def == 1) {
|
|
2461
2597
|
boosts.def += 1;
|
|
2462
2598
|
if (showdown) {
|
|
@@ -2465,15 +2601,15 @@ pub const Effects = struct {
|
|
|
2465
2601
|
}
|
|
2466
2602
|
return try log.fail(foe_ident, .None);
|
|
2467
2603
|
}
|
|
2468
|
-
var mod = BOOSTS[@
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2604
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2605
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2470
2606
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2607
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2472
2608
|
},
|
|
2473
2609
|
.SpeedDown1, .SpeedDownChance => {
|
|
2474
2610
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2475
|
-
if (boosts.spe == -6) return try log.fail(foe_ident, .None);
|
|
2476
|
-
boosts.spe = @intCast(
|
|
2611
|
+
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2612
|
+
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2477
2613
|
if (stats.spe == 1) {
|
|
2478
2614
|
boosts.spe += 1;
|
|
2479
2615
|
if (showdown) {
|
|
@@ -2482,16 +2618,16 @@ pub const Effects = struct {
|
|
|
2482
2618
|
}
|
|
2483
2619
|
return try log.fail(foe_ident, .None);
|
|
2484
2620
|
}
|
|
2485
|
-
var mod = BOOSTS[@
|
|
2486
|
-
const stat = unmodifiedStats(battle,
|
|
2621
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2622
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2487
2623
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2488
2624
|
try log.boost(foe_ident, .Speed, -1);
|
|
2489
2625
|
assert(boosts.spe >= -6);
|
|
2490
2626
|
},
|
|
2491
2627
|
.SpecialDownChance => {
|
|
2492
2628
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2493
|
-
if (boosts.spc == -6) return try log.fail(foe_ident, .None);
|
|
2494
|
-
boosts.spc = @intCast(
|
|
2629
|
+
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2630
|
+
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2495
2631
|
if (stats.spc == 1) {
|
|
2496
2632
|
boosts.spc += 1;
|
|
2497
2633
|
if (showdown) {
|
|
@@ -2502,16 +2638,16 @@ pub const Effects = struct {
|
|
|
2502
2638
|
}
|
|
2503
2639
|
return try log.fail(foe_ident, .None);
|
|
2504
2640
|
}
|
|
2505
|
-
var mod = BOOSTS[@
|
|
2506
|
-
const stat = unmodifiedStats(battle,
|
|
2641
|
+
var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2642
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2507
2643
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2508
2644
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2509
2645
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2510
2646
|
},
|
|
2511
2647
|
.AccuracyDown1 => {
|
|
2512
2648
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2513
|
-
if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
|
|
2514
|
-
boosts.accuracy = @intCast(
|
|
2649
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2650
|
+
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2515
2651
|
try log.boost(foe_ident, .Accuracy, -1);
|
|
2516
2652
|
},
|
|
2517
2653
|
else => unreachable,
|
|
@@ -2522,8 +2658,7 @@ pub const Effects = struct {
|
|
|
2522
2658
|
}
|
|
2523
2659
|
};
|
|
2524
2660
|
|
|
2525
|
-
fn unmodifiedStats(battle: anytype,
|
|
2526
|
-
const side = battle.side(who);
|
|
2661
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2527
2662
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2528
2663
|
const id = ID.from(side.active.volatiles.transform);
|
|
2529
2664
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2537,16 +2672,19 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2537
2672
|
}
|
|
2538
2673
|
}
|
|
2539
2674
|
|
|
2540
|
-
inline fn isForced(active:
|
|
2675
|
+
inline fn isForced(active: anytype) bool {
|
|
2541
2676
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2542
2677
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2543
2678
|
}
|
|
2544
2679
|
|
|
2545
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2680
|
+
fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
|
|
2681
|
+
var log = options.log;
|
|
2546
2682
|
var side = battle.side(who);
|
|
2547
2683
|
var volatiles = &side.active.volatiles;
|
|
2548
2684
|
const ident = battle.active(who);
|
|
2549
2685
|
|
|
2686
|
+
options.chance.clearDurations(who, clear);
|
|
2687
|
+
|
|
2550
2688
|
if (volatiles.disabled_move != 0) {
|
|
2551
2689
|
volatiles.disabled_move = 0;
|
|
2552
2690
|
volatiles.disabled_duration = 0;
|
|
@@ -2559,15 +2697,15 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2559
2697
|
}
|
|
2560
2698
|
if (volatiles.Mist) {
|
|
2561
2699
|
volatiles.Mist = false;
|
|
2562
|
-
try log.end(ident, .
|
|
2700
|
+
try log.end(ident, .MistSilent);
|
|
2563
2701
|
}
|
|
2564
2702
|
if (volatiles.FocusEnergy) {
|
|
2565
2703
|
volatiles.FocusEnergy = false;
|
|
2566
|
-
try log.end(ident, .
|
|
2704
|
+
try log.end(ident, .FocusEnergySilent);
|
|
2567
2705
|
}
|
|
2568
2706
|
if (volatiles.LeechSeed) {
|
|
2569
2707
|
volatiles.LeechSeed = false;
|
|
2570
|
-
try log.end(ident, .
|
|
2708
|
+
try log.end(ident, .LeechSeedSilent);
|
|
2571
2709
|
}
|
|
2572
2710
|
if (!showdown and volatiles.Toxic) {
|
|
2573
2711
|
volatiles.Toxic = false;
|
|
@@ -2575,102 +2713,329 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2575
2713
|
}
|
|
2576
2714
|
if (volatiles.LightScreen) {
|
|
2577
2715
|
volatiles.LightScreen = false;
|
|
2578
|
-
try log.end(ident, .
|
|
2716
|
+
try log.end(ident, .LightScreenSilent);
|
|
2579
2717
|
}
|
|
2580
2718
|
if (volatiles.Reflect) {
|
|
2581
2719
|
volatiles.Reflect = false;
|
|
2582
|
-
try log.end(ident, .
|
|
2720
|
+
try log.end(ident, .ReflectSilent);
|
|
2583
2721
|
}
|
|
2584
2722
|
if (showdown and volatiles.Toxic) {
|
|
2585
2723
|
volatiles.Toxic = false;
|
|
2586
2724
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2587
2725
|
volatiles.toxic = 0;
|
|
2588
|
-
try log.end(ident, .
|
|
2726
|
+
try log.end(ident, .ToxicSilent);
|
|
2589
2727
|
}
|
|
2590
2728
|
}
|
|
2591
2729
|
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
if (
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2730
|
+
pub const Rolls = struct {
|
|
2731
|
+
inline fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2732
|
+
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2733
|
+
player == .P1
|
|
2734
|
+
else if (showdown)
|
|
2735
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
2736
|
+
else
|
|
2737
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
2738
|
+
|
|
2739
|
+
try options.chance.speedTie(p1);
|
|
2740
|
+
return p1;
|
|
2601
2741
|
}
|
|
2602
|
-
return false;
|
|
2603
|
-
}
|
|
2604
2742
|
|
|
2605
|
-
inline fn
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2743
|
+
inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
|
|
2744
|
+
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2745
|
+
val == .true
|
|
2746
|
+
else if (showdown)
|
|
2747
|
+
battle.rng.chance(u8, rate, 256)
|
|
2748
|
+
else
|
|
2749
|
+
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2750
|
+
|
|
2751
|
+
try options.chance.criticalHit(player, crit, rate);
|
|
2752
|
+
return crit;
|
|
2609
2753
|
}
|
|
2610
|
-
}
|
|
2611
2754
|
|
|
2612
|
-
fn
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2755
|
+
inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
|
|
2756
|
+
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2757
|
+
val
|
|
2758
|
+
else roll: {
|
|
2759
|
+
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2760
|
+
while (true) {
|
|
2761
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2762
|
+
if (r >= 217) break :roll r;
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2616
2765
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
else
|
|
2622
|
-
(battle.rng.next() & 3) + 2);
|
|
2766
|
+
assert(roll >= 217 and roll <= 255);
|
|
2767
|
+
try options.chance.damage(player, roll);
|
|
2768
|
+
return roll;
|
|
2769
|
+
}
|
|
2623
2770
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2771
|
+
inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
|
|
2772
|
+
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2773
|
+
val == .true
|
|
2774
|
+
else if (showdown)
|
|
2775
|
+
battle.rng.chance(u8, accuracy, 256)
|
|
2776
|
+
else
|
|
2777
|
+
battle.rng.next() < accuracy;
|
|
2626
2778
|
|
|
2627
|
-
|
|
2779
|
+
options.chance.hit(ok, accuracy);
|
|
2780
|
+
return ok;
|
|
2781
|
+
}
|
|
2628
2782
|
|
|
2629
|
-
fn
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2783
|
+
inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2784
|
+
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2785
|
+
val == .true
|
|
2786
|
+
else if (showdown)
|
|
2787
|
+
!battle.rng.chance(u8, 128, 256)
|
|
2788
|
+
else
|
|
2789
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2634
2790
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2791
|
+
try options.chance.confused(player, cfz);
|
|
2792
|
+
return cfz;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2796
|
+
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2797
|
+
val == .true
|
|
2798
|
+
else if (showdown)
|
|
2799
|
+
battle.rng.chance(u8, 63, 256)
|
|
2800
|
+
else
|
|
2801
|
+
battle.rng.next() < Gen12.percent(25);
|
|
2802
|
+
|
|
2803
|
+
try options.chance.paralyzed(player, par);
|
|
2804
|
+
return par;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2808
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2809
|
+
val == .true
|
|
2810
|
+
else if (showdown)
|
|
2811
|
+
battle.rng.chance(u8, 25, 256)
|
|
2812
|
+
else
|
|
2813
|
+
battle.rng.next() < Gen12.percent(10);
|
|
2814
|
+
|
|
2815
|
+
try options.chance.secondaryChance(player, proc, 25);
|
|
2816
|
+
return proc;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2820
|
+
const rate: u8 = if (low) 26 else 77;
|
|
2821
|
+
|
|
2822
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2823
|
+
val == .true
|
|
2824
|
+
else if (showdown)
|
|
2825
|
+
battle.rng.chance(u8, rate, 256)
|
|
2826
|
+
else
|
|
2827
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2828
|
+
|
|
2829
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2830
|
+
return proc;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2834
|
+
const rate: u8 = if (low) 52 else 103;
|
|
2835
|
+
|
|
2836
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2837
|
+
val == .true
|
|
2838
|
+
else if (showdown)
|
|
2839
|
+
battle.rng.chance(u8, rate, 256)
|
|
2840
|
+
else
|
|
2841
|
+
battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2842
|
+
|
|
2843
|
+
try options.chance.secondaryChance(player, proc, rate);
|
|
2844
|
+
return proc;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2848
|
+
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2849
|
+
val == .true
|
|
2850
|
+
else if (showdown)
|
|
2851
|
+
battle.rng.chance(u8, 85, 256)
|
|
2852
|
+
else
|
|
2853
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2854
|
+
|
|
2855
|
+
try options.chance.secondaryChance(player, proc, 85);
|
|
2856
|
+
return proc;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2860
|
+
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2861
|
+
val
|
|
2862
|
+
else if (showdown)
|
|
2863
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2864
|
+
else move: {
|
|
2865
|
+
while (true) {
|
|
2866
|
+
const r = battle.rng.next();
|
|
2867
|
+
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
2868
|
+
if (r >= @intFromEnum(Move.Struggle)) continue;
|
|
2869
|
+
break :move @enumFromInt(r);
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
try options.chance.metronome(player, move);
|
|
2874
|
+
return move;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2878
|
+
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2879
|
+
val - 1
|
|
2880
|
+
else if (showdown)
|
|
2881
|
+
battle.rng.range(u8, 0, max)
|
|
2882
|
+
else power: {
|
|
2883
|
+
while (true) {
|
|
2884
|
+
const r = battle.rng.next();
|
|
2885
|
+
if (r < max) break :power r;
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
assert(power < max);
|
|
2890
|
+
try options.chance.psywave(player, power, max);
|
|
2891
|
+
return power;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2895
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2896
|
+
@intCast(val)
|
|
2897
|
+
else if (showdown)
|
|
2898
|
+
battle.rng.range(u3, 1, 8)
|
|
2899
|
+
else duration: {
|
|
2900
|
+
while (true) {
|
|
2901
|
+
const r = battle.rng.next() & 7;
|
|
2902
|
+
if (r != 0) break :duration @intCast(r);
|
|
2642
2903
|
}
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2906
|
+
assert(duration >= 1 and duration <= 7);
|
|
2907
|
+
options.chance.duration(.sleep, player, player.foe(), duration);
|
|
2908
|
+
return duration;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2912
|
+
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2913
|
+
val
|
|
2914
|
+
else if (showdown)
|
|
2915
|
+
battle.rng.range(u4, 1, 9)
|
|
2916
|
+
else
|
|
2917
|
+
@intCast((battle.rng.next() & 7) + 1);
|
|
2918
|
+
|
|
2919
|
+
assert(duration >= 1 and duration <= 8);
|
|
2920
|
+
options.chance.duration(.disable, player, player.foe(), duration);
|
|
2921
|
+
return duration;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
|
|
2925
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2926
|
+
@intCast(val)
|
|
2927
|
+
else if (showdown)
|
|
2928
|
+
battle.rng.range(u3, 2, 6)
|
|
2929
|
+
else
|
|
2930
|
+
@intCast((battle.rng.next() & 3) + 2);
|
|
2931
|
+
|
|
2932
|
+
assert(duration >= 2 and duration <= 5);
|
|
2933
|
+
options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
|
|
2934
|
+
return duration;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2938
|
+
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2939
|
+
@intCast(val)
|
|
2940
|
+
else if (showdown)
|
|
2941
|
+
battle.rng.range(u3, 2, 4)
|
|
2942
|
+
else
|
|
2943
|
+
@intCast((battle.rng.next() & 1) + 2);
|
|
2944
|
+
|
|
2945
|
+
assert(duration >= 2 and duration <= 3);
|
|
2946
|
+
options.chance.duration(.attacking, player, player, duration);
|
|
2947
|
+
return duration;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2951
|
+
|
|
2952
|
+
inline fn distribution(
|
|
2953
|
+
battle: anytype,
|
|
2954
|
+
comptime effect: Move.Effect,
|
|
2955
|
+
player: Player,
|
|
2956
|
+
options: anytype,
|
|
2957
|
+
) !u3 {
|
|
2958
|
+
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2959
|
+
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2960
|
+
@intCast(val)
|
|
2961
|
+
else if (showdown)
|
|
2962
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2963
|
+
else n: {
|
|
2964
|
+
const r = (battle.rng.next() & 3);
|
|
2965
|
+
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2968
|
+
assert(n >= 2 and n <= 5);
|
|
2969
|
+
if (effect == .Binding) {
|
|
2970
|
+
options.chance.duration(.binding, player, player, n);
|
|
2643
2971
|
} else {
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2972
|
+
try options.chance.multiHit(player, n);
|
|
2973
|
+
}
|
|
2974
|
+
return n;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
fn moveSlot(
|
|
2978
|
+
battle: anytype,
|
|
2979
|
+
player: Player,
|
|
2980
|
+
moves: []MoveSlot,
|
|
2981
|
+
check_pp: u4,
|
|
2982
|
+
options: anytype,
|
|
2983
|
+
) !u4 {
|
|
2984
|
+
// TODO: consider throwing error instead of rerolling?
|
|
2985
|
+
const overridden = if (options.calc.overridden(player, .move_slot)) |val|
|
|
2986
|
+
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2987
|
+
val
|
|
2988
|
+
else
|
|
2989
|
+
null
|
|
2990
|
+
else
|
|
2991
|
+
null;
|
|
2992
|
+
|
|
2993
|
+
const slot: u4 = overridden orelse slot: {
|
|
2994
|
+
if (showdown) {
|
|
2995
|
+
if (check_pp == 0) {
|
|
2996
|
+
var i: usize = moves.len;
|
|
2997
|
+
while (i > 0) {
|
|
2998
|
+
i -= 1;
|
|
2999
|
+
if (moves[i].id != .None) {
|
|
3000
|
+
break :slot battle.rng.range(u4, 0, @as(u4, @intCast(i + 1))) + 1;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
} else {
|
|
3004
|
+
var r = battle.rng.range(u4, 0, check_pp) + 1;
|
|
3005
|
+
var i: usize = 0;
|
|
3006
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
3007
|
+
if (moves[i].pp > 0) {
|
|
3008
|
+
r -= 1;
|
|
3009
|
+
if (r == 0) break;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
break :slot @intCast(i + 1);
|
|
2650
3013
|
}
|
|
2651
3014
|
}
|
|
2652
|
-
return @intCast(u4, i + 1);
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
3015
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3016
|
+
while (true) {
|
|
3017
|
+
const r: u4 = @intCast(battle.rng.next() & 3);
|
|
3018
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) break :slot r + 1;
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
try options.chance.moveSlot(player, slot, moves, check_pp);
|
|
3023
|
+
return slot;
|
|
2659
3024
|
}
|
|
2660
|
-
}
|
|
3025
|
+
};
|
|
2661
3026
|
|
|
2662
3027
|
test "RNG agreement" {
|
|
2663
3028
|
if (!showdown) return;
|
|
2664
3029
|
var expected: [256]u32 = undefined;
|
|
2665
3030
|
for (0..expected.len) |i| {
|
|
2666
|
-
expected[i] = @intCast(
|
|
3031
|
+
expected[i] = @intCast(i * 0x1000000);
|
|
2667
3032
|
}
|
|
2668
3033
|
|
|
2669
|
-
var spe
|
|
2670
|
-
var cfz
|
|
2671
|
-
var par
|
|
2672
|
-
var brn
|
|
2673
|
-
var eff
|
|
3034
|
+
var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3035
|
+
var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3036
|
+
var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3037
|
+
var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
3038
|
+
var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
|
|
2674
3039
|
|
|
2675
3040
|
for (0..expected.len) |i| {
|
|
2676
3041
|
try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
|
|
@@ -2706,7 +3071,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2706
3071
|
const side = battle.side(player);
|
|
2707
3072
|
const foe = battle.foe(player);
|
|
2708
3073
|
|
|
2709
|
-
var active = side.active;
|
|
3074
|
+
var active = &side.active;
|
|
2710
3075
|
const stored = side.stored();
|
|
2711
3076
|
|
|
2712
3077
|
// While players are not given any input options on the cartridge in these cases,
|
|
@@ -2720,7 +3085,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2720
3085
|
// strictly correct as the player would not have been presented the option to move or
|
|
2721
3086
|
// switch at all.
|
|
2722
3087
|
if (isForced(active)) {
|
|
2723
|
-
out[n] = .{ .type = .Move, .data = @
|
|
3088
|
+
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
2724
3089
|
n += 1;
|
|
2725
3090
|
return n;
|
|
2726
3091
|
}
|
|
@@ -2757,7 +3122,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2757
3122
|
while (slot <= 4) : (slot += 1) {
|
|
2758
3123
|
const m = active.moves[slot - 1];
|
|
2759
3124
|
if (m.id == .None) break;
|
|
2760
|
-
if (m.id == side.last_selected_move) {
|
|
3125
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2761
3126
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2762
3127
|
const struggle =
|
|
2763
3128
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|