@pkmn/engine 0.1.0-dev.f918c3bd → 0.1.0-dev.fb6653d6
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/LICENSE +1 -3
- package/README.md +51 -23
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +97 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -3
- package/build/pkg/addon.js +39 -71
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/ids.json +12 -12
- package/build/pkg/data/layout.json +17 -4
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +27 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +13 -17
- package/build/pkg/gen1.js +8 -12
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -91
- package/build/pkg/index.js +6 -63
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -69
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +142 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +303 -101
- package/package.json +61 -132
- package/src/bin/install-pkmn-engine +30 -20
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/{binding → bindings}/node.zig +8 -21
- package/src/lib/bindings/wasm.zig +58 -0
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +21 -10
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +421 -393
- package/src/lib/common/rational.zig +131 -35
- package/src/lib/common/rng.zig +16 -9
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +687 -160
- package/src/lib/gen1/chance.zig +434 -307
- package/src/lib/gen1/data/moves.zig +26 -14
- package/src/lib/gen1/data/species.zig +4 -6
- package/src/lib/gen1/data/types.zig +7 -8
- package/src/lib/gen1/data.zig +44 -47
- package/src/lib/gen1/helpers.zig +91 -562
- package/src/lib/gen1/mechanics.zig +641 -557
- package/src/lib/gen2/calc.zig +153 -0
- package/src/lib/gen2/chance.zig +902 -0
- package/src/lib/gen2/data/items.zig +13 -21
- package/src/lib/gen2/data/moves.zig +67 -47
- package/src/lib/gen2/data/species.zig +2 -4
- package/src/lib/gen2/data/types.zig +134 -32
- package/src/lib/gen2/data.zig +56 -54
- package/src/lib/gen2/generated.zig +801 -0
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +2778 -362
- package/src/lib/node.zig +18 -0
- package/src/lib/pkmn.zig +37 -15
- package/src/lib/wasm.zig +17 -0
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/lib/binding/wasm.zig +0 -12
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -328
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,38 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const Action = @import("chance.zig").Action;
|
|
11
|
-
|
|
12
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
13
11
|
|
|
14
12
|
const assert = std.debug.assert;
|
|
15
|
-
|
|
16
|
-
const expectEqual = std.testing.expectEqual;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
|
-
const showdown = pkmn.options.showdown;
|
|
24
|
-
|
|
25
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
26
15
|
const Damage = protocol.Damage;
|
|
27
|
-
const Heal = protocol.Heal;
|
|
28
|
-
|
|
29
|
-
const Gen12 = rng.Gen12;
|
|
30
|
-
|
|
31
|
-
const ActivePokemon = data.ActivePokemon;
|
|
32
16
|
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
18
|
+
const Fail = protocol.Fail;
|
|
19
|
+
const Gen12 = rng.Gen12;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
33
22
|
const Move = data.Move;
|
|
34
23
|
const MoveSlot = data.MoveSlot;
|
|
35
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
36
27
|
const Side = data.Side;
|
|
37
28
|
const Species = data.Species;
|
|
38
29
|
const Stats = data.Stats;
|
|
@@ -59,30 +50,26 @@ const BOOSTS = &[_][2]u8{
|
|
|
59
50
|
|
|
60
51
|
const MAX_STAT_VALUE = 999;
|
|
61
52
|
|
|
62
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
63
|
-
|
|
64
53
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
|
|
65
54
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
66
55
|
if (battle.turn == 0) return start(battle, options);
|
|
67
56
|
|
|
68
57
|
var s1 = false;
|
|
69
58
|
var s2 = false;
|
|
70
|
-
var f1: ?Move = null;
|
|
71
|
-
var f2: ?Move = null;
|
|
72
59
|
|
|
73
|
-
if (selectMove(battle, .P1, c1, c2, &s1
|
|
74
|
-
if (selectMove(battle, .P2, c2, c1, &s2
|
|
60
|
+
if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
|
|
61
|
+
if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
|
|
75
62
|
|
|
76
63
|
var p1 = battle.side(.P1);
|
|
77
64
|
var p2 = battle.side(.P2);
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
67
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
81
68
|
|
|
82
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
83
|
-
if (try doTurn(battle, .P1, c1, r1, s1,
|
|
70
|
+
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
84
71
|
} else {
|
|
85
|
-
if (try doTurn(battle, .P2, c2, r2, s2,
|
|
72
|
+
if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
|
|
86
73
|
}
|
|
87
74
|
|
|
88
75
|
if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
|
|
@@ -95,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
95
82
|
const p1 = battle.side(.P1);
|
|
96
83
|
const p2 = battle.side(.P2);
|
|
97
84
|
|
|
98
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
99
86
|
assert(!showdown or p1_slot == 1);
|
|
100
87
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
101
88
|
|
|
102
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
103
90
|
assert(!showdown or p2_slot == 1);
|
|
104
91
|
if (p2_slot == 0) return Result.Win;
|
|
105
92
|
|
|
@@ -120,7 +107,6 @@ fn selectMove(
|
|
|
120
107
|
choice: Choice,
|
|
121
108
|
foe_choice: Choice,
|
|
122
109
|
skip_turn: *bool,
|
|
123
|
-
from: *?Move,
|
|
124
110
|
) ?Result {
|
|
125
111
|
if (choice.type == .Pass) return null;
|
|
126
112
|
|
|
@@ -137,7 +123,6 @@ fn selectMove(
|
|
|
137
123
|
return null;
|
|
138
124
|
}
|
|
139
125
|
if (volatiles.Rage) {
|
|
140
|
-
from.* = side.last_used_move;
|
|
141
126
|
if (showdown) {
|
|
142
127
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
143
128
|
saveMove(battle, player, null);
|
|
@@ -147,7 +132,6 @@ fn selectMove(
|
|
|
147
132
|
// Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
|
|
148
133
|
if (!showdown) volatiles.Flinch = false;
|
|
149
134
|
if (volatiles.Thrashing or volatiles.Charging) {
|
|
150
|
-
from.* = side.last_selected_move;
|
|
151
135
|
if (showdown) {
|
|
152
136
|
if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
|
|
153
137
|
saveMove(battle, player, null);
|
|
@@ -168,15 +152,13 @@ fn selectMove(
|
|
|
168
152
|
return null;
|
|
169
153
|
}
|
|
170
154
|
if (volatiles.Binding) {
|
|
171
|
-
from.* = side.last_used_move;
|
|
172
155
|
if (showdown) {
|
|
173
|
-
if (foe_choice.type == .Switch) from.* = null;
|
|
174
156
|
// Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
|
|
175
157
|
// should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
|
|
176
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
177
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
178
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
179
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
180
162
|
saveMove(battle, player, choice);
|
|
181
163
|
} else {
|
|
182
164
|
assert(choice.data == 0);
|
|
@@ -184,7 +166,10 @@ fn selectMove(
|
|
|
184
166
|
if (foe_choice.type == .Switch) {
|
|
185
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
186
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
187
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
188
173
|
}
|
|
189
174
|
}
|
|
190
175
|
return null;
|
|
@@ -206,7 +191,7 @@ fn selectMove(
|
|
|
206
191
|
if (choice.data == 0) {
|
|
207
192
|
const struggle = ok: {
|
|
208
193
|
for (side.active.moves, 0..) |move, i| {
|
|
209
|
-
if (move.pp > 0 and volatiles.
|
|
194
|
+
if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
|
|
210
195
|
}
|
|
211
196
|
break :ok true;
|
|
212
197
|
};
|
|
@@ -225,7 +210,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
|
|
|
225
210
|
if (c.data == 0) {
|
|
226
211
|
side.last_selected_move = .Struggle;
|
|
227
212
|
} else {
|
|
228
|
-
assert(showdown or side.active.volatiles.
|
|
213
|
+
assert(showdown or side.active.volatiles.disable_move != c.data);
|
|
229
214
|
const move = side.active.move(c.data);
|
|
230
215
|
// You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
|
|
231
216
|
// sometimes required...), but a 0 PP move can be used automatically
|
|
@@ -263,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
263
248
|
active.moves = incoming.moves;
|
|
264
249
|
|
|
265
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
266
252
|
|
|
267
253
|
foe.active.volatiles.Binding = false;
|
|
268
254
|
|
|
269
|
-
try options.log.switched(battle.active(player), incoming);
|
|
270
|
-
options.chance.switched(player, side.order[0], out);
|
|
255
|
+
try options.log.switched(.{ battle.active(player), incoming });
|
|
271
256
|
|
|
272
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
273
258
|
incoming.status = Status.init(.PSN);
|
|
274
259
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
275
260
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
276
|
-
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
261
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
277
262
|
}
|
|
278
263
|
}
|
|
279
264
|
|
|
@@ -336,12 +321,10 @@ fn doTurn(
|
|
|
336
321
|
player_choice: Choice,
|
|
337
322
|
player_rewrap: bool,
|
|
338
323
|
player_skip: bool,
|
|
339
|
-
player_from: ?Move,
|
|
340
324
|
foe_player: Player,
|
|
341
325
|
foe_choice: Choice,
|
|
342
326
|
foe_rewrap: bool,
|
|
343
327
|
foe_skip: bool,
|
|
344
|
-
foe_from: ?Move,
|
|
345
328
|
options: anytype,
|
|
346
329
|
) !?Result {
|
|
347
330
|
assert(player_choice.type != .Pass);
|
|
@@ -354,19 +337,18 @@ fn doTurn(
|
|
|
354
337
|
player_choice,
|
|
355
338
|
player_rewrap,
|
|
356
339
|
player_skip,
|
|
357
|
-
player_from,
|
|
358
340
|
&residual,
|
|
359
341
|
options,
|
|
360
342
|
)) |r| return r;
|
|
361
343
|
if (!replace) {
|
|
362
344
|
if (player_choice.type != .Switch) {
|
|
363
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
364
346
|
}
|
|
365
347
|
if (residual) try handleResidual(battle, player, options);
|
|
366
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
367
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
368
350
|
|
|
369
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
370
352
|
|
|
371
353
|
residual = true;
|
|
372
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -377,16 +359,15 @@ fn doTurn(
|
|
|
377
359
|
foe_choice,
|
|
378
360
|
foe_rewrap,
|
|
379
361
|
foe_skip,
|
|
380
|
-
foe_from,
|
|
381
362
|
&residual,
|
|
382
363
|
options,
|
|
383
364
|
)) |r| return r;
|
|
384
365
|
if (!replace) {
|
|
385
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
386
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
387
368
|
}
|
|
388
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
389
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
390
371
|
}
|
|
391
372
|
|
|
392
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -405,7 +386,6 @@ fn executeMove(
|
|
|
405
386
|
choice: Choice,
|
|
406
387
|
rewrap: bool,
|
|
407
388
|
skip: bool,
|
|
408
|
-
from: ?Move,
|
|
409
389
|
residual: *bool,
|
|
410
390
|
options: anytype,
|
|
411
391
|
) !?Result {
|
|
@@ -422,7 +402,7 @@ fn executeMove(
|
|
|
422
402
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
423
403
|
assert(!showdown);
|
|
424
404
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
425
|
-
try options.log.cant(battle.active(player), .Bound);
|
|
405
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
426
406
|
}
|
|
427
407
|
return null;
|
|
428
408
|
}
|
|
@@ -440,11 +420,13 @@ fn executeMove(
|
|
|
440
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
441
421
|
const stored = side.stored();
|
|
442
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
443
|
-
auto =
|
|
444
|
-
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
424
|
+
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
425
|
+
side.last_selected_move == .HyperBeam or
|
|
426
|
+
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
445
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
446
428
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
447
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
429
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
448
430
|
if (!auto) {
|
|
449
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
450
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -463,7 +445,7 @@ fn executeMove(
|
|
|
463
445
|
|
|
464
446
|
var skip_can = false;
|
|
465
447
|
var skip_pp = false;
|
|
466
|
-
switch (try beforeMove(battle, player, skip,
|
|
448
|
+
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
467
449
|
.done => return null,
|
|
468
450
|
.skip_can => skip_can = true,
|
|
469
451
|
.skip_pp => skip_pp = true,
|
|
@@ -471,10 +453,29 @@ fn executeMove(
|
|
|
471
453
|
.err => return @as(?Result, Result.Error),
|
|
472
454
|
}
|
|
473
455
|
|
|
474
|
-
|
|
475
|
-
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
476
473
|
}
|
|
477
474
|
|
|
475
|
+
const can = skip_can or
|
|
476
|
+
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
477
|
+
if (!can) return null;
|
|
478
|
+
|
|
478
479
|
return doMove(battle, player, mslot, rewrap, auto, residual, options);
|
|
479
480
|
}
|
|
480
481
|
|
|
@@ -484,7 +485,6 @@ fn beforeMove(
|
|
|
484
485
|
battle: anytype,
|
|
485
486
|
player: Player,
|
|
486
487
|
skip: bool,
|
|
487
|
-
from: ?Move,
|
|
488
488
|
residual: *bool,
|
|
489
489
|
options: anytype,
|
|
490
490
|
) !BeforeMove {
|
|
@@ -498,36 +498,41 @@ fn beforeMove(
|
|
|
498
498
|
|
|
499
499
|
if (Status.is(stored.status, .SLP)) {
|
|
500
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
501
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
502
|
-
if (options.calc.
|
|
503
|
-
|
|
503
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
504
509
|
} else {
|
|
505
510
|
stored.status -= 1;
|
|
506
511
|
}
|
|
507
512
|
|
|
508
513
|
const duration = Status.duration(stored.status);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
512
517
|
|
|
513
518
|
if (duration == 0) {
|
|
514
|
-
try log.curestatus(ident, before, .Message);
|
|
519
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
515
520
|
stored.status = 0; // clears EXT if present
|
|
516
521
|
} else {
|
|
517
|
-
try log.cant(ident, .Sleep);
|
|
522
|
+
try log.cant(.{ ident, .Sleep });
|
|
518
523
|
}
|
|
519
524
|
side.last_used_move = .None;
|
|
520
525
|
return .done;
|
|
521
526
|
}
|
|
522
527
|
|
|
523
528
|
if (Status.is(stored.status, .FRZ)) {
|
|
524
|
-
try log.cant(ident, .Freeze);
|
|
529
|
+
try log.cant(.{ ident, .Freeze });
|
|
525
530
|
side.last_used_move = .None;
|
|
526
531
|
return .done;
|
|
527
532
|
}
|
|
528
533
|
|
|
529
534
|
if (skip or foe.active.volatiles.Binding) {
|
|
530
|
-
try log.cant(ident, .Bound);
|
|
535
|
+
try log.cant(.{ ident, .Bound });
|
|
531
536
|
return .done;
|
|
532
537
|
}
|
|
533
538
|
|
|
@@ -535,27 +540,27 @@ fn beforeMove(
|
|
|
535
540
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
536
541
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
537
542
|
if (!showdown) volatiles.Flinch = false;
|
|
538
|
-
try log.cant(ident, .Flinch);
|
|
543
|
+
try log.cant(.{ ident, .Flinch });
|
|
539
544
|
return .done;
|
|
540
545
|
}
|
|
541
546
|
|
|
542
547
|
if (volatiles.Recharging) {
|
|
543
548
|
volatiles.Recharging = false;
|
|
544
|
-
try log.cant(ident, .Recharge);
|
|
549
|
+
try log.cant(.{ ident, .Recharge });
|
|
545
550
|
return .done;
|
|
546
551
|
}
|
|
547
552
|
|
|
548
|
-
if (volatiles.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
553
|
+
if (volatiles.disable_duration > 0) {
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
555
560
|
|
|
556
|
-
if (volatiles.
|
|
557
|
-
volatiles.
|
|
558
|
-
try log.end(ident, .Disable);
|
|
561
|
+
if (volatiles.disable_duration == 0) {
|
|
562
|
+
volatiles.disable_move = 0;
|
|
563
|
+
try log.end(.{ ident, .Disable });
|
|
559
564
|
}
|
|
560
565
|
}
|
|
561
566
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -564,25 +569,30 @@ fn beforeMove(
|
|
|
564
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
565
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
566
571
|
if (side.last_selected_move == .None) {
|
|
567
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
568
573
|
return .err;
|
|
569
574
|
}
|
|
570
575
|
|
|
571
576
|
if (volatiles.Confusion) {
|
|
572
577
|
assert(volatiles.confusion > 0);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
576
585
|
} else {
|
|
577
586
|
volatiles.confusion -= 1;
|
|
578
587
|
}
|
|
579
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
580
590
|
|
|
581
591
|
if (volatiles.confusion == 0) {
|
|
582
592
|
volatiles.Confusion = false;
|
|
583
|
-
try log.end(ident, .Confusion);
|
|
593
|
+
try log.end(.{ ident, .Confusion });
|
|
584
594
|
} else {
|
|
585
|
-
try log.activate(ident, .Confusion);
|
|
595
|
+
try log.activate(.{ ident, .Confusion });
|
|
586
596
|
|
|
587
597
|
if (try Rolls.confused(battle, player, options)) {
|
|
588
598
|
assert(!volatiles.MultiHit);
|
|
@@ -594,7 +604,9 @@ fn beforeMove(
|
|
|
594
604
|
volatiles.Charging = false;
|
|
595
605
|
volatiles.Binding = false;
|
|
596
606
|
volatiles.Invulnerable = false;
|
|
597
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
598
610
|
{
|
|
599
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
600
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -602,7 +614,9 @@ fn beforeMove(
|
|
|
602
614
|
const def = foe.active.stats.def;
|
|
603
615
|
foe.active.stats.def = active.stats.def;
|
|
604
616
|
defer foe.active.stats.def = def;
|
|
605
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
606
620
|
}
|
|
607
621
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
608
622
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -631,9 +645,12 @@ fn beforeMove(
|
|
|
631
645
|
volatiles.Thrashing = false;
|
|
632
646
|
volatiles.Charging = false;
|
|
633
647
|
volatiles.Binding = false;
|
|
634
|
-
|
|
648
|
+
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
650
|
+
options.chance.observe(.binding, player, .None);
|
|
651
|
+
|
|
635
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
636
|
-
try log.cant(ident, .Paralysis);
|
|
653
|
+
try log.cant(.{ ident, .Paralysis });
|
|
637
654
|
return .done;
|
|
638
655
|
}
|
|
639
656
|
|
|
@@ -649,28 +666,22 @@ fn beforeMove(
|
|
|
649
666
|
volatiles.state +%= battle.last_damage;
|
|
650
667
|
}
|
|
651
668
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
655
|
-
if (!extend) volatiles.attacks = 0;
|
|
656
|
-
} else {
|
|
657
|
-
volatiles.attacks -= 1;
|
|
658
|
-
}
|
|
659
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
660
671
|
|
|
661
672
|
if (volatiles.attacks != 0) {
|
|
662
|
-
try log.activate(ident, .Bide);
|
|
673
|
+
try log.activate(.{ ident, .Bide });
|
|
663
674
|
return .done;
|
|
664
675
|
}
|
|
665
676
|
|
|
666
677
|
volatiles.Bide = false;
|
|
667
|
-
try log.end(ident, .Bide);
|
|
678
|
+
try log.end(.{ ident, .Bide });
|
|
668
679
|
|
|
669
680
|
battle.last_damage = volatiles.state *% 2;
|
|
670
681
|
volatiles.state = 0;
|
|
671
682
|
|
|
672
683
|
if (battle.last_damage == 0) {
|
|
673
|
-
try log.fail(ident, .None);
|
|
684
|
+
try log.fail(.{ ident, .None });
|
|
674
685
|
return .done;
|
|
675
686
|
}
|
|
676
687
|
|
|
@@ -690,47 +701,42 @@ fn beforeMove(
|
|
|
690
701
|
}
|
|
691
702
|
|
|
692
703
|
if (volatiles.Thrashing) {
|
|
693
|
-
|
|
694
|
-
assert(from != null);
|
|
704
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
695
705
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
} else {
|
|
699
|
-
volatiles.attacks -= 1;
|
|
700
|
-
}
|
|
701
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
702
708
|
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
709
|
+
if (volatiles.attacks == 0) {
|
|
710
|
+
const overwritten = volatiles.Confusion;
|
|
711
|
+
|
|
712
|
+
volatiles.Thrashing = false;
|
|
713
|
+
volatiles.Confusion = true;
|
|
714
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
715
|
+
|
|
716
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
717
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
718
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
719
|
+
// correct player)
|
|
720
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
721
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
722
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
723
|
+
else
|
|
724
|
+
.started);
|
|
725
|
+
if (!overwritten) options.calc.confusion(player);
|
|
714
726
|
}
|
|
727
|
+
|
|
728
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
729
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
730
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
731
|
+
|
|
715
732
|
return .skip_can;
|
|
716
733
|
}
|
|
717
734
|
|
|
718
735
|
if (volatiles.Binding) {
|
|
719
|
-
|
|
736
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
737
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
720
738
|
|
|
721
|
-
|
|
722
|
-
if (!extend) volatiles.attacks = 0;
|
|
723
|
-
} else {
|
|
724
|
-
volatiles.attacks -= 1;
|
|
725
|
-
}
|
|
726
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
727
|
-
|
|
728
|
-
try log.move(
|
|
729
|
-
battle.active(player),
|
|
730
|
-
side.last_selected_move,
|
|
731
|
-
battle.active(player.foe()),
|
|
732
|
-
side.last_selected_move,
|
|
733
|
-
);
|
|
739
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
734
740
|
if (showdown or battle.last_damage != 0) {
|
|
735
741
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
736
742
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -750,7 +756,7 @@ fn canMove(
|
|
|
750
756
|
mslot: u4,
|
|
751
757
|
auto: bool,
|
|
752
758
|
skip_pp: bool,
|
|
753
|
-
from:
|
|
759
|
+
from: Move,
|
|
754
760
|
residual: *bool,
|
|
755
761
|
options: anytype,
|
|
756
762
|
) !bool {
|
|
@@ -758,33 +764,21 @@ fn canMove(
|
|
|
758
764
|
const player_ident = battle.active(player);
|
|
759
765
|
const move = Move.get(side.last_selected_move);
|
|
760
766
|
|
|
761
|
-
const foe_last = battle.foe(player).last_used_move;
|
|
762
|
-
const special = from != null and (from.? == .Metronome and
|
|
763
|
-
(from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
|
|
764
|
-
|
|
765
|
-
var skip = skip_pp;
|
|
766
767
|
if (side.active.volatiles.Charging) {
|
|
767
768
|
side.active.volatiles.Charging = false;
|
|
768
769
|
side.active.volatiles.Invulnerable = false;
|
|
769
770
|
} else if (move.effect == .Charge) {
|
|
770
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
771
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
771
772
|
setCounterable(battle, player, side, move);
|
|
772
773
|
try Effects.charge(battle, player, options);
|
|
773
774
|
return false;
|
|
774
775
|
}
|
|
775
776
|
|
|
776
|
-
|
|
777
|
-
if (!
|
|
777
|
+
side.last_used_move = side.last_selected_move;
|
|
778
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
778
779
|
|
|
779
780
|
const target = if (move.target == .Self) player else player.foe();
|
|
780
|
-
|
|
781
|
-
// don't have space to be able to track it in all circumstances and thus usually try to infer it
|
|
782
|
-
// from side.last_used_move. However, this gets reset by the opponent switching or fainting,
|
|
783
|
-
// meaning sometimes we lose this information. from is still set because it not being null is
|
|
784
|
-
// relevant for control flow, but we don't have anything useful to put in the log. This is
|
|
785
|
-
// regrettable, but this information is more "nice to have" than required
|
|
786
|
-
const f = if (from != null and from.? == .None) null else from;
|
|
787
|
-
try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
781
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
788
782
|
setCounterable(battle, player, side, move);
|
|
789
783
|
|
|
790
784
|
if (move.effect.onBegin()) {
|
|
@@ -794,9 +788,14 @@ fn canMove(
|
|
|
794
788
|
|
|
795
789
|
if (move.effect == .Thrashing) {
|
|
796
790
|
Effects.thrashing(battle, player, options);
|
|
797
|
-
} else if (
|
|
798
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
799
|
-
|
|
791
|
+
} else if (move.effect == .Binding) {
|
|
792
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
793
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
794
|
+
if (showdown) {
|
|
795
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
796
|
+
} else {
|
|
797
|
+
try Effects.binding(battle, player, false, options);
|
|
798
|
+
}
|
|
800
799
|
}
|
|
801
800
|
|
|
802
801
|
return true;
|
|
@@ -822,16 +821,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
822
821
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
823
822
|
if (volatiles.Bide) return;
|
|
824
823
|
|
|
825
|
-
|
|
826
|
-
|
|
824
|
+
var move_slot = active.move(mslot);
|
|
825
|
+
assert(move_slot.pp > 0 or auto);
|
|
826
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
827
827
|
if (volatiles.Transform) return;
|
|
828
828
|
|
|
829
|
-
|
|
830
|
-
|
|
829
|
+
move_slot = side.stored().move(mslot);
|
|
830
|
+
assert(move_slot.pp > 0 or auto);
|
|
831
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
831
832
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
832
833
|
}
|
|
833
834
|
|
|
834
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
835
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
835
836
|
var active = &side.active;
|
|
836
837
|
const volatiles = &active.volatiles;
|
|
837
838
|
|
|
@@ -839,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
839
840
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
840
841
|
if (showdown and volatiles.Transform) return;
|
|
841
842
|
|
|
843
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
845
|
+
|
|
842
846
|
assert(mslot > 0 and mslot <= 4);
|
|
843
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
847
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
844
848
|
}
|
|
845
849
|
|
|
846
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -888,13 +892,13 @@ fn doMove(
|
|
|
888
892
|
break :miss true;
|
|
889
893
|
}
|
|
890
894
|
break :miss (move.target != .Self and
|
|
891
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
895
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
892
896
|
};
|
|
893
897
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
894
898
|
|
|
895
899
|
var late = showdown and move.effect != .Explode;
|
|
896
900
|
const skip = status or immune;
|
|
897
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
898
902
|
if (showdown and move.effect.isMulti()) {
|
|
899
903
|
try Effects.multiHit(battle, player, move, options);
|
|
900
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -916,8 +920,9 @@ fn doMove(
|
|
|
916
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
917
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
918
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
919
|
-
if (showdown) break :
|
|
920
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
923
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
924
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
925
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
921
926
|
return @as(?Result, Result.Error);
|
|
922
927
|
}
|
|
923
928
|
if (battle.last_damage == 0) {
|
|
@@ -931,11 +936,12 @@ fn doMove(
|
|
|
931
936
|
}
|
|
932
937
|
}
|
|
933
938
|
|
|
934
|
-
var
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
+
var missed: bool = undefined;
|
|
940
|
+
const zero = battle.last_damage == 0;
|
|
941
|
+
if (!showdown and !skip) {
|
|
942
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
943
|
+
miss = missed or zero;
|
|
944
|
+
}
|
|
939
945
|
|
|
940
946
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
941
947
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -959,16 +965,37 @@ fn doMove(
|
|
|
959
965
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
960
966
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
961
967
|
if (ohko) {
|
|
962
|
-
try log.immune(foe_ident, .OHKO);
|
|
968
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
963
969
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
964
|
-
try log.immune(foe_ident, .None);
|
|
970
|
+
try log.immune(.{ foe_ident, .None });
|
|
971
|
+
if (!showdown) {
|
|
972
|
+
if (move.effect == .Binding) {
|
|
973
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
974
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
975
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
976
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
977
|
+
// (considered missing)
|
|
978
|
+
assert(zero);
|
|
979
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
980
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
981
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
982
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
983
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
984
|
+
// roll)
|
|
985
|
+
try options.chance.commit(player, .binding);
|
|
986
|
+
} else {
|
|
987
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
988
|
+
// division-by-zero freeze alternative timeline scenario
|
|
989
|
+
try options.chance.commit(player, .glitch);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
965
992
|
} else if (mist) {
|
|
966
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
967
|
-
try log.fail(foe_ident, .None);
|
|
993
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
994
|
+
try log.fail(.{ foe_ident, .None });
|
|
968
995
|
} else {
|
|
969
|
-
if (showdown
|
|
970
|
-
try log.lastmiss();
|
|
971
|
-
try log.miss(battle.active(player));
|
|
996
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
997
|
+
try log.lastmiss(.{});
|
|
998
|
+
try log.miss(.{battle.active(player)});
|
|
972
999
|
}
|
|
973
1000
|
if (move.effect == .JumpKick) {
|
|
974
1001
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -981,17 +1008,15 @@ fn doMove(
|
|
|
981
1008
|
try buildRage(battle, player.foe(), options);
|
|
982
1009
|
} else if (showdown and move.effect == .Disable) {
|
|
983
1010
|
try buildRage(battle, player.foe(), options);
|
|
984
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
985
|
-
try options.chance.commit(player, .binding);
|
|
986
1011
|
}
|
|
987
1012
|
return null;
|
|
988
|
-
} else {
|
|
1013
|
+
} else if (!showdown) {
|
|
989
1014
|
try options.chance.commit(player, .hit);
|
|
990
1015
|
}
|
|
991
1016
|
|
|
992
1017
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
993
1018
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
994
|
-
// it up above before damage calculation)
|
|
1019
|
+
// it up above before damage calculation)
|
|
995
1020
|
if (!showdown and move.effect.isMulti()) {
|
|
996
1021
|
try Effects.multiHit(battle, player, move, options);
|
|
997
1022
|
hits = side.active.volatiles.attacks;
|
|
@@ -1004,15 +1029,15 @@ fn doMove(
|
|
|
1004
1029
|
var hit: u4 = 0;
|
|
1005
1030
|
while (hit < hits) {
|
|
1006
1031
|
if (hit == 0) {
|
|
1007
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1032
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1008
1033
|
if (effectiveness > Effectiveness.neutral) {
|
|
1009
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1034
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1010
1035
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1011
|
-
try log.resisted(battle.active(player.foe()));
|
|
1036
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1012
1037
|
}
|
|
1013
1038
|
}
|
|
1014
1039
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1015
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1040
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1016
1041
|
hit += 1;
|
|
1017
1042
|
if (foe.stored().hp == 0) break;
|
|
1018
1043
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1029,33 +1054,31 @@ fn doMove(
|
|
|
1029
1054
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1030
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1031
1056
|
}
|
|
1032
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1033
|
-
} else if (showdown) {
|
|
1057
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1034
1059
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
try buildRage(battle, player.foe(), options);
|
|
1046
|
-
}
|
|
1047
|
-
return null;
|
|
1060
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1061
|
+
if (immune) {
|
|
1062
|
+
battle.last_damage = 0;
|
|
1063
|
+
assert(foe.stored().hp > 0);
|
|
1064
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1065
|
+
if (sub) {
|
|
1066
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1067
|
+
} else {
|
|
1068
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1069
|
+
try buildRage(battle, player.foe(), options);
|
|
1048
1070
|
}
|
|
1071
|
+
return null;
|
|
1049
1072
|
}
|
|
1050
1073
|
}
|
|
1051
1074
|
|
|
1052
1075
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1053
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1076
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1054
1077
|
if (nullified) return null;
|
|
1055
1078
|
|
|
1056
1079
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1057
1080
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1058
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1081
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1059
1082
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1060
1083
|
|
|
1061
1084
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1070,7 +1093,7 @@ fn doMove(
|
|
|
1070
1093
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1071
1094
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1072
1095
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1073
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1096
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1074
1097
|
if (move.effect == .Twineedle) {
|
|
1075
1098
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1076
1099
|
} else if (move.effect == .Disable) {
|
|
@@ -1089,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1089
1112
|
const side = battle.side(player);
|
|
1090
1113
|
|
|
1091
1114
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1092
|
-
var rate =
|
|
1115
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1093
1116
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1094
1117
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1095
1118
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1096
1119
|
|
|
1097
|
-
|
|
1120
|
+
const spurious =
|
|
1121
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1122
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1098
1123
|
}
|
|
1099
1124
|
|
|
1100
1125
|
fn calcDamage(
|
|
@@ -1103,6 +1128,7 @@ fn calcDamage(
|
|
|
1103
1128
|
target_player: Player,
|
|
1104
1129
|
m: ?Move.Data,
|
|
1105
1130
|
crit: bool,
|
|
1131
|
+
options: anytype,
|
|
1106
1132
|
) bool {
|
|
1107
1133
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1108
1134
|
const cfz = m == null;
|
|
@@ -1114,28 +1140,8 @@ fn calcDamage(
|
|
|
1114
1140
|
|
|
1115
1141
|
const special = move.type.special();
|
|
1116
1142
|
|
|
1117
|
-
|
|
1118
|
-
var
|
|
1119
|
-
if (crit)
|
|
1120
|
-
if (special) side.stored().stats.spc
|
|
1121
|
-
else side.stored().stats.atk
|
|
1122
|
-
else
|
|
1123
|
-
if (special) side.active.stats.spc
|
|
1124
|
-
else side.active.stats.atk;
|
|
1125
|
-
var def: u32 =
|
|
1126
|
-
if (crit)
|
|
1127
|
-
if (special) target.stored().stats.spc
|
|
1128
|
-
else target.stored().stats.def
|
|
1129
|
-
else
|
|
1130
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1131
|
-
if (special)
|
|
1132
|
-
target.active.stats.spc *
|
|
1133
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1134
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1135
|
-
else
|
|
1136
|
-
target.active.stats.def *
|
|
1137
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1138
|
-
// zig fmt: on
|
|
1143
|
+
var atk = attack(side, crit, special);
|
|
1144
|
+
var def = defense(target, crit, special, cfz);
|
|
1139
1145
|
|
|
1140
1146
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1141
1147
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1143,12 +1149,12 @@ fn calcDamage(
|
|
|
1143
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1144
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1145
1151
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1146
|
-
def = @max((def / 4) & 255, if (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1147
1153
|
}
|
|
1148
1154
|
|
|
1149
|
-
const lvl =
|
|
1155
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1150
1156
|
|
|
1151
|
-
def =
|
|
1157
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1152
1158
|
|
|
1153
1159
|
if (def == 0) return false;
|
|
1154
1160
|
|
|
@@ -1160,11 +1166,41 @@ fn calcDamage(
|
|
|
1160
1166
|
d = @min(997, d);
|
|
1161
1167
|
d += 2;
|
|
1162
1168
|
|
|
1169
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1170
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1171
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1172
|
+
// that results in counter intuitive probabilities in the common case
|
|
1173
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1174
|
+
atk = attack(side, !crit, special);
|
|
1175
|
+
def = defense(target, !crit, special, false);
|
|
1176
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1163
1179
|
battle.last_damage = @intCast(d);
|
|
1164
1180
|
|
|
1165
1181
|
return true;
|
|
1166
1182
|
}
|
|
1167
1183
|
|
|
1184
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1185
|
+
return if (crit)
|
|
1186
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1187
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1191
|
+
return if (crit)
|
|
1192
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1193
|
+
else
|
|
1194
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1195
|
+
if (special)
|
|
1196
|
+
target.active.stats.spc *
|
|
1197
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1198
|
+
else
|
|
1199
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1200
|
+
target.active.stats.def *
|
|
1201
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1168
1204
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1169
1205
|
const side = battle.side(player);
|
|
1170
1206
|
const foe = battle.foe(player);
|
|
@@ -1209,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1209
1245
|
const foe = battle.foe(player);
|
|
1210
1246
|
|
|
1211
1247
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1212
|
-
try options.chance.commit(player, .hit);
|
|
1213
1248
|
|
|
1214
1249
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1215
1250
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1220,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1220
1255
|
.Psywave => power: {
|
|
1221
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1222
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1223
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1224
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1225
1260
|
},
|
|
1226
1261
|
else => unreachable,
|
|
1227
1262
|
};
|
|
1228
1263
|
|
|
1229
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1230
1265
|
|
|
1231
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1232
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1239,7 +1274,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1239
1274
|
const foe = battle.foe(player);
|
|
1240
1275
|
|
|
1241
1276
|
if (battle.last_damage == 0) {
|
|
1242
|
-
try options.log.fail(battle.active(player), .None);
|
|
1277
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1243
1278
|
return null;
|
|
1244
1279
|
}
|
|
1245
1280
|
|
|
@@ -1257,22 +1292,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1257
1292
|
foe_last_selected_move.type == .Fighting);
|
|
1258
1293
|
|
|
1259
1294
|
if (!used and !selected) {
|
|
1260
|
-
try options.log.fail(battle.active(player), .None);
|
|
1295
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1261
1296
|
return null;
|
|
1262
1297
|
}
|
|
1263
1298
|
|
|
1264
1299
|
if (!used or !selected) {
|
|
1265
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1266
|
-
if (!
|
|
1267
|
-
try options.log.fail(battle.active(player), .None);
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1302
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1268
1303
|
return null;
|
|
1269
1304
|
}
|
|
1270
1305
|
|
|
1271
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1272
1311
|
|
|
1273
|
-
// Pokémon Showdown calls
|
|
1312
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1274
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1275
|
-
try options.chance.commit(player, .hit);
|
|
1276
1314
|
|
|
1277
1315
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1278
1316
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1300,20 +1338,20 @@ fn applyDamage(
|
|
|
1300
1338
|
subbed.active.volatiles.substitute = 0;
|
|
1301
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1302
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1303
|
-
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1304
|
-
options.calc.capped(
|
|
1341
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1305
1343
|
return true;
|
|
1306
1344
|
} else {
|
|
1307
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1308
1346
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1309
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1347
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1310
1348
|
return false;
|
|
1311
1349
|
}
|
|
1312
1350
|
}
|
|
1313
1351
|
|
|
1314
1352
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1315
1353
|
target.stored().hp -= battle.last_damage;
|
|
1316
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1354
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1317
1355
|
return false;
|
|
1318
1356
|
}
|
|
1319
1357
|
|
|
@@ -1332,10 +1370,10 @@ fn mirrorMove(
|
|
|
1332
1370
|
side.last_selected_move = foe.last_used_move;
|
|
1333
1371
|
|
|
1334
1372
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1335
|
-
try options.log.fail(battle.active(player), .None);
|
|
1373
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1336
1374
|
return null;
|
|
1337
1375
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1338
|
-
incrementPP(side, mslot);
|
|
1376
|
+
incrementPP(side, player, mslot, options);
|
|
1339
1377
|
}
|
|
1340
1378
|
|
|
1341
1379
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1356,7 +1394,7 @@ fn metronome(
|
|
|
1356
1394
|
var side = battle.side(player);
|
|
1357
1395
|
|
|
1358
1396
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1359
|
-
incrementPP(side, mslot);
|
|
1397
|
+
incrementPP(side, player, mslot, options);
|
|
1360
1398
|
|
|
1361
1399
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1362
1400
|
return null;
|
|
@@ -1368,20 +1406,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1368
1406
|
var immune = false;
|
|
1369
1407
|
var mist = false;
|
|
1370
1408
|
|
|
1371
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1409
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1410
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1372
1413
|
|
|
1373
1414
|
assert(!immune);
|
|
1374
1415
|
if (mist) {
|
|
1375
1416
|
assert(!showdown);
|
|
1376
1417
|
const foe_ident = battle.active(player.foe());
|
|
1377
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1378
|
-
try options.log.fail(foe_ident, .None);
|
|
1418
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1419
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1379
1420
|
} else {
|
|
1380
|
-
try options.
|
|
1381
|
-
try options.log.
|
|
1382
|
-
try options.log.miss(battle.active(player));
|
|
1421
|
+
try options.log.lastmiss(.{});
|
|
1422
|
+
try options.log.miss(.{battle.active(player)});
|
|
1383
1423
|
}
|
|
1384
1424
|
|
|
1425
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1385
1426
|
return false;
|
|
1386
1427
|
}
|
|
1387
1428
|
|
|
@@ -1392,7 +1433,7 @@ fn moveHit(
|
|
|
1392
1433
|
immune: *bool,
|
|
1393
1434
|
mist: *bool,
|
|
1394
1435
|
options: anytype,
|
|
1395
|
-
) bool {
|
|
1436
|
+
) !bool {
|
|
1396
1437
|
var side = battle.side(player);
|
|
1397
1438
|
const foe = battle.foe(player);
|
|
1398
1439
|
|
|
@@ -1431,7 +1472,7 @@ fn moveHit(
|
|
|
1431
1472
|
const overwritten = overwrite and state > 0;
|
|
1432
1473
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1433
1474
|
|
|
1434
|
-
var accuracy = if (overwritten) state else
|
|
1475
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1435
1476
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1436
1477
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1437
1478
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1443,7 +1484,7 @@ fn moveHit(
|
|
|
1443
1484
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1444
1485
|
|
|
1445
1486
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1446
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1487
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1447
1488
|
};
|
|
1448
1489
|
|
|
1449
1490
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1454,13 +1495,17 @@ fn moveHit(
|
|
|
1454
1495
|
|
|
1455
1496
|
if (!miss) return true;
|
|
1456
1497
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1498
|
+
|
|
1457
1499
|
side.active.volatiles.Binding = false;
|
|
1500
|
+
options.chance.observe(.binding, player, .None);
|
|
1501
|
+
|
|
1458
1502
|
return false;
|
|
1459
1503
|
}
|
|
1460
1504
|
|
|
1461
1505
|
fn checkFaint(
|
|
1462
1506
|
battle: anytype,
|
|
1463
1507
|
player: Player,
|
|
1508
|
+
cap: bool,
|
|
1464
1509
|
options: anytype,
|
|
1465
1510
|
) @TypeOf(options.log).Error!?Result {
|
|
1466
1511
|
const side = battle.side(player);
|
|
@@ -1470,24 +1515,23 @@ fn checkFaint(
|
|
|
1470
1515
|
const foe_fainted = foe.stored().hp == 0;
|
|
1471
1516
|
|
|
1472
1517
|
const player_out = findFirstAlive(side) == 0;
|
|
1473
|
-
const foe_out = findFirstAlive(foe) == 0;
|
|
1474
|
-
const
|
|
1475
|
-
const more = tie or player_out or foe_out;
|
|
1518
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1519
|
+
const more = player_out or foe_out;
|
|
1476
1520
|
|
|
1477
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1478
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1521
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1522
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1479
1523
|
|
|
1480
1524
|
assert(!side.active.volatiles.MultiHit);
|
|
1481
1525
|
assert(!foe.active.volatiles.MultiHit);
|
|
1482
1526
|
|
|
1483
|
-
if (
|
|
1484
|
-
try options.log.tie();
|
|
1527
|
+
if (player_out and foe_out) {
|
|
1528
|
+
try options.log.tie(.{});
|
|
1485
1529
|
return Result.Tie;
|
|
1486
1530
|
} else if (player_out) {
|
|
1487
|
-
try options.log.win(player.foe());
|
|
1531
|
+
try options.log.win(.{player.foe()});
|
|
1488
1532
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1489
1533
|
} else if (foe_out) {
|
|
1490
|
-
try options.log.win(player);
|
|
1534
|
+
try options.log.win(.{player});
|
|
1491
1535
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1492
1536
|
}
|
|
1493
1537
|
|
|
@@ -1496,7 +1540,7 @@ fn checkFaint(
|
|
|
1496
1540
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1497
1541
|
}
|
|
1498
1542
|
|
|
1499
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1543
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1500
1544
|
var side = battle.side(player);
|
|
1501
1545
|
var foe = battle.foe(player);
|
|
1502
1546
|
assert(side.stored().hp == 0);
|
|
@@ -1505,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1505
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1506
1550
|
if (foe_volatiles.Bide) {
|
|
1507
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1508
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1509
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1510
1554
|
}
|
|
1511
1555
|
|
|
@@ -1529,8 +1573,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1529
1573
|
side.stored().stats.spe;
|
|
1530
1574
|
}
|
|
1531
1575
|
|
|
1532
|
-
try options.log.faint(battle.active(player), done);
|
|
1533
|
-
|
|
1576
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1577
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1578
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1579
|
+
if (cap) options.calc.capped(player);
|
|
1534
1580
|
return null;
|
|
1535
1581
|
}
|
|
1536
1582
|
|
|
@@ -1551,8 +1597,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1551
1597
|
|
|
1552
1598
|
if (!showdown or damage > 0) {
|
|
1553
1599
|
stored.hp -= damage;
|
|
1554
|
-
// Pokémon Showdown uses
|
|
1555
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1600
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1601
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1556
1602
|
}
|
|
1557
1603
|
}
|
|
1558
1604
|
|
|
@@ -1577,14 +1623,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1577
1623
|
const amount = @min(damage, stored.hp);
|
|
1578
1624
|
stored.hp -= amount;
|
|
1579
1625
|
|
|
1580
|
-
// As above, Pokémon Showdown uses
|
|
1581
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1626
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1627
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1582
1628
|
|
|
1583
1629
|
const before = foe_stored.hp;
|
|
1584
1630
|
// Uncapped damage is added back to the foe
|
|
1585
1631
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1586
1632
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1587
|
-
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1633
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1588
1634
|
}
|
|
1589
1635
|
}
|
|
1590
1636
|
|
|
@@ -1594,19 +1640,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1594
1640
|
|
|
1595
1641
|
battle.turn += 1;
|
|
1596
1642
|
|
|
1597
|
-
if (
|
|
1598
|
-
try options.log.tie();
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1644
|
+
try options.log.tie(.{});
|
|
1599
1645
|
return Result.Tie;
|
|
1600
1646
|
}
|
|
1601
1647
|
|
|
1602
|
-
if (
|
|
1603
|
-
try options.log.tie();
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1649
|
+
try options.log.tie(.{});
|
|
1604
1650
|
return Result.Tie;
|
|
1605
1651
|
} else if (battle.turn >= 65535) {
|
|
1606
1652
|
return Result.Error;
|
|
1607
1653
|
}
|
|
1608
1654
|
|
|
1609
|
-
try options.log.turn(battle.turn);
|
|
1655
|
+
try options.log.turn(.{battle.turn});
|
|
1610
1656
|
|
|
1611
1657
|
return Result.Default;
|
|
1612
1658
|
}
|
|
@@ -1666,9 +1712,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1666
1712
|
}
|
|
1667
1713
|
|
|
1668
1714
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1669
|
-
if (side.active.volatiles.
|
|
1715
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1670
1716
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1671
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1717
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1672
1718
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1673
1719
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1674
1720
|
m
|
|
@@ -1676,32 +1722,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1676
1722
|
side.last_selected_move;
|
|
1677
1723
|
if (m != .None and m == last) {
|
|
1678
1724
|
side.active.volatiles.Charging = false;
|
|
1679
|
-
try options.log.
|
|
1725
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1680
1726
|
return true;
|
|
1681
1727
|
}
|
|
1682
1728
|
}
|
|
1683
1729
|
return false;
|
|
1684
1730
|
}
|
|
1685
1731
|
|
|
1686
|
-
|
|
1732
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1687
1733
|
const side = battle.side(who);
|
|
1688
1734
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1689
1735
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1690
1736
|
}
|
|
1691
1737
|
}
|
|
1692
1738
|
|
|
1693
|
-
fn
|
|
1694
|
-
var volatiles = &active.volatiles;
|
|
1695
|
-
assert(volatiles.Thrashing);
|
|
1696
|
-
if (volatiles.attacks > 0) return false;
|
|
1697
|
-
|
|
1698
|
-
volatiles.Thrashing = false;
|
|
1699
|
-
volatiles.Confusion = true;
|
|
1700
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1701
|
-
return true;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
inline fn onBegin(
|
|
1739
|
+
fn onBegin(
|
|
1705
1740
|
battle: anytype,
|
|
1706
1741
|
player: Player,
|
|
1707
1742
|
move: Move.Data,
|
|
@@ -1709,7 +1744,6 @@ inline fn onBegin(
|
|
|
1709
1744
|
residual: *bool,
|
|
1710
1745
|
options: anytype,
|
|
1711
1746
|
) !void {
|
|
1712
|
-
assert(move.effect.onBegin());
|
|
1713
1747
|
return switch (move.effect) {
|
|
1714
1748
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1715
1749
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1731,8 +1765,7 @@ inline fn onBegin(
|
|
|
1731
1765
|
};
|
|
1732
1766
|
}
|
|
1733
1767
|
|
|
1734
|
-
|
|
1735
|
-
assert(move.effect.onEnd());
|
|
1768
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1736
1769
|
return switch (move.effect) {
|
|
1737
1770
|
.Bide => Effects.bide(battle, player, options),
|
|
1738
1771
|
// zig fmt: off
|
|
@@ -1747,14 +1780,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1747
1780
|
};
|
|
1748
1781
|
}
|
|
1749
1782
|
|
|
1750
|
-
|
|
1783
|
+
fn alwaysHappens(
|
|
1751
1784
|
battle: anytype,
|
|
1752
1785
|
player: Player,
|
|
1753
1786
|
move: Move.Data,
|
|
1754
1787
|
residual: *bool,
|
|
1755
1788
|
options: anytype,
|
|
1756
1789
|
) !void {
|
|
1757
|
-
assert(move.effect.alwaysHappens());
|
|
1758
1790
|
return switch (move.effect) {
|
|
1759
1791
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1760
1792
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1766,7 +1798,7 @@ inline fn alwaysHappens(
|
|
|
1766
1798
|
};
|
|
1767
1799
|
}
|
|
1768
1800
|
|
|
1769
|
-
|
|
1801
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1770
1802
|
return switch (move.effect) {
|
|
1771
1803
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1772
1804
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1790,10 +1822,12 @@ pub const Effects = struct {
|
|
|
1790
1822
|
|
|
1791
1823
|
side.active.volatiles.Bide = true;
|
|
1792
1824
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1825
|
+
|
|
1793
1826
|
side.active.volatiles.state = 0;
|
|
1794
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1795
1829
|
|
|
1796
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1830
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1797
1831
|
}
|
|
1798
1832
|
|
|
1799
1833
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1807,11 +1841,11 @@ pub const Effects = struct {
|
|
|
1807
1841
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1808
1842
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1809
1843
|
assert(move.type == .Fire);
|
|
1810
|
-
try options.log.curestatus(
|
|
1844
|
+
try options.log.curestatus(.{
|
|
1811
1845
|
battle.active(player.foe()),
|
|
1812
1846
|
foe_stored.status,
|
|
1813
1847
|
.Message,
|
|
1814
|
-
);
|
|
1848
|
+
});
|
|
1815
1849
|
foe_stored.status = 0;
|
|
1816
1850
|
}
|
|
1817
1851
|
return;
|
|
@@ -1825,7 +1859,7 @@ pub const Effects = struct {
|
|
|
1825
1859
|
foe_stored.status = Status.init(.BRN);
|
|
1826
1860
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1827
1861
|
|
|
1828
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1862
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1829
1863
|
}
|
|
1830
1864
|
|
|
1831
1865
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1835,8 +1869,8 @@ pub const Effects = struct {
|
|
|
1835
1869
|
volatiles.Charging = true;
|
|
1836
1870
|
const move = side.last_selected_move;
|
|
1837
1871
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1838
|
-
try options.log.laststill();
|
|
1839
|
-
try options.log.prepare(battle.active(player), move);
|
|
1872
|
+
try options.log.laststill(.{});
|
|
1873
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1840
1874
|
}
|
|
1841
1875
|
|
|
1842
1876
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1847,42 +1881,39 @@ pub const Effects = struct {
|
|
|
1847
1881
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1848
1882
|
} else {
|
|
1849
1883
|
if (showdown) {
|
|
1850
|
-
if (!try checkHit(battle, player, move, options))
|
|
1851
|
-
|
|
1852
|
-
} else if (sub) {
|
|
1853
|
-
return options.log.fail(battle.active(player.foe()), .None);
|
|
1854
|
-
}
|
|
1884
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1885
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1855
1886
|
} else {
|
|
1856
|
-
if (sub) {
|
|
1857
|
-
|
|
1858
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1887
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1888
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1861
1889
|
}
|
|
1862
|
-
try options.chance.commit(player, .hit);
|
|
1863
1890
|
}
|
|
1864
1891
|
|
|
1865
1892
|
if (foe.active.volatiles.Confusion) return;
|
|
1866
1893
|
foe.active.volatiles.Confusion = true;
|
|
1867
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1868
1894
|
|
|
1869
|
-
|
|
1895
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1896
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1898
|
+
|
|
1899
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1870
1900
|
}
|
|
1871
1901
|
|
|
1872
1902
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1873
1903
|
const foe = battle.foe(player);
|
|
1874
1904
|
|
|
1875
1905
|
if (foe.active.volatiles.Invulnerable) {
|
|
1876
|
-
try options.log.lastmiss();
|
|
1877
|
-
return options.log.miss(battle.active(player));
|
|
1906
|
+
try options.log.lastmiss(.{});
|
|
1907
|
+
return options.log.miss(.{battle.active(player)});
|
|
1878
1908
|
}
|
|
1879
1909
|
|
|
1880
1910
|
battle.side(player).active.types = foe.active.types;
|
|
1881
|
-
return options.log.
|
|
1911
|
+
return options.log.start(.{
|
|
1882
1912
|
battle.active(player),
|
|
1913
|
+
.TypeChange,
|
|
1883
1914
|
foe.active.types,
|
|
1884
1915
|
battle.active(player.foe()),
|
|
1885
|
-
);
|
|
1916
|
+
});
|
|
1886
1917
|
}
|
|
1887
1918
|
|
|
1888
1919
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1891,13 +1922,10 @@ pub const Effects = struct {
|
|
|
1891
1922
|
const foe_ident = battle.active(player.foe());
|
|
1892
1923
|
|
|
1893
1924
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1894
|
-
if (!showdown)
|
|
1895
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1896
|
-
try options.chance.commit(player, .hit);
|
|
1897
|
-
}
|
|
1925
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1898
1926
|
|
|
1899
|
-
if (volatiles.
|
|
1900
|
-
try options.log.fail(foe_ident, .None);
|
|
1927
|
+
if (volatiles.disable_move != 0) {
|
|
1928
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1901
1929
|
return null;
|
|
1902
1930
|
}
|
|
1903
1931
|
|
|
@@ -1912,27 +1940,27 @@ pub const Effects = struct {
|
|
|
1912
1940
|
|
|
1913
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1914
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1915
|
-
if (n == 0) {
|
|
1916
|
-
try options.log.fail(foe_ident, .None);
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1944
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1917
1945
|
return null;
|
|
1918
1946
|
} else if (err) {
|
|
1919
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1920
|
-
assert(!showdown);
|
|
1921
1948
|
return Result.Error;
|
|
1922
1949
|
}
|
|
1923
1950
|
|
|
1924
|
-
volatiles.
|
|
1951
|
+
volatiles.disable_move =
|
|
1925
1952
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1926
|
-
volatiles.
|
|
1953
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1954
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1927
1955
|
|
|
1928
|
-
const id = foe.active.move(volatiles.
|
|
1929
|
-
try options.log.
|
|
1956
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1957
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1930
1958
|
return null;
|
|
1931
1959
|
}
|
|
1932
1960
|
|
|
1933
1961
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1934
|
-
var
|
|
1935
|
-
|
|
1962
|
+
var stored = battle.side(player).stored();
|
|
1963
|
+
const ident = battle.active(player);
|
|
1936
1964
|
|
|
1937
1965
|
if (battle.last_damage == 0) {
|
|
1938
1966
|
assert(showdown);
|
|
@@ -1945,7 +1973,7 @@ pub const Effects = struct {
|
|
|
1945
1973
|
if (stored.hp == stored.stats.hp) return;
|
|
1946
1974
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1947
1975
|
|
|
1948
|
-
try options.log.
|
|
1976
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1949
1977
|
}
|
|
1950
1978
|
|
|
1951
1979
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1977,7 +2005,7 @@ pub const Effects = struct {
|
|
|
1977
2005
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1978
2006
|
side.active.volatiles.FocusEnergy = true;
|
|
1979
2007
|
|
|
1980
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2008
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1981
2009
|
}
|
|
1982
2010
|
|
|
1983
2011
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1994,12 +2022,12 @@ pub const Effects = struct {
|
|
|
1994
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
1995
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1996
2024
|
// Freeze Clause Mod
|
|
1997
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1998
2026
|
|
|
1999
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
2000
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2001
2029
|
|
|
2002
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2030
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2003
2031
|
}
|
|
2004
2032
|
|
|
2005
2033
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2007,7 +2035,7 @@ pub const Effects = struct {
|
|
|
2007
2035
|
var side = battle.side(player);
|
|
2008
2036
|
var foe = battle.foe(player);
|
|
2009
2037
|
|
|
2010
|
-
|
|
2038
|
+
const side_stored = side.stored();
|
|
2011
2039
|
var foe_stored = foe.stored();
|
|
2012
2040
|
|
|
2013
2041
|
const player_ident = battle.active(player);
|
|
@@ -2019,8 +2047,8 @@ pub const Effects = struct {
|
|
|
2019
2047
|
side.active.stats = side_stored.stats;
|
|
2020
2048
|
foe.active.stats = foe_stored.stats;
|
|
2021
2049
|
|
|
2022
|
-
try log.activate(player_ident, .Haze);
|
|
2023
|
-
try log.clearallboost();
|
|
2050
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2051
|
+
try log.clearallboost(.{});
|
|
2024
2052
|
|
|
2025
2053
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2026
2054
|
if (showdown) {
|
|
@@ -2028,24 +2056,26 @@ pub const Effects = struct {
|
|
|
2028
2056
|
const p: Player = @enumFromInt(i);
|
|
2029
2057
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2030
2058
|
if (p != player and Status.any(s.stored().status)) {
|
|
2031
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2059
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2032
2060
|
s.stored().status = 0;
|
|
2061
|
+
options.chance.observe(.sleep, p, .None);
|
|
2033
2062
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2034
2063
|
s.stored().status = Status.init(.PSN);
|
|
2035
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2064
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2036
2065
|
}
|
|
2037
|
-
try clearVolatiles(battle, p,
|
|
2066
|
+
try clearVolatiles(battle, p, options);
|
|
2038
2067
|
}
|
|
2039
2068
|
} else {
|
|
2040
2069
|
if (Status.any(foe_stored.status)) {
|
|
2041
2070
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2042
2071
|
foe.last_selected_move = .SKIP_TURN;
|
|
2072
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2043
2073
|
}
|
|
2044
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2074
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2045
2075
|
foe_stored.status = 0;
|
|
2046
2076
|
}
|
|
2047
|
-
try clearVolatiles(battle, player,
|
|
2048
|
-
try clearVolatiles(battle, player.foe(),
|
|
2077
|
+
try clearVolatiles(battle, player, options);
|
|
2078
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2049
2079
|
}
|
|
2050
2080
|
}
|
|
2051
2081
|
|
|
@@ -2057,7 +2087,7 @@ pub const Effects = struct {
|
|
|
2057
2087
|
// GLITCH: HP recovery move failure glitches
|
|
2058
2088
|
const delta = stored.stats.hp - stored.hp;
|
|
2059
2089
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2060
|
-
return
|
|
2090
|
+
return options.log.fail(.{ ident, .None });
|
|
2061
2091
|
}
|
|
2062
2092
|
|
|
2063
2093
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2065,17 +2095,17 @@ pub const Effects = struct {
|
|
|
2065
2095
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2066
2096
|
if (showdown) battle.rng.advance(1);
|
|
2067
2097
|
stored.status = Status.slf(2);
|
|
2068
|
-
try options.log.
|
|
2098
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2069
2099
|
stored.hp = stored.stats.hp;
|
|
2070
2100
|
} else {
|
|
2071
2101
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2072
2102
|
}
|
|
2073
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2103
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2074
2104
|
}
|
|
2075
2105
|
|
|
2076
2106
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2077
2107
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2078
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2108
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2079
2109
|
}
|
|
2080
2110
|
|
|
2081
2111
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2084,33 +2114,31 @@ pub const Effects = struct {
|
|
|
2084
2114
|
if (showdown) {
|
|
2085
2115
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2086
2116
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2087
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2117
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2088
2118
|
}
|
|
2089
2119
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2090
2120
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2091
2121
|
} else {
|
|
2092
2122
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2093
2123
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2094
|
-
try options.log.lastmiss();
|
|
2095
|
-
return options.log.miss(battle.active(player));
|
|
2124
|
+
try options.log.lastmiss(.{});
|
|
2125
|
+
return options.log.miss(.{battle.active(player)});
|
|
2096
2126
|
}
|
|
2097
2127
|
}
|
|
2098
2128
|
|
|
2099
|
-
try options.chance.commit(player, .hit);
|
|
2100
2129
|
foe.active.volatiles.LeechSeed = true;
|
|
2101
2130
|
|
|
2102
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2131
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2103
2132
|
}
|
|
2104
2133
|
|
|
2105
2134
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2106
2135
|
var side = battle.side(player);
|
|
2136
|
+
const ident = battle.active(player);
|
|
2107
2137
|
|
|
2108
|
-
if (side.active.volatiles.LightScreen) {
|
|
2109
|
-
return options.log.fail(battle.active(player), .None);
|
|
2110
|
-
}
|
|
2138
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2111
2139
|
side.active.volatiles.LightScreen = true;
|
|
2112
2140
|
|
|
2113
|
-
try options.log.start(
|
|
2141
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2114
2142
|
}
|
|
2115
2143
|
|
|
2116
2144
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2138,17 +2166,15 @@ pub const Effects = struct {
|
|
|
2138
2166
|
if (!has_mimic) {
|
|
2139
2167
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2140
2168
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2141
|
-
|
|
2142
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2169
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2143
2170
|
}
|
|
2144
2171
|
}
|
|
2145
2172
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2146
|
-
try options.chance.commit(player, .hit);
|
|
2147
2173
|
|
|
2148
2174
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2149
2175
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2150
2176
|
|
|
2151
|
-
try options.log.
|
|
2177
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2152
2178
|
}
|
|
2153
2179
|
|
|
2154
2180
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2157,7 +2183,7 @@ pub const Effects = struct {
|
|
|
2157
2183
|
if (side.active.volatiles.Mist) return;
|
|
2158
2184
|
side.active.volatiles.Mist = true;
|
|
2159
2185
|
|
|
2160
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2186
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2161
2187
|
}
|
|
2162
2188
|
|
|
2163
2189
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2183,25 +2209,25 @@ pub const Effects = struct {
|
|
|
2183
2209
|
|
|
2184
2210
|
if (showdown) {
|
|
2185
2211
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2186
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2187
|
-
|
|
2212
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2213
|
+
return log.immune(.{ foe_ident, .None });
|
|
2214
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2188
2215
|
}
|
|
2189
2216
|
if (Status.any(foe_stored.status)) {
|
|
2190
|
-
return log.fail(
|
|
2217
|
+
return log.fail(.{
|
|
2191
2218
|
foe_ident,
|
|
2192
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2193
|
-
);
|
|
2219
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2220
|
+
});
|
|
2194
2221
|
}
|
|
2195
2222
|
if (!showdown) {
|
|
2196
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2223
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2197
2224
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2198
2225
|
}
|
|
2199
|
-
try options.chance.commit(player, .hit);
|
|
2200
2226
|
|
|
2201
2227
|
foe_stored.status = Status.init(.PAR);
|
|
2202
2228
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2203
2229
|
|
|
2204
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2230
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2205
2231
|
}
|
|
2206
2232
|
|
|
2207
2233
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2223,12 +2249,12 @@ pub const Effects = struct {
|
|
|
2223
2249
|
foe_stored.status = Status.init(.PAR);
|
|
2224
2250
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2225
2251
|
|
|
2226
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2252
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2227
2253
|
}
|
|
2228
2254
|
|
|
2229
2255
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2230
2256
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2231
|
-
try options.log.fieldactivate();
|
|
2257
|
+
try options.log.fieldactivate(.{});
|
|
2232
2258
|
}
|
|
2233
2259
|
}
|
|
2234
2260
|
|
|
@@ -2243,28 +2269,27 @@ pub const Effects = struct {
|
|
|
2243
2269
|
return;
|
|
2244
2270
|
} else if (foe.active.volatiles.Substitute) {
|
|
2245
2271
|
if (move.effect != .Poison) return;
|
|
2246
|
-
return log.fail(foe_ident, .None);
|
|
2272
|
+
return log.fail(.{ foe_ident, .None });
|
|
2247
2273
|
} else if (Status.any(foe_stored.status)) {
|
|
2248
2274
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2249
2275
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2250
2276
|
// will not include a fail reason for Toxic vs. Poison or vice-versa...
|
|
2251
|
-
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2277
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2252
2278
|
if (!showdown)
|
|
2253
|
-
.Poison
|
|
2279
|
+
Fail.Poison
|
|
2254
2280
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2255
|
-
if (toxic) .Toxic else .Poison
|
|
2281
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2256
2282
|
else
|
|
2257
|
-
.None
|
|
2283
|
+
Fail.None
|
|
2258
2284
|
else
|
|
2259
|
-
.None);
|
|
2285
|
+
Fail.None });
|
|
2260
2286
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2261
2287
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2262
|
-
return log.immune(foe_ident, .None);
|
|
2288
|
+
return log.immune(.{ foe_ident, .None });
|
|
2263
2289
|
}
|
|
2264
2290
|
|
|
2265
2291
|
if (move.effect == .Poison) {
|
|
2266
2292
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2267
|
-
try options.chance.commit(player, .hit);
|
|
2268
2293
|
} else {
|
|
2269
2294
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2270
2295
|
return;
|
|
@@ -2278,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2278
2303
|
foe.active.volatiles.toxic = 0;
|
|
2279
2304
|
}
|
|
2280
2305
|
|
|
2281
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2306
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2282
2307
|
}
|
|
2283
2308
|
|
|
2284
2309
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2298,22 +2323,23 @@ pub const Effects = struct {
|
|
|
2298
2323
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2299
2324
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2300
2325
|
|
|
2301
|
-
try options.log.
|
|
2326
|
+
try options.log.damage(.{
|
|
2302
2327
|
battle.active(player),
|
|
2303
2328
|
stored,
|
|
2304
2329
|
.RecoilOf,
|
|
2305
2330
|
battle.active(player.foe()),
|
|
2306
|
-
);
|
|
2331
|
+
});
|
|
2307
2332
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2308
2333
|
}
|
|
2309
2334
|
|
|
2310
2335
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2311
2336
|
var side = battle.side(player);
|
|
2337
|
+
const ident = battle.active(player);
|
|
2312
2338
|
|
|
2313
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2339
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2314
2340
|
side.active.volatiles.Reflect = true;
|
|
2315
2341
|
|
|
2316
|
-
try options.log.start(
|
|
2342
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2317
2343
|
}
|
|
2318
2344
|
|
|
2319
2345
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2327,18 +2353,17 @@ pub const Effects = struct {
|
|
|
2327
2353
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2328
2354
|
} else {
|
|
2329
2355
|
if (Status.any(foe_stored.status)) {
|
|
2330
|
-
return options.log.fail(
|
|
2356
|
+
return options.log.fail(.{
|
|
2331
2357
|
foe_ident,
|
|
2332
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2333
|
-
);
|
|
2358
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2359
|
+
});
|
|
2334
2360
|
}
|
|
2335
|
-
// If
|
|
2361
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2336
2362
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2337
|
-
try options.chance.commit(player, .hit);
|
|
2338
2363
|
}
|
|
2339
2364
|
|
|
2340
2365
|
// Sleep Clause Mod
|
|
2341
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2342
2367
|
for (foe.pokemon) |p| {
|
|
2343
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2344
2369
|
}
|
|
@@ -2346,19 +2371,20 @@ pub const Effects = struct {
|
|
|
2346
2371
|
foe.active.volatiles.Recharging = false;
|
|
2347
2372
|
|
|
2348
2373
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2374
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2375
|
+
|
|
2349
2376
|
const last = battle.side(player).last_selected_move;
|
|
2350
|
-
try options.log.
|
|
2377
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2351
2378
|
}
|
|
2352
2379
|
|
|
2353
2380
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2354
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2381
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2355
2382
|
}
|
|
2356
2383
|
|
|
2357
2384
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2358
2385
|
var side = battle.side(player);
|
|
2359
2386
|
if (side.active.volatiles.Substitute) {
|
|
2360
|
-
|
|
2361
|
-
return;
|
|
2387
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2362
2388
|
}
|
|
2363
2389
|
|
|
2364
2390
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2370,17 +2396,16 @@ pub const Effects = struct {
|
|
|
2370
2396
|
// avoid having to convert to floating point)
|
|
2371
2397
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2372
2398
|
if (side.stored().hp < required_hp) {
|
|
2373
|
-
|
|
2374
|
-
return;
|
|
2399
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2375
2400
|
}
|
|
2376
2401
|
|
|
2377
2402
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2378
2403
|
side.stored().hp -= hp;
|
|
2379
2404
|
side.active.volatiles.substitute = hp + 1;
|
|
2380
2405
|
side.active.volatiles.Substitute = true;
|
|
2381
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2406
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2382
2407
|
if (hp > 0) {
|
|
2383
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2408
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2384
2409
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2385
2410
|
}
|
|
2386
2411
|
}
|
|
@@ -2389,13 +2414,11 @@ pub const Effects = struct {
|
|
|
2389
2414
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2390
2415
|
if (showdown) {
|
|
2391
2416
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2392
|
-
|
|
2393
|
-
try options.chance.commit(player, .hit);
|
|
2394
|
-
}
|
|
2417
|
+
_ = try checkHit(battle, player, move, options);
|
|
2395
2418
|
battle.last_damage = 0;
|
|
2396
2419
|
} else {
|
|
2397
|
-
try options.log.fail(battle.active(player), .None);
|
|
2398
|
-
try options.log.laststill();
|
|
2420
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2421
|
+
try options.log.laststill(.{});
|
|
2399
2422
|
}
|
|
2400
2423
|
}
|
|
2401
2424
|
|
|
@@ -2406,6 +2429,8 @@ pub const Effects = struct {
|
|
|
2406
2429
|
|
|
2407
2430
|
volatiles.Thrashing = true;
|
|
2408
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2432
|
+
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2409
2434
|
}
|
|
2410
2435
|
|
|
2411
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2434,7 +2459,7 @@ pub const Effects = struct {
|
|
|
2434
2459
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2435
2460
|
}
|
|
2436
2461
|
|
|
2437
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2462
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2438
2463
|
}
|
|
2439
2464
|
|
|
2440
2465
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2444,13 +2469,14 @@ pub const Effects = struct {
|
|
|
2444
2469
|
if (side.active.volatiles.Binding) return;
|
|
2445
2470
|
side.active.volatiles.Binding = true;
|
|
2446
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2447
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2448
2473
|
if (!showdown) {
|
|
2449
2474
|
foe.active.volatiles.Recharging = false;
|
|
2450
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2451
2476
|
|
|
2452
2477
|
side.active.volatiles.attacks =
|
|
2453
2478
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2479
|
+
options.chance.observe(.binding, player, .started);
|
|
2454
2480
|
}
|
|
2455
2481
|
|
|
2456
2482
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2464,87 +2490,87 @@ pub const Effects = struct {
|
|
|
2464
2490
|
switch (move.effect) {
|
|
2465
2491
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2466
2492
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2467
|
-
if (boosts.atk == 6) return
|
|
2493
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2468
2494
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2469
2495
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2470
|
-
const reason = if (move.effect == .Rage)
|
|
2496
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2471
2497
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2472
2498
|
boosts.atk -= 1;
|
|
2473
2499
|
if (showdown) {
|
|
2474
|
-
try log.boost(ident, reason, n);
|
|
2475
|
-
try log.boost(ident,
|
|
2500
|
+
try log.boost(.{ ident, reason, n });
|
|
2501
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2476
2502
|
if (move.effect == .Rage) return;
|
|
2477
2503
|
}
|
|
2478
|
-
return
|
|
2504
|
+
return log.fail(.{ ident, .None });
|
|
2479
2505
|
}
|
|
2480
|
-
|
|
2506
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2481
2507
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2482
2508
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2483
|
-
try log.boost(ident, reason, n);
|
|
2509
|
+
try log.boost(.{ ident, reason, n });
|
|
2484
2510
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2485
2511
|
if (showdown and move.effect == .Rage) return;
|
|
2486
2512
|
},
|
|
2487
2513
|
.DefenseUp1, .DefenseUp2 => {
|
|
2488
2514
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2489
|
-
if (boosts.def == 6) return
|
|
2515
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2490
2516
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2491
2517
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2492
2518
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2493
2519
|
boosts.def -= 1;
|
|
2494
2520
|
if (showdown) {
|
|
2495
|
-
try log.boost(ident, .Defense, n);
|
|
2496
|
-
try log.boost(ident, .Defense, -1);
|
|
2521
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2522
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2497
2523
|
}
|
|
2498
|
-
return
|
|
2524
|
+
return log.fail(.{ ident, .None });
|
|
2499
2525
|
}
|
|
2500
|
-
|
|
2526
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2501
2527
|
const stat = unmodifiedStats(battle, side).def;
|
|
2502
2528
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2503
|
-
try log.boost(ident, .Defense, n);
|
|
2529
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2504
2530
|
},
|
|
2505
2531
|
.SpeedUp2 => {
|
|
2506
2532
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2507
|
-
if (boosts.spe == 6) return
|
|
2533
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2508
2534
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2509
2535
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2510
2536
|
boosts.spe -= 1;
|
|
2511
2537
|
if (showdown) {
|
|
2512
|
-
try log.boost(ident, .Speed, 2);
|
|
2513
|
-
try log.boost(ident, .Speed, -1);
|
|
2538
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2539
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2514
2540
|
}
|
|
2515
|
-
return
|
|
2541
|
+
return log.fail(.{ ident, .None });
|
|
2516
2542
|
}
|
|
2517
|
-
|
|
2543
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2518
2544
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2519
2545
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2520
|
-
try log.boost(ident, .Speed, 2);
|
|
2546
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2521
2547
|
},
|
|
2522
2548
|
.SpecialUp1, .SpecialUp2 => {
|
|
2523
2549
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2524
|
-
if (boosts.spc == 6) return
|
|
2550
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2525
2551
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2526
2552
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2527
2553
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2528
2554
|
boosts.spc -= 1;
|
|
2529
2555
|
if (showdown) {
|
|
2530
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2531
|
-
try log.boost(ident, .SpecialAttack, -1);
|
|
2532
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2533
|
-
try log.boost(ident, .SpecialDefense, -1);
|
|
2556
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2557
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2558
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2559
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2534
2560
|
}
|
|
2535
|
-
return
|
|
2561
|
+
return log.fail(.{ ident, .None });
|
|
2536
2562
|
}
|
|
2537
|
-
|
|
2563
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2538
2564
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2539
2565
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2540
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2541
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2566
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2567
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2542
2568
|
},
|
|
2543
2569
|
.EvasionUp1 => {
|
|
2544
2570
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2545
|
-
if (boosts.evasion == 6) return
|
|
2571
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2546
2572
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2547
|
-
try log.boost(ident, .Evasion, 1);
|
|
2573
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2548
2574
|
},
|
|
2549
2575
|
else => unreachable,
|
|
2550
2576
|
}
|
|
@@ -2559,7 +2585,7 @@ pub const Effects = struct {
|
|
|
2559
2585
|
const foe_ident = battle.active(player.foe());
|
|
2560
2586
|
const secondary = move.effect.isStatDownChance();
|
|
2561
2587
|
|
|
2562
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2588
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2563
2589
|
|
|
2564
2590
|
if (secondary) {
|
|
2565
2591
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2567,7 +2593,6 @@ pub const Effects = struct {
|
|
|
2567
2593
|
} else {
|
|
2568
2594
|
// checkHit already checks for Invulnerable
|
|
2569
2595
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2570
|
-
try options.chance.commit(player, .hit);
|
|
2571
2596
|
}
|
|
2572
2597
|
|
|
2573
2598
|
var stats = &foe.active.stats;
|
|
@@ -2577,82 +2602,82 @@ pub const Effects = struct {
|
|
|
2577
2602
|
switch (move.effect) {
|
|
2578
2603
|
.AttackDown1, .AttackDownChance => {
|
|
2579
2604
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2580
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2605
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2581
2606
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2582
2607
|
if (stats.atk == 1) {
|
|
2583
2608
|
boosts.atk += 1;
|
|
2584
2609
|
if (showdown) {
|
|
2585
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2586
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2610
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2611
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2587
2612
|
}
|
|
2588
|
-
return
|
|
2613
|
+
return log.fail(.{ foe_ident, .None });
|
|
2589
2614
|
}
|
|
2590
|
-
|
|
2615
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2591
2616
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2592
2617
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2593
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2618
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2594
2619
|
},
|
|
2595
2620
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2596
2621
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2597
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2622
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2598
2623
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2599
2624
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2600
2625
|
if (stats.def == 1) {
|
|
2601
2626
|
boosts.def += 1;
|
|
2602
2627
|
if (showdown) {
|
|
2603
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2604
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2628
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2629
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2605
2630
|
}
|
|
2606
|
-
return
|
|
2631
|
+
return log.fail(.{ foe_ident, .None });
|
|
2607
2632
|
}
|
|
2608
|
-
|
|
2633
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2609
2634
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2610
2635
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2611
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2636
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2612
2637
|
},
|
|
2613
2638
|
.SpeedDown1, .SpeedDownChance => {
|
|
2614
2639
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2615
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2640
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2616
2641
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2617
2642
|
if (stats.spe == 1) {
|
|
2618
2643
|
boosts.spe += 1;
|
|
2619
2644
|
if (showdown) {
|
|
2620
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2621
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2645
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2646
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2622
2647
|
}
|
|
2623
|
-
return
|
|
2648
|
+
return log.fail(.{ foe_ident, .None });
|
|
2624
2649
|
}
|
|
2625
|
-
|
|
2650
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2626
2651
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2627
2652
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2628
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2653
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2629
2654
|
assert(boosts.spe >= -6);
|
|
2630
2655
|
},
|
|
2631
2656
|
.SpecialDownChance => {
|
|
2632
2657
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2633
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2658
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2634
2659
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2635
2660
|
if (stats.spc == 1) {
|
|
2636
2661
|
boosts.spc += 1;
|
|
2637
2662
|
if (showdown) {
|
|
2638
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2639
|
-
try log.boost(foe_ident, .SpecialAttack, 1);
|
|
2640
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2641
|
-
try log.boost(foe_ident, .SpecialDefense, 1);
|
|
2663
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2664
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2665
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2642
2667
|
}
|
|
2643
|
-
return
|
|
2668
|
+
return log.fail(.{ foe_ident, .None });
|
|
2644
2669
|
}
|
|
2645
|
-
|
|
2670
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2646
2671
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2647
2672
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2648
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2649
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2673
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2674
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2650
2675
|
},
|
|
2651
2676
|
.AccuracyDown1 => {
|
|
2652
2677
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2653
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2678
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2654
2679
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2655
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2680
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2656
2681
|
},
|
|
2657
2682
|
else => unreachable,
|
|
2658
2683
|
}
|
|
@@ -2676,40 +2701,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2676
2701
|
}
|
|
2677
2702
|
}
|
|
2678
2703
|
|
|
2679
|
-
|
|
2704
|
+
fn isForced(active: anytype) bool {
|
|
2680
2705
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2681
2706
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2682
2707
|
}
|
|
2683
2708
|
|
|
2684
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2709
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2685
2710
|
var log = options.log;
|
|
2686
2711
|
var side = battle.side(who);
|
|
2687
2712
|
var volatiles = &side.active.volatiles;
|
|
2688
2713
|
const ident = battle.active(who);
|
|
2689
2714
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
try log.end(ident, .DisableSilent);
|
|
2715
|
+
if (volatiles.disable_move != 0) {
|
|
2716
|
+
volatiles.disable_move = 0;
|
|
2717
|
+
volatiles.disable_duration = 0;
|
|
2718
|
+
options.chance.observe(.disable, who, .None);
|
|
2719
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2696
2720
|
}
|
|
2697
2721
|
if (volatiles.Confusion) {
|
|
2698
2722
|
// volatiles.confusion is left unchanged
|
|
2699
2723
|
volatiles.Confusion = false;
|
|
2700
|
-
|
|
2724
|
+
options.chance.observe(.confusion, who, .None);
|
|
2725
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2701
2726
|
}
|
|
2702
2727
|
if (volatiles.Mist) {
|
|
2703
2728
|
volatiles.Mist = false;
|
|
2704
|
-
try log.end(ident, .MistSilent);
|
|
2729
|
+
try log.end(.{ ident, .MistSilent });
|
|
2705
2730
|
}
|
|
2706
2731
|
if (volatiles.FocusEnergy) {
|
|
2707
2732
|
volatiles.FocusEnergy = false;
|
|
2708
|
-
try log.end(ident, .FocusEnergySilent);
|
|
2733
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2709
2734
|
}
|
|
2710
2735
|
if (volatiles.LeechSeed) {
|
|
2711
2736
|
volatiles.LeechSeed = false;
|
|
2712
|
-
try log.end(ident, .LeechSeedSilent);
|
|
2737
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2713
2738
|
}
|
|
2714
2739
|
if (!showdown and volatiles.Toxic) {
|
|
2715
2740
|
volatiles.Toxic = false;
|
|
@@ -2717,24 +2742,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2717
2742
|
}
|
|
2718
2743
|
if (volatiles.LightScreen) {
|
|
2719
2744
|
volatiles.LightScreen = false;
|
|
2720
|
-
try log.end(ident, .LightScreenSilent);
|
|
2745
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2721
2746
|
}
|
|
2722
2747
|
if (volatiles.Reflect) {
|
|
2723
2748
|
volatiles.Reflect = false;
|
|
2724
|
-
try log.end(ident, .ReflectSilent);
|
|
2749
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2725
2750
|
}
|
|
2726
2751
|
if (showdown and volatiles.Toxic) {
|
|
2727
2752
|
volatiles.Toxic = false;
|
|
2728
2753
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2729
2754
|
volatiles.toxic = 0;
|
|
2730
|
-
try log.end(ident, .ToxicSilent);
|
|
2755
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2731
2756
|
}
|
|
2732
2757
|
}
|
|
2733
2758
|
|
|
2759
|
+
fn decrement(
|
|
2760
|
+
comptime field: chance.Action.Field,
|
|
2761
|
+
player: Player,
|
|
2762
|
+
options: anytype,
|
|
2763
|
+
n: anytype,
|
|
2764
|
+
) @TypeOf(n) {
|
|
2765
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2766
|
+
.started, .ended => 0,
|
|
2767
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2768
|
+
else => unreachable,
|
|
2769
|
+
} else n - 1;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2734
2772
|
pub const Rolls = struct {
|
|
2735
|
-
|
|
2773
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2736
2774
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2737
2775
|
player == .P1
|
|
2776
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2777
|
+
battle.rng.speedTie()
|
|
2738
2778
|
else if (showdown)
|
|
2739
2779
|
battle.rng.range(u8, 0, 2) == 0
|
|
2740
2780
|
else
|
|
@@ -2744,21 +2784,31 @@ pub const Rolls = struct {
|
|
|
2744
2784
|
return p1;
|
|
2745
2785
|
}
|
|
2746
2786
|
|
|
2747
|
-
|
|
2787
|
+
fn criticalHit(
|
|
2788
|
+
battle: anytype,
|
|
2789
|
+
player: Player,
|
|
2790
|
+
rate: u8,
|
|
2791
|
+
spurious: bool,
|
|
2792
|
+
options: anytype,
|
|
2793
|
+
) !bool {
|
|
2748
2794
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2749
2795
|
val == .true
|
|
2796
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2797
|
+
battle.rng.criticalHit(player, rate)
|
|
2750
2798
|
else if (showdown)
|
|
2751
2799
|
battle.rng.chance(u8, rate, 256)
|
|
2752
2800
|
else
|
|
2753
2801
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2754
2802
|
|
|
2755
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2803
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2756
2804
|
return crit;
|
|
2757
2805
|
}
|
|
2758
2806
|
|
|
2759
|
-
|
|
2807
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2760
2808
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2761
2809
|
val
|
|
2810
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2811
|
+
battle.rng.damage(player)
|
|
2762
2812
|
else roll: {
|
|
2763
2813
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2764
2814
|
while (true) {
|
|
@@ -2772,21 +2822,27 @@ pub const Rolls = struct {
|
|
|
2772
2822
|
return roll;
|
|
2773
2823
|
}
|
|
2774
2824
|
|
|
2775
|
-
|
|
2825
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2826
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2827
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2776
2828
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2777
2829
|
val == .true
|
|
2830
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2831
|
+
battle.rng.hit(player, acc)
|
|
2778
2832
|
else if (showdown)
|
|
2779
|
-
battle.rng.chance(
|
|
2833
|
+
battle.rng.chance(uN, acc, 256)
|
|
2780
2834
|
else
|
|
2781
|
-
battle.rng.next() <
|
|
2835
|
+
battle.rng.next() < acc;
|
|
2782
2836
|
|
|
2783
|
-
options.chance.hit(ok,
|
|
2837
|
+
try options.chance.hit(player, ok, acc);
|
|
2784
2838
|
return ok;
|
|
2785
2839
|
}
|
|
2786
2840
|
|
|
2787
|
-
|
|
2841
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2788
2842
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2789
2843
|
val == .true
|
|
2844
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2845
|
+
battle.rng.confused(player)
|
|
2790
2846
|
else if (showdown)
|
|
2791
2847
|
!battle.rng.chance(u8, 128, 256)
|
|
2792
2848
|
else
|
|
@@ -2796,9 +2852,11 @@ pub const Rolls = struct {
|
|
|
2796
2852
|
return cfz;
|
|
2797
2853
|
}
|
|
2798
2854
|
|
|
2799
|
-
|
|
2855
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2800
2856
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2801
2857
|
val == .true
|
|
2858
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2859
|
+
battle.rng.paralyzed(player)
|
|
2802
2860
|
else if (showdown)
|
|
2803
2861
|
battle.rng.chance(u8, 63, 256)
|
|
2804
2862
|
else
|
|
@@ -2808,9 +2866,11 @@ pub const Rolls = struct {
|
|
|
2808
2866
|
return par;
|
|
2809
2867
|
}
|
|
2810
2868
|
|
|
2811
|
-
|
|
2869
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2812
2870
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2813
2871
|
val == .true
|
|
2872
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2873
|
+
battle.rng.secondaryChance(player, 25)
|
|
2814
2874
|
else if (showdown)
|
|
2815
2875
|
battle.rng.chance(u8, 25, 256)
|
|
2816
2876
|
else
|
|
@@ -2820,11 +2880,13 @@ pub const Rolls = struct {
|
|
|
2820
2880
|
return proc;
|
|
2821
2881
|
}
|
|
2822
2882
|
|
|
2823
|
-
|
|
2883
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2824
2884
|
const rate: u8 = if (low) 26 else 77;
|
|
2825
2885
|
|
|
2826
2886
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2827
2887
|
val == .true
|
|
2888
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2889
|
+
battle.rng.secondaryChance(player, rate)
|
|
2828
2890
|
else if (showdown)
|
|
2829
2891
|
battle.rng.chance(u8, rate, 256)
|
|
2830
2892
|
else
|
|
@@ -2834,11 +2896,13 @@ pub const Rolls = struct {
|
|
|
2834
2896
|
return proc;
|
|
2835
2897
|
}
|
|
2836
2898
|
|
|
2837
|
-
|
|
2899
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2838
2900
|
const rate: u8 = if (low) 52 else 103;
|
|
2839
2901
|
|
|
2840
2902
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2841
2903
|
val == .true
|
|
2904
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2905
|
+
battle.rng.secondaryChance(player, rate)
|
|
2842
2906
|
else if (showdown)
|
|
2843
2907
|
battle.rng.chance(u8, rate, 256)
|
|
2844
2908
|
else
|
|
@@ -2848,9 +2912,11 @@ pub const Rolls = struct {
|
|
|
2848
2912
|
return proc;
|
|
2849
2913
|
}
|
|
2850
2914
|
|
|
2851
|
-
|
|
2915
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2852
2916
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2853
2917
|
val == .true
|
|
2918
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2919
|
+
battle.rng.secondaryChance(player, 85)
|
|
2854
2920
|
else if (showdown)
|
|
2855
2921
|
battle.rng.chance(u8, 85, 256)
|
|
2856
2922
|
else
|
|
@@ -2860,14 +2926,14 @@ pub const Rolls = struct {
|
|
|
2860
2926
|
return proc;
|
|
2861
2927
|
}
|
|
2862
2928
|
|
|
2863
|
-
|
|
2929
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2864
2930
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2865
2931
|
val
|
|
2866
|
-
else if (
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2932
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2933
|
+
battle.rng.metronome(player)
|
|
2934
|
+
else if (showdown)
|
|
2935
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2936
|
+
else move: {
|
|
2871
2937
|
while (true) {
|
|
2872
2938
|
const r = battle.rng.next();
|
|
2873
2939
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2880,9 +2946,11 @@ pub const Rolls = struct {
|
|
|
2880
2946
|
return move;
|
|
2881
2947
|
}
|
|
2882
2948
|
|
|
2883
|
-
|
|
2949
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2884
2950
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2885
2951
|
val - 1
|
|
2952
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2953
|
+
battle.rng.psywave(player)
|
|
2886
2954
|
else if (showdown)
|
|
2887
2955
|
battle.rng.range(u8, 0, max)
|
|
2888
2956
|
else power: {
|
|
@@ -2897,11 +2965,14 @@ pub const Rolls = struct {
|
|
|
2897
2965
|
return power;
|
|
2898
2966
|
}
|
|
2899
2967
|
|
|
2900
|
-
|
|
2901
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2902
|
-
@intCast(val)
|
|
2968
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2973
|
+
battle.rng.sleepDuration(player)
|
|
2903
2974
|
else if (showdown)
|
|
2904
|
-
|
|
2975
|
+
battle.rng.range(u3, 1, 8)
|
|
2905
2976
|
else duration: {
|
|
2906
2977
|
while (true) {
|
|
2907
2978
|
const r = battle.rng.next() & 7;
|
|
@@ -2910,60 +2981,74 @@ pub const Rolls = struct {
|
|
|
2910
2981
|
};
|
|
2911
2982
|
|
|
2912
2983
|
assert(duration >= 1 and duration <= 7);
|
|
2913
|
-
options.chance.duration(
|
|
2984
|
+
options.chance.duration(player, duration);
|
|
2914
2985
|
return duration;
|
|
2915
2986
|
}
|
|
2916
2987
|
|
|
2917
|
-
|
|
2918
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2919
|
-
val
|
|
2988
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2993
|
+
battle.rng.disableDuration(player)
|
|
2920
2994
|
else if (showdown)
|
|
2921
|
-
|
|
2995
|
+
battle.rng.range(u4, 1, 9)
|
|
2922
2996
|
else
|
|
2923
2997
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2924
2998
|
|
|
2925
2999
|
assert(duration >= 1 and duration <= 8);
|
|
2926
|
-
options.chance.duration(
|
|
3000
|
+
options.chance.duration(player, duration);
|
|
2927
3001
|
return duration;
|
|
2928
3002
|
}
|
|
2929
3003
|
|
|
2930
|
-
|
|
2931
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2932
|
-
@intCast(val)
|
|
3004
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3009
|
+
battle.rng.confusionDuration(player)
|
|
2933
3010
|
else if (showdown)
|
|
2934
|
-
|
|
3011
|
+
battle.rng.range(u3, 2, 6)
|
|
2935
3012
|
else
|
|
2936
3013
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2937
3014
|
|
|
2938
3015
|
assert(duration >= 2 and duration <= 5);
|
|
2939
|
-
options.chance.duration(
|
|
3016
|
+
options.chance.duration(player, duration);
|
|
2940
3017
|
return duration;
|
|
2941
3018
|
}
|
|
2942
3019
|
|
|
2943
|
-
|
|
2944
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2945
|
-
@intCast(val)
|
|
3020
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3025
|
+
battle.rng.attackingDuration(player)
|
|
2946
3026
|
else if (showdown)
|
|
2947
|
-
|
|
3027
|
+
battle.rng.range(u3, 2, 4)
|
|
2948
3028
|
else
|
|
2949
3029
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2950
3030
|
|
|
2951
3031
|
assert(duration >= 2 and duration <= 3);
|
|
2952
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, duration);
|
|
2953
3033
|
return duration;
|
|
2954
3034
|
}
|
|
2955
3035
|
|
|
2956
|
-
|
|
3036
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3037
|
+
|
|
3038
|
+
fn distribution(
|
|
2957
3039
|
battle: anytype,
|
|
2958
3040
|
comptime effect: Move.Effect,
|
|
2959
3041
|
player: Player,
|
|
2960
3042
|
options: anytype,
|
|
2961
3043
|
) !u3 {
|
|
2962
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2963
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2964
|
-
@intCast(val)
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3049
|
+
battle.rng.distribution(player)
|
|
2965
3050
|
else if (showdown)
|
|
2966
|
-
DISTRIBUTION[battle.rng.range(
|
|
3051
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2967
3052
|
else n: {
|
|
2968
3053
|
const r = (battle.rng.next() & 3);
|
|
2969
3054
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
@@ -2971,7 +3056,7 @@ pub const Rolls = struct {
|
|
|
2971
3056
|
|
|
2972
3057
|
assert(n >= 2 and n <= 5);
|
|
2973
3058
|
if (effect == .Binding) {
|
|
2974
|
-
options.chance.duration(
|
|
3059
|
+
options.chance.duration(player, n);
|
|
2975
3060
|
} else {
|
|
2976
3061
|
try options.chance.multiHit(player, n);
|
|
2977
3062
|
}
|
|
@@ -2986,15 +3071,15 @@ pub const Rolls = struct {
|
|
|
2986
3071
|
options: anytype,
|
|
2987
3072
|
) !u4 {
|
|
2988
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
2989
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2990
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2991
3076
|
val
|
|
2992
3077
|
else
|
|
2993
3078
|
null
|
|
2994
3079
|
else
|
|
2995
|
-
null
|
|
2996
|
-
|
|
2997
|
-
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3081
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3082
|
+
else slot: {
|
|
2998
3083
|
if (showdown) {
|
|
2999
3084
|
if (check_pp == 0) {
|
|
3000
3085
|
var i: usize = moves.len;
|
|
@@ -3010,10 +3095,9 @@ pub const Rolls = struct {
|
|
|
3010
3095
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3011
3096
|
if (moves[i].pp > 0) {
|
|
3012
3097
|
r -= 1;
|
|
3013
|
-
if (r == 0) break;
|
|
3098
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3014
3099
|
}
|
|
3015
3100
|
}
|
|
3016
|
-
break :slot @intCast(i + 1);
|
|
3017
3101
|
}
|
|
3018
3102
|
}
|
|
3019
3103
|
|
|
@@ -3075,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3075
3159
|
const side = battle.side(player);
|
|
3076
3160
|
const foe = battle.foe(player);
|
|
3077
3161
|
|
|
3078
|
-
|
|
3162
|
+
const active = &side.active;
|
|
3079
3163
|
const stored = side.stored();
|
|
3080
3164
|
|
|
3081
3165
|
// While players are not given any input options on the cartridge in these cases,
|
|
3082
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3166
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3083
3167
|
//
|
|
3084
3168
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3085
3169
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3087,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3087
3171
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3088
3172
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3089
3173
|
// strictly correct as the player would not have been presented the option to move or
|
|
3090
|
-
// switch at all
|
|
3174
|
+
// switch at all
|
|
3091
3175
|
if (isForced(active)) {
|
|
3092
3176
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3093
3177
|
n += 1;
|
|
@@ -3106,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3106
3190
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3107
3191
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3108
3192
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3109
|
-
// move normally if sleeping/frozen/bound
|
|
3193
|
+
// move normally if sleeping/frozen/bound
|
|
3110
3194
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3111
3195
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3112
3196
|
{
|
|
@@ -3119,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3119
3203
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3120
3204
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3121
3205
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3122
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3206
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3123
3207
|
if (limited) {
|
|
3124
3208
|
assert(showdown);
|
|
3125
3209
|
assert(side.last_selected_move != .None);
|
|
@@ -3129,7 +3213,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3129
3213
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3130
3214
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3131
3215
|
const struggle =
|
|
3132
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3216
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3133
3217
|
const s = if (struggle) 0 else slot;
|
|
3134
3218
|
out[n] = .{ .type = .Move, .data = s };
|
|
3135
3219
|
n += 1;
|
|
@@ -3144,14 +3228,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3144
3228
|
const m = active.moves[slot - 1];
|
|
3145
3229
|
if (m.id == .None) break;
|
|
3146
3230
|
if (m.pp == 0) continue;
|
|
3147
|
-
if (active.volatiles.
|
|
3231
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3148
3232
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3149
3233
|
n += 1;
|
|
3150
3234
|
}
|
|
3151
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3152
3236
|
if (n == before) {
|
|
3153
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3154
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3155
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3156
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3157
3241
|
}
|