@pkmn/engine 0.1.0-dev.bcb16a6c → 0.1.0-dev.bce6e8a5
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 +47 -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 +110 -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/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 +15 -18
- package/build/pkg/gen1.js +30 -21
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +42 -91
- package/build/pkg/index.js +23 -70
- 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 +53 -76
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +344 -101
- package/package.json +62 -132
- package/src/bin/install-pkmn-engine +31 -20
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- package/src/lib/{binding → bindings}/node.zig +8 -21
- package/src/lib/bindings/wasm.zig +59 -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 +17 -15
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +414 -389
- 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 +430 -303
- 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 +6 -7
- package/src/lib/gen1/data.zig +44 -47
- package/src/lib/gen1/helpers.zig +91 -562
- package/src/lib/gen1/mechanics.zig +642 -565
- package/src/lib/gen2/calc.zig +153 -0
- package/src/lib/gen2/chance.zig +902 -0
- package/src/lib/gen2/data/items.zig +4 -6
- package/src/lib/gen2/data/moves.zig +34 -19
- package/src/lib/gen2/data/species.zig +2 -4
- package/src/lib/gen2/data/types.zig +13 -13
- package/src/lib/gen2/data.zig +45 -54
- package/src/lib/gen2/generated.zig +801 -0
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +2483 -452
- 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 +93 -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 -330
- 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,19 +614,14 @@ 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
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
608
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
609
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
610
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
611
|
-
player
|
|
612
|
-
else
|
|
613
|
-
player.foe();
|
|
614
621
|
|
|
615
622
|
const uncapped = battle.last_damage;
|
|
616
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
617
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
618
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
619
626
|
if (showdown) battle.last_damage = uncapped;
|
|
620
627
|
|
|
@@ -631,9 +638,12 @@ fn beforeMove(
|
|
|
631
638
|
volatiles.Thrashing = false;
|
|
632
639
|
volatiles.Charging = false;
|
|
633
640
|
volatiles.Binding = false;
|
|
634
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
635
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
636
|
-
try log.cant(ident, .Paralysis);
|
|
646
|
+
try log.cant(.{ ident, .Paralysis });
|
|
637
647
|
return .done;
|
|
638
648
|
}
|
|
639
649
|
|
|
@@ -649,28 +659,22 @@ fn beforeMove(
|
|
|
649
659
|
volatiles.state +%= battle.last_damage;
|
|
650
660
|
}
|
|
651
661
|
|
|
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);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
660
664
|
|
|
661
665
|
if (volatiles.attacks != 0) {
|
|
662
|
-
try log.activate(ident, .Bide);
|
|
666
|
+
try log.activate(.{ ident, .Bide });
|
|
663
667
|
return .done;
|
|
664
668
|
}
|
|
665
669
|
|
|
666
670
|
volatiles.Bide = false;
|
|
667
|
-
try log.end(ident, .Bide);
|
|
671
|
+
try log.end(.{ ident, .Bide });
|
|
668
672
|
|
|
669
673
|
battle.last_damage = volatiles.state *% 2;
|
|
670
674
|
volatiles.state = 0;
|
|
671
675
|
|
|
672
676
|
if (battle.last_damage == 0) {
|
|
673
|
-
try log.fail(ident, .None);
|
|
677
|
+
try log.fail(.{ ident, .None });
|
|
674
678
|
return .done;
|
|
675
679
|
}
|
|
676
680
|
|
|
@@ -690,47 +694,42 @@ fn beforeMove(
|
|
|
690
694
|
}
|
|
691
695
|
|
|
692
696
|
if (volatiles.Thrashing) {
|
|
693
|
-
|
|
694
|
-
assert(from != null);
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
695
698
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
} else {
|
|
699
|
-
volatiles.attacks -= 1;
|
|
700
|
-
}
|
|
701
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
702
701
|
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
708
|
+
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
713
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
714
719
|
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
715
725
|
return .skip_can;
|
|
716
726
|
}
|
|
717
727
|
|
|
718
728
|
if (volatiles.Binding) {
|
|
719
|
-
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
720
731
|
|
|
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
|
-
);
|
|
732
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
734
733
|
if (showdown or battle.last_damage != 0) {
|
|
735
734
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
736
735
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -750,7 +749,7 @@ fn canMove(
|
|
|
750
749
|
mslot: u4,
|
|
751
750
|
auto: bool,
|
|
752
751
|
skip_pp: bool,
|
|
753
|
-
from:
|
|
752
|
+
from: Move,
|
|
754
753
|
residual: *bool,
|
|
755
754
|
options: anytype,
|
|
756
755
|
) !bool {
|
|
@@ -758,33 +757,21 @@ fn canMove(
|
|
|
758
757
|
const player_ident = battle.active(player);
|
|
759
758
|
const move = Move.get(side.last_selected_move);
|
|
760
759
|
|
|
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
760
|
if (side.active.volatiles.Charging) {
|
|
767
761
|
side.active.volatiles.Charging = false;
|
|
768
762
|
side.active.volatiles.Invulnerable = false;
|
|
769
763
|
} else if (move.effect == .Charge) {
|
|
770
|
-
try options.log.move(player_ident, side.last_selected_move,
|
|
764
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
771
765
|
setCounterable(battle, player, side, move);
|
|
772
766
|
try Effects.charge(battle, player, options);
|
|
773
767
|
return false;
|
|
774
768
|
}
|
|
775
769
|
|
|
776
|
-
|
|
777
|
-
if (!
|
|
770
|
+
side.last_used_move = side.last_selected_move;
|
|
771
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
778
772
|
|
|
779
773
|
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);
|
|
774
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
788
775
|
setCounterable(battle, player, side, move);
|
|
789
776
|
|
|
790
777
|
if (move.effect.onBegin()) {
|
|
@@ -794,9 +781,14 @@ fn canMove(
|
|
|
794
781
|
|
|
795
782
|
if (move.effect == .Thrashing) {
|
|
796
783
|
Effects.thrashing(battle, player, options);
|
|
797
|
-
} else if (
|
|
798
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
799
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
800
792
|
}
|
|
801
793
|
|
|
802
794
|
return true;
|
|
@@ -822,16 +814,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
822
814
|
assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
|
|
823
815
|
if (volatiles.Bide) return;
|
|
824
816
|
|
|
825
|
-
|
|
826
|
-
|
|
817
|
+
var move_slot = active.move(mslot);
|
|
818
|
+
assert(move_slot.pp > 0 or auto);
|
|
819
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
827
820
|
if (volatiles.Transform) return;
|
|
828
821
|
|
|
829
|
-
|
|
830
|
-
|
|
822
|
+
move_slot = side.stored().move(mslot);
|
|
823
|
+
assert(move_slot.pp > 0 or auto);
|
|
824
|
+
move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
|
|
831
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
832
826
|
}
|
|
833
827
|
|
|
834
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
835
829
|
var active = &side.active;
|
|
836
830
|
const volatiles = &active.volatiles;
|
|
837
831
|
|
|
@@ -839,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
839
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
840
834
|
if (showdown and volatiles.Transform) return;
|
|
841
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
842
839
|
assert(mslot > 0 and mslot <= 4);
|
|
843
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
844
841
|
}
|
|
845
842
|
|
|
846
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -888,13 +885,13 @@ fn doMove(
|
|
|
888
885
|
break :miss true;
|
|
889
886
|
}
|
|
890
887
|
break :miss (move.target != .Self and
|
|
891
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
892
889
|
};
|
|
893
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
894
891
|
|
|
895
892
|
var late = showdown and move.effect != .Explode;
|
|
896
893
|
const skip = status or immune;
|
|
897
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
898
895
|
if (showdown and move.effect.isMulti()) {
|
|
899
896
|
try Effects.multiHit(battle, player, move, options);
|
|
900
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -916,8 +913,9 @@ fn doMove(
|
|
|
916
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
917
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
918
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
919
|
-
if (showdown) break :
|
|
920
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
921
919
|
return @as(?Result, Result.Error);
|
|
922
920
|
}
|
|
923
921
|
if (battle.last_damage == 0) {
|
|
@@ -931,11 +929,12 @@ fn doMove(
|
|
|
931
929
|
}
|
|
932
930
|
}
|
|
933
931
|
|
|
934
|
-
var
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
932
|
+
var missed: bool = undefined;
|
|
933
|
+
const zero = battle.last_damage == 0;
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
939
938
|
|
|
940
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
941
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -959,16 +958,37 @@ fn doMove(
|
|
|
959
958
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
960
959
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
961
960
|
if (ohko) {
|
|
962
|
-
try log.immune(foe_ident, .OHKO);
|
|
961
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
963
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
964
|
-
try log.immune(foe_ident, .None);
|
|
963
|
+
try log.immune(.{ foe_ident, .None });
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
965
985
|
} else if (mist) {
|
|
966
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
967
|
-
try log.fail(foe_ident, .None);
|
|
986
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
987
|
+
try log.fail(.{ foe_ident, .None });
|
|
968
988
|
} else {
|
|
969
|
-
if (showdown
|
|
970
|
-
try log.lastmiss();
|
|
971
|
-
try log.miss(battle.active(player));
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
990
|
+
try log.lastmiss(.{});
|
|
991
|
+
try log.miss(.{battle.active(player)});
|
|
972
992
|
}
|
|
973
993
|
if (move.effect == .JumpKick) {
|
|
974
994
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -981,17 +1001,15 @@ fn doMove(
|
|
|
981
1001
|
try buildRage(battle, player.foe(), options);
|
|
982
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
983
1003
|
try buildRage(battle, player.foe(), options);
|
|
984
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
985
|
-
try options.chance.commit(player, .binding);
|
|
986
1004
|
}
|
|
987
1005
|
return null;
|
|
988
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
989
1007
|
try options.chance.commit(player, .hit);
|
|
990
1008
|
}
|
|
991
1009
|
|
|
992
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
993
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
994
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
995
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
996
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
997
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -1004,15 +1022,15 @@ fn doMove(
|
|
|
1004
1022
|
var hit: u4 = 0;
|
|
1005
1023
|
while (hit < hits) {
|
|
1006
1024
|
if (hit == 0) {
|
|
1007
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1025
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
1008
1026
|
if (effectiveness > Effectiveness.neutral) {
|
|
1009
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1027
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
1010
1028
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
1011
|
-
try log.resisted(battle.active(player.foe()));
|
|
1029
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
1012
1030
|
}
|
|
1013
1031
|
}
|
|
1014
1032
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
1015
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1033
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
1016
1034
|
hit += 1;
|
|
1017
1035
|
if (foe.stored().hp == 0) break;
|
|
1018
1036
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -1029,33 +1047,31 @@ fn doMove(
|
|
|
1029
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1030
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1031
1049
|
}
|
|
1032
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1033
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1034
1052
|
// 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;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1048
1063
|
}
|
|
1064
|
+
return null;
|
|
1049
1065
|
}
|
|
1050
1066
|
}
|
|
1051
1067
|
|
|
1052
1068
|
// 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
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1054
1070
|
if (nullified) return null;
|
|
1055
1071
|
|
|
1056
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1057
1073
|
// 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
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1059
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1060
1076
|
|
|
1061
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1070,7 +1086,7 @@ fn doMove(
|
|
|
1070
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1071
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1072
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1073
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1074
1090
|
if (move.effect == .Twineedle) {
|
|
1075
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1076
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1089,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1089
1105
|
const side = battle.side(player);
|
|
1090
1106
|
|
|
1091
1107
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1092
|
-
var rate =
|
|
1108
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1093
1109
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1094
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1095
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1096
1112
|
|
|
1097
|
-
|
|
1113
|
+
const spurious =
|
|
1114
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1115
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1098
1116
|
}
|
|
1099
1117
|
|
|
1100
1118
|
fn calcDamage(
|
|
@@ -1103,6 +1121,7 @@ fn calcDamage(
|
|
|
1103
1121
|
target_player: Player,
|
|
1104
1122
|
m: ?Move.Data,
|
|
1105
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1106
1125
|
) bool {
|
|
1107
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1108
1127
|
const cfz = m == null;
|
|
@@ -1114,28 +1133,8 @@ fn calcDamage(
|
|
|
1114
1133
|
|
|
1115
1134
|
const special = move.type.special();
|
|
1116
1135
|
|
|
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
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1139
1138
|
|
|
1140
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1141
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1143,12 +1142,12 @@ fn calcDamage(
|
|
|
1143
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1144
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1145
1144
|
// 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 (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1147
1146
|
}
|
|
1148
1147
|
|
|
1149
|
-
const lvl =
|
|
1148
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1150
1149
|
|
|
1151
|
-
def =
|
|
1150
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1152
1151
|
|
|
1153
1152
|
if (def == 0) return false;
|
|
1154
1153
|
|
|
@@ -1160,11 +1159,41 @@ fn calcDamage(
|
|
|
1160
1159
|
d = @min(997, d);
|
|
1161
1160
|
d += 2;
|
|
1162
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1163
1172
|
battle.last_damage = @intCast(d);
|
|
1164
1173
|
|
|
1165
1174
|
return true;
|
|
1166
1175
|
}
|
|
1167
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1168
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1169
1198
|
const side = battle.side(player);
|
|
1170
1199
|
const foe = battle.foe(player);
|
|
@@ -1209,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1209
1238
|
const foe = battle.foe(player);
|
|
1210
1239
|
|
|
1211
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1212
|
-
try options.chance.commit(player, .hit);
|
|
1213
1241
|
|
|
1214
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1215
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1220,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1220
1248
|
.Psywave => power: {
|
|
1221
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1222
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1223
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1224
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1225
1253
|
},
|
|
1226
1254
|
else => unreachable,
|
|
1227
1255
|
};
|
|
1228
1256
|
|
|
1229
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1230
1258
|
|
|
1231
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1232
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1239,7 +1267,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1239
1267
|
const foe = battle.foe(player);
|
|
1240
1268
|
|
|
1241
1269
|
if (battle.last_damage == 0) {
|
|
1242
|
-
try options.log.fail(battle.active(player), .None);
|
|
1270
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1243
1271
|
return null;
|
|
1244
1272
|
}
|
|
1245
1273
|
|
|
@@ -1257,22 +1285,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1257
1285
|
foe_last_selected_move.type == .Fighting);
|
|
1258
1286
|
|
|
1259
1287
|
if (!used and !selected) {
|
|
1260
|
-
try options.log.fail(battle.active(player), .None);
|
|
1288
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1261
1289
|
return null;
|
|
1262
1290
|
}
|
|
1263
1291
|
|
|
1264
1292
|
if (!used or !selected) {
|
|
1265
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1266
|
-
if (!
|
|
1267
|
-
try options.log.fail(battle.active(player), .None);
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1295
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1268
1296
|
return null;
|
|
1269
1297
|
}
|
|
1270
1298
|
|
|
1271
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1272
1304
|
|
|
1273
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1274
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1275
|
-
try options.chance.commit(player, .hit);
|
|
1276
1307
|
|
|
1277
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1278
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1300,20 +1331,20 @@ fn applyDamage(
|
|
|
1300
1331
|
subbed.active.volatiles.substitute = 0;
|
|
1301
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1302
1333
|
// 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(
|
|
1334
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1305
1336
|
return true;
|
|
1306
1337
|
} else {
|
|
1307
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1308
1339
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1309
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1340
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1310
1341
|
return false;
|
|
1311
1342
|
}
|
|
1312
1343
|
}
|
|
1313
1344
|
|
|
1314
1345
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1315
1346
|
target.stored().hp -= battle.last_damage;
|
|
1316
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1347
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1317
1348
|
return false;
|
|
1318
1349
|
}
|
|
1319
1350
|
|
|
@@ -1332,10 +1363,10 @@ fn mirrorMove(
|
|
|
1332
1363
|
side.last_selected_move = foe.last_used_move;
|
|
1333
1364
|
|
|
1334
1365
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1335
|
-
try options.log.fail(battle.active(player), .None);
|
|
1366
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1336
1367
|
return null;
|
|
1337
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1338
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1339
1370
|
}
|
|
1340
1371
|
|
|
1341
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1356,7 +1387,7 @@ fn metronome(
|
|
|
1356
1387
|
var side = battle.side(player);
|
|
1357
1388
|
|
|
1358
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1359
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1360
1391
|
|
|
1361
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1362
1393
|
return null;
|
|
@@ -1368,20 +1399,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1368
1399
|
var immune = false;
|
|
1369
1400
|
var mist = false;
|
|
1370
1401
|
|
|
1371
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1402
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1372
1406
|
|
|
1373
1407
|
assert(!immune);
|
|
1374
1408
|
if (mist) {
|
|
1375
1409
|
assert(!showdown);
|
|
1376
1410
|
const foe_ident = battle.active(player.foe());
|
|
1377
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1378
|
-
try options.log.fail(foe_ident, .None);
|
|
1411
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1412
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1379
1413
|
} else {
|
|
1380
|
-
try options.
|
|
1381
|
-
try options.log.
|
|
1382
|
-
try options.log.miss(battle.active(player));
|
|
1414
|
+
try options.log.lastmiss(.{});
|
|
1415
|
+
try options.log.miss(.{battle.active(player)});
|
|
1383
1416
|
}
|
|
1384
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1385
1419
|
return false;
|
|
1386
1420
|
}
|
|
1387
1421
|
|
|
@@ -1392,7 +1426,7 @@ fn moveHit(
|
|
|
1392
1426
|
immune: *bool,
|
|
1393
1427
|
mist: *bool,
|
|
1394
1428
|
options: anytype,
|
|
1395
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1396
1430
|
var side = battle.side(player);
|
|
1397
1431
|
const foe = battle.foe(player);
|
|
1398
1432
|
|
|
@@ -1431,7 +1465,7 @@ fn moveHit(
|
|
|
1431
1465
|
const overwritten = overwrite and state > 0;
|
|
1432
1466
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1433
1467
|
|
|
1434
|
-
var accuracy = if (overwritten) state else
|
|
1468
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1435
1469
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1436
1470
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1437
1471
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1443,7 +1477,7 @@ fn moveHit(
|
|
|
1443
1477
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1444
1478
|
|
|
1445
1479
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1446
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1480
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1447
1481
|
};
|
|
1448
1482
|
|
|
1449
1483
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1454,13 +1488,17 @@ fn moveHit(
|
|
|
1454
1488
|
|
|
1455
1489
|
if (!miss) return true;
|
|
1456
1490
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1491
|
+
|
|
1457
1492
|
side.active.volatiles.Binding = false;
|
|
1493
|
+
options.chance.observe(.binding, player, .None);
|
|
1494
|
+
|
|
1458
1495
|
return false;
|
|
1459
1496
|
}
|
|
1460
1497
|
|
|
1461
1498
|
fn checkFaint(
|
|
1462
1499
|
battle: anytype,
|
|
1463
1500
|
player: Player,
|
|
1501
|
+
cap: bool,
|
|
1464
1502
|
options: anytype,
|
|
1465
1503
|
) @TypeOf(options.log).Error!?Result {
|
|
1466
1504
|
const side = battle.side(player);
|
|
@@ -1470,24 +1508,23 @@ fn checkFaint(
|
|
|
1470
1508
|
const foe_fainted = foe.stored().hp == 0;
|
|
1471
1509
|
|
|
1472
1510
|
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;
|
|
1511
|
+
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1512
|
+
const more = player_out or foe_out;
|
|
1476
1513
|
|
|
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;
|
|
1514
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1515
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1479
1516
|
|
|
1480
1517
|
assert(!side.active.volatiles.MultiHit);
|
|
1481
1518
|
assert(!foe.active.volatiles.MultiHit);
|
|
1482
1519
|
|
|
1483
|
-
if (
|
|
1484
|
-
try options.log.tie();
|
|
1520
|
+
if (player_out and foe_out) {
|
|
1521
|
+
try options.log.tie(.{});
|
|
1485
1522
|
return Result.Tie;
|
|
1486
1523
|
} else if (player_out) {
|
|
1487
|
-
try options.log.win(player.foe());
|
|
1524
|
+
try options.log.win(.{player.foe()});
|
|
1488
1525
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1489
1526
|
} else if (foe_out) {
|
|
1490
|
-
try options.log.win(player);
|
|
1527
|
+
try options.log.win(.{player});
|
|
1491
1528
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1492
1529
|
}
|
|
1493
1530
|
|
|
@@ -1496,7 +1533,7 @@ fn checkFaint(
|
|
|
1496
1533
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1497
1534
|
}
|
|
1498
1535
|
|
|
1499
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1536
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1500
1537
|
var side = battle.side(player);
|
|
1501
1538
|
var foe = battle.foe(player);
|
|
1502
1539
|
assert(side.stored().hp == 0);
|
|
@@ -1505,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1505
1542
|
assert(!foe_volatiles.MultiHit);
|
|
1506
1543
|
if (foe_volatiles.Bide) {
|
|
1507
1544
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1508
|
-
foe_volatiles.state = if (
|
|
1545
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1509
1546
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1510
1547
|
}
|
|
1511
1548
|
|
|
@@ -1529,8 +1566,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1529
1566
|
side.stored().stats.spe;
|
|
1530
1567
|
}
|
|
1531
1568
|
|
|
1532
|
-
try options.log.faint(battle.active(player), done);
|
|
1533
|
-
|
|
1569
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1570
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1571
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1572
|
+
if (cap) options.calc.capped(player);
|
|
1534
1573
|
return null;
|
|
1535
1574
|
}
|
|
1536
1575
|
|
|
@@ -1551,8 +1590,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1551
1590
|
|
|
1552
1591
|
if (!showdown or damage > 0) {
|
|
1553
1592
|
stored.hp -= damage;
|
|
1554
|
-
// Pokémon Showdown uses
|
|
1555
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1593
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1594
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1556
1595
|
}
|
|
1557
1596
|
}
|
|
1558
1597
|
|
|
@@ -1577,14 +1616,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1577
1616
|
const amount = @min(damage, stored.hp);
|
|
1578
1617
|
stored.hp -= amount;
|
|
1579
1618
|
|
|
1580
|
-
// As above, Pokémon Showdown uses
|
|
1581
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1619
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1620
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1582
1621
|
|
|
1583
1622
|
const before = foe_stored.hp;
|
|
1584
1623
|
// Uncapped damage is added back to the foe
|
|
1585
1624
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1586
1625
|
// 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);
|
|
1626
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1588
1627
|
}
|
|
1589
1628
|
}
|
|
1590
1629
|
|
|
@@ -1594,19 +1633,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1594
1633
|
|
|
1595
1634
|
battle.turn += 1;
|
|
1596
1635
|
|
|
1597
|
-
if (
|
|
1598
|
-
try options.log.tie();
|
|
1636
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1637
|
+
try options.log.tie(.{});
|
|
1599
1638
|
return Result.Tie;
|
|
1600
1639
|
}
|
|
1601
1640
|
|
|
1602
|
-
if (
|
|
1603
|
-
try options.log.tie();
|
|
1641
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1642
|
+
try options.log.tie(.{});
|
|
1604
1643
|
return Result.Tie;
|
|
1605
1644
|
} else if (battle.turn >= 65535) {
|
|
1606
1645
|
return Result.Error;
|
|
1607
1646
|
}
|
|
1608
1647
|
|
|
1609
|
-
try options.log.turn(battle.turn);
|
|
1648
|
+
try options.log.turn(.{battle.turn});
|
|
1610
1649
|
|
|
1611
1650
|
return Result.Default;
|
|
1612
1651
|
}
|
|
@@ -1666,9 +1705,9 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1666
1705
|
}
|
|
1667
1706
|
|
|
1668
1707
|
fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
1669
|
-
if (side.active.volatiles.
|
|
1708
|
+
if (side.active.volatiles.disable_move != 0) {
|
|
1670
1709
|
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1671
|
-
const m = side.active.moves[side.active.volatiles.
|
|
1710
|
+
const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
|
|
1672
1711
|
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1673
1712
|
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1674
1713
|
m
|
|
@@ -1676,32 +1715,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1676
1715
|
side.last_selected_move;
|
|
1677
1716
|
if (m != .None and m == last) {
|
|
1678
1717
|
side.active.volatiles.Charging = false;
|
|
1679
|
-
try options.log.
|
|
1718
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1680
1719
|
return true;
|
|
1681
1720
|
}
|
|
1682
1721
|
}
|
|
1683
1722
|
return false;
|
|
1684
1723
|
}
|
|
1685
1724
|
|
|
1686
|
-
|
|
1725
|
+
fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
1687
1726
|
const side = battle.side(who);
|
|
1688
1727
|
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1689
1728
|
try Effects.boost(battle, who, Move.get(.Rage), options);
|
|
1690
1729
|
}
|
|
1691
1730
|
}
|
|
1692
1731
|
|
|
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(
|
|
1732
|
+
fn onBegin(
|
|
1705
1733
|
battle: anytype,
|
|
1706
1734
|
player: Player,
|
|
1707
1735
|
move: Move.Data,
|
|
@@ -1709,7 +1737,6 @@ inline fn onBegin(
|
|
|
1709
1737
|
residual: *bool,
|
|
1710
1738
|
options: anytype,
|
|
1711
1739
|
) !void {
|
|
1712
|
-
assert(move.effect.onBegin());
|
|
1713
1740
|
return switch (move.effect) {
|
|
1714
1741
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1715
1742
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1731,8 +1758,7 @@ inline fn onBegin(
|
|
|
1731
1758
|
};
|
|
1732
1759
|
}
|
|
1733
1760
|
|
|
1734
|
-
|
|
1735
|
-
assert(move.effect.onEnd());
|
|
1761
|
+
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1736
1762
|
return switch (move.effect) {
|
|
1737
1763
|
.Bide => Effects.bide(battle, player, options),
|
|
1738
1764
|
// zig fmt: off
|
|
@@ -1747,14 +1773,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
|
|
|
1747
1773
|
};
|
|
1748
1774
|
}
|
|
1749
1775
|
|
|
1750
|
-
|
|
1776
|
+
fn alwaysHappens(
|
|
1751
1777
|
battle: anytype,
|
|
1752
1778
|
player: Player,
|
|
1753
1779
|
move: Move.Data,
|
|
1754
1780
|
residual: *bool,
|
|
1755
1781
|
options: anytype,
|
|
1756
1782
|
) !void {
|
|
1757
|
-
assert(move.effect.alwaysHappens());
|
|
1758
1783
|
return switch (move.effect) {
|
|
1759
1784
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1760
1785
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1766,7 +1791,7 @@ inline fn alwaysHappens(
|
|
|
1766
1791
|
};
|
|
1767
1792
|
}
|
|
1768
1793
|
|
|
1769
|
-
|
|
1794
|
+
fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1770
1795
|
return switch (move.effect) {
|
|
1771
1796
|
.BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
|
|
1772
1797
|
.ConfusionChance => Effects.confusion(battle, player, move, options),
|
|
@@ -1790,10 +1815,12 @@ pub const Effects = struct {
|
|
|
1790
1815
|
|
|
1791
1816
|
side.active.volatiles.Bide = true;
|
|
1792
1817
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1818
|
+
|
|
1793
1819
|
side.active.volatiles.state = 0;
|
|
1794
1820
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1821
|
+
options.chance.observe(.attacking, player, .started);
|
|
1795
1822
|
|
|
1796
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1823
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1797
1824
|
}
|
|
1798
1825
|
|
|
1799
1826
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1807,11 +1834,11 @@ pub const Effects = struct {
|
|
|
1807
1834
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1808
1835
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1809
1836
|
assert(move.type == .Fire);
|
|
1810
|
-
try options.log.curestatus(
|
|
1837
|
+
try options.log.curestatus(.{
|
|
1811
1838
|
battle.active(player.foe()),
|
|
1812
1839
|
foe_stored.status,
|
|
1813
1840
|
.Message,
|
|
1814
|
-
);
|
|
1841
|
+
});
|
|
1815
1842
|
foe_stored.status = 0;
|
|
1816
1843
|
}
|
|
1817
1844
|
return;
|
|
@@ -1825,7 +1852,7 @@ pub const Effects = struct {
|
|
|
1825
1852
|
foe_stored.status = Status.init(.BRN);
|
|
1826
1853
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1827
1854
|
|
|
1828
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1855
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1829
1856
|
}
|
|
1830
1857
|
|
|
1831
1858
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1835,8 +1862,8 @@ pub const Effects = struct {
|
|
|
1835
1862
|
volatiles.Charging = true;
|
|
1836
1863
|
const move = side.last_selected_move;
|
|
1837
1864
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1838
|
-
try options.log.laststill();
|
|
1839
|
-
try options.log.prepare(battle.active(player), move);
|
|
1865
|
+
try options.log.laststill(.{});
|
|
1866
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1840
1867
|
}
|
|
1841
1868
|
|
|
1842
1869
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1847,42 +1874,39 @@ pub const Effects = struct {
|
|
|
1847
1874
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1848
1875
|
} else {
|
|
1849
1876
|
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
|
-
}
|
|
1877
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1878
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1855
1879
|
} else {
|
|
1856
|
-
if (sub) {
|
|
1857
|
-
|
|
1858
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1880
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1881
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1861
1882
|
}
|
|
1862
|
-
try options.chance.commit(player, .hit);
|
|
1863
1883
|
}
|
|
1864
1884
|
|
|
1865
1885
|
if (foe.active.volatiles.Confusion) return;
|
|
1866
1886
|
foe.active.volatiles.Confusion = true;
|
|
1867
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1868
1887
|
|
|
1869
|
-
|
|
1888
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1889
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1890
|
+
options.calc.confusion(player.foe());
|
|
1891
|
+
|
|
1892
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1870
1893
|
}
|
|
1871
1894
|
|
|
1872
1895
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1873
1896
|
const foe = battle.foe(player);
|
|
1874
1897
|
|
|
1875
1898
|
if (foe.active.volatiles.Invulnerable) {
|
|
1876
|
-
try options.log.lastmiss();
|
|
1877
|
-
return options.log.miss(battle.active(player));
|
|
1899
|
+
try options.log.lastmiss(.{});
|
|
1900
|
+
return options.log.miss(.{battle.active(player)});
|
|
1878
1901
|
}
|
|
1879
1902
|
|
|
1880
1903
|
battle.side(player).active.types = foe.active.types;
|
|
1881
|
-
return options.log.
|
|
1904
|
+
return options.log.start(.{
|
|
1882
1905
|
battle.active(player),
|
|
1906
|
+
.TypeChange,
|
|
1883
1907
|
foe.active.types,
|
|
1884
1908
|
battle.active(player.foe()),
|
|
1885
|
-
);
|
|
1909
|
+
});
|
|
1886
1910
|
}
|
|
1887
1911
|
|
|
1888
1912
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1891,13 +1915,10 @@ pub const Effects = struct {
|
|
|
1891
1915
|
const foe_ident = battle.active(player.foe());
|
|
1892
1916
|
|
|
1893
1917
|
// 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
|
-
}
|
|
1918
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1898
1919
|
|
|
1899
|
-
if (volatiles.
|
|
1900
|
-
try options.log.fail(foe_ident, .None);
|
|
1920
|
+
if (volatiles.disable_move != 0) {
|
|
1921
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1901
1922
|
return null;
|
|
1902
1923
|
}
|
|
1903
1924
|
|
|
@@ -1912,27 +1933,27 @@ pub const Effects = struct {
|
|
|
1912
1933
|
|
|
1913
1934
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1914
1935
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1915
|
-
if (n == 0) {
|
|
1916
|
-
try options.log.fail(foe_ident, .None);
|
|
1936
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1937
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1917
1938
|
return null;
|
|
1918
1939
|
} else if (err) {
|
|
1919
1940
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1920
|
-
assert(!showdown);
|
|
1921
1941
|
return Result.Error;
|
|
1922
1942
|
}
|
|
1923
1943
|
|
|
1924
|
-
volatiles.
|
|
1944
|
+
volatiles.disable_move =
|
|
1925
1945
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1926
|
-
volatiles.
|
|
1946
|
+
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1947
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1927
1948
|
|
|
1928
|
-
const id = foe.active.move(volatiles.
|
|
1929
|
-
try options.log.
|
|
1949
|
+
const id = foe.active.move(volatiles.disable_move).id;
|
|
1950
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1930
1951
|
return null;
|
|
1931
1952
|
}
|
|
1932
1953
|
|
|
1933
1954
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1934
|
-
var
|
|
1935
|
-
|
|
1955
|
+
var stored = battle.side(player).stored();
|
|
1956
|
+
const ident = battle.active(player);
|
|
1936
1957
|
|
|
1937
1958
|
if (battle.last_damage == 0) {
|
|
1938
1959
|
assert(showdown);
|
|
@@ -1945,7 +1966,7 @@ pub const Effects = struct {
|
|
|
1945
1966
|
if (stored.hp == stored.stats.hp) return;
|
|
1946
1967
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1947
1968
|
|
|
1948
|
-
try options.log.
|
|
1969
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1949
1970
|
}
|
|
1950
1971
|
|
|
1951
1972
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1977,7 +1998,7 @@ pub const Effects = struct {
|
|
|
1977
1998
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1978
1999
|
side.active.volatiles.FocusEnergy = true;
|
|
1979
2000
|
|
|
1980
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2001
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1981
2002
|
}
|
|
1982
2003
|
|
|
1983
2004
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1994,12 +2015,12 @@ pub const Effects = struct {
|
|
|
1994
2015
|
if (foe.active.types.includes(move.type)) return;
|
|
1995
2016
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1996
2017
|
// Freeze Clause Mod
|
|
1997
|
-
if (
|
|
2018
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1998
2019
|
|
|
1999
2020
|
foe_stored.status = Status.init(.FRZ);
|
|
2000
2021
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
2001
2022
|
|
|
2002
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2023
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2003
2024
|
}
|
|
2004
2025
|
|
|
2005
2026
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2007,7 +2028,7 @@ pub const Effects = struct {
|
|
|
2007
2028
|
var side = battle.side(player);
|
|
2008
2029
|
var foe = battle.foe(player);
|
|
2009
2030
|
|
|
2010
|
-
|
|
2031
|
+
const side_stored = side.stored();
|
|
2011
2032
|
var foe_stored = foe.stored();
|
|
2012
2033
|
|
|
2013
2034
|
const player_ident = battle.active(player);
|
|
@@ -2019,8 +2040,8 @@ pub const Effects = struct {
|
|
|
2019
2040
|
side.active.stats = side_stored.stats;
|
|
2020
2041
|
foe.active.stats = foe_stored.stats;
|
|
2021
2042
|
|
|
2022
|
-
try log.activate(player_ident, .Haze);
|
|
2023
|
-
try log.clearallboost();
|
|
2043
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2044
|
+
try log.clearallboost(.{});
|
|
2024
2045
|
|
|
2025
2046
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
2026
2047
|
if (showdown) {
|
|
@@ -2028,24 +2049,26 @@ pub const Effects = struct {
|
|
|
2028
2049
|
const p: Player = @enumFromInt(i);
|
|
2029
2050
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
2030
2051
|
if (p != player and Status.any(s.stored().status)) {
|
|
2031
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2052
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2032
2053
|
s.stored().status = 0;
|
|
2054
|
+
options.chance.observe(.sleep, p, .None);
|
|
2033
2055
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2034
2056
|
s.stored().status = Status.init(.PSN);
|
|
2035
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2057
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2036
2058
|
}
|
|
2037
|
-
try clearVolatiles(battle, p,
|
|
2059
|
+
try clearVolatiles(battle, p, options);
|
|
2038
2060
|
}
|
|
2039
2061
|
} else {
|
|
2040
2062
|
if (Status.any(foe_stored.status)) {
|
|
2041
2063
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2042
2064
|
foe.last_selected_move = .SKIP_TURN;
|
|
2065
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2043
2066
|
}
|
|
2044
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2067
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2045
2068
|
foe_stored.status = 0;
|
|
2046
2069
|
}
|
|
2047
|
-
try clearVolatiles(battle, player,
|
|
2048
|
-
try clearVolatiles(battle, player.foe(),
|
|
2070
|
+
try clearVolatiles(battle, player, options);
|
|
2071
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2049
2072
|
}
|
|
2050
2073
|
}
|
|
2051
2074
|
|
|
@@ -2057,7 +2080,7 @@ pub const Effects = struct {
|
|
|
2057
2080
|
// GLITCH: HP recovery move failure glitches
|
|
2058
2081
|
const delta = stored.stats.hp - stored.hp;
|
|
2059
2082
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2060
|
-
return
|
|
2083
|
+
return options.log.fail(.{ ident, .None });
|
|
2061
2084
|
}
|
|
2062
2085
|
|
|
2063
2086
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2065,17 +2088,17 @@ pub const Effects = struct {
|
|
|
2065
2088
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2066
2089
|
if (showdown) battle.rng.advance(1);
|
|
2067
2090
|
stored.status = Status.slf(2);
|
|
2068
|
-
try options.log.
|
|
2091
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2069
2092
|
stored.hp = stored.stats.hp;
|
|
2070
2093
|
} else {
|
|
2071
2094
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2072
2095
|
}
|
|
2073
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2096
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2074
2097
|
}
|
|
2075
2098
|
|
|
2076
2099
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2077
2100
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2078
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2101
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2079
2102
|
}
|
|
2080
2103
|
|
|
2081
2104
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2084,33 +2107,31 @@ pub const Effects = struct {
|
|
|
2084
2107
|
if (showdown) {
|
|
2085
2108
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2086
2109
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2087
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2110
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2088
2111
|
}
|
|
2089
2112
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2090
2113
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2091
2114
|
} else {
|
|
2092
2115
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2093
2116
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2094
|
-
try options.log.lastmiss();
|
|
2095
|
-
return options.log.miss(battle.active(player));
|
|
2117
|
+
try options.log.lastmiss(.{});
|
|
2118
|
+
return options.log.miss(.{battle.active(player)});
|
|
2096
2119
|
}
|
|
2097
2120
|
}
|
|
2098
2121
|
|
|
2099
|
-
try options.chance.commit(player, .hit);
|
|
2100
2122
|
foe.active.volatiles.LeechSeed = true;
|
|
2101
2123
|
|
|
2102
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2124
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2103
2125
|
}
|
|
2104
2126
|
|
|
2105
2127
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2106
2128
|
var side = battle.side(player);
|
|
2129
|
+
const ident = battle.active(player);
|
|
2107
2130
|
|
|
2108
|
-
if (side.active.volatiles.LightScreen) {
|
|
2109
|
-
return options.log.fail(battle.active(player), .None);
|
|
2110
|
-
}
|
|
2131
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2111
2132
|
side.active.volatiles.LightScreen = true;
|
|
2112
2133
|
|
|
2113
|
-
try options.log.start(
|
|
2134
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2114
2135
|
}
|
|
2115
2136
|
|
|
2116
2137
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2138,17 +2159,15 @@ pub const Effects = struct {
|
|
|
2138
2159
|
if (!has_mimic) {
|
|
2139
2160
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2140
2161
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2141
|
-
|
|
2142
|
-
return try options.log.fail(battle.active(player.foe()), .None);
|
|
2162
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2143
2163
|
}
|
|
2144
2164
|
}
|
|
2145
2165
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2146
|
-
try options.chance.commit(player, .hit);
|
|
2147
2166
|
|
|
2148
2167
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2149
2168
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2150
2169
|
|
|
2151
|
-
try options.log.
|
|
2170
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2152
2171
|
}
|
|
2153
2172
|
|
|
2154
2173
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2157,7 +2176,7 @@ pub const Effects = struct {
|
|
|
2157
2176
|
if (side.active.volatiles.Mist) return;
|
|
2158
2177
|
side.active.volatiles.Mist = true;
|
|
2159
2178
|
|
|
2160
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2179
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2161
2180
|
}
|
|
2162
2181
|
|
|
2163
2182
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2183,25 +2202,25 @@ pub const Effects = struct {
|
|
|
2183
2202
|
|
|
2184
2203
|
if (showdown) {
|
|
2185
2204
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2186
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2187
|
-
|
|
2205
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2206
|
+
return log.immune(.{ foe_ident, .None });
|
|
2207
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2188
2208
|
}
|
|
2189
2209
|
if (Status.any(foe_stored.status)) {
|
|
2190
|
-
return log.fail(
|
|
2210
|
+
return log.fail(.{
|
|
2191
2211
|
foe_ident,
|
|
2192
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2193
|
-
);
|
|
2212
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2213
|
+
});
|
|
2194
2214
|
}
|
|
2195
2215
|
if (!showdown) {
|
|
2196
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2216
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2197
2217
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2198
2218
|
}
|
|
2199
|
-
try options.chance.commit(player, .hit);
|
|
2200
2219
|
|
|
2201
2220
|
foe_stored.status = Status.init(.PAR);
|
|
2202
2221
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2203
2222
|
|
|
2204
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2223
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2205
2224
|
}
|
|
2206
2225
|
|
|
2207
2226
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2223,12 +2242,12 @@ pub const Effects = struct {
|
|
|
2223
2242
|
foe_stored.status = Status.init(.PAR);
|
|
2224
2243
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2225
2244
|
|
|
2226
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2245
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2227
2246
|
}
|
|
2228
2247
|
|
|
2229
2248
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2230
2249
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2231
|
-
try options.log.fieldactivate();
|
|
2250
|
+
try options.log.fieldactivate(.{});
|
|
2232
2251
|
}
|
|
2233
2252
|
}
|
|
2234
2253
|
|
|
@@ -2243,28 +2262,27 @@ pub const Effects = struct {
|
|
|
2243
2262
|
return;
|
|
2244
2263
|
} else if (foe.active.volatiles.Substitute) {
|
|
2245
2264
|
if (move.effect != .Poison) return;
|
|
2246
|
-
return log.fail(foe_ident, .None);
|
|
2265
|
+
return log.fail(.{ foe_ident, .None });
|
|
2247
2266
|
} else if (Status.any(foe_stored.status)) {
|
|
2248
2267
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2249
2268
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2250
2269
|
// 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))
|
|
2270
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2252
2271
|
if (!showdown)
|
|
2253
|
-
.Poison
|
|
2272
|
+
Fail.Poison
|
|
2254
2273
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2255
|
-
if (toxic) .Toxic else .Poison
|
|
2274
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2256
2275
|
else
|
|
2257
|
-
.None
|
|
2276
|
+
Fail.None
|
|
2258
2277
|
else
|
|
2259
|
-
.None);
|
|
2278
|
+
Fail.None });
|
|
2260
2279
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2261
2280
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2262
|
-
return log.immune(foe_ident, .None);
|
|
2281
|
+
return log.immune(.{ foe_ident, .None });
|
|
2263
2282
|
}
|
|
2264
2283
|
|
|
2265
2284
|
if (move.effect == .Poison) {
|
|
2266
2285
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2267
|
-
try options.chance.commit(player, .hit);
|
|
2268
2286
|
} else {
|
|
2269
2287
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2270
2288
|
return;
|
|
@@ -2278,7 +2296,7 @@ pub const Effects = struct {
|
|
|
2278
2296
|
foe.active.volatiles.toxic = 0;
|
|
2279
2297
|
}
|
|
2280
2298
|
|
|
2281
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2299
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2282
2300
|
}
|
|
2283
2301
|
|
|
2284
2302
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2298,22 +2316,23 @@ pub const Effects = struct {
|
|
|
2298
2316
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2299
2317
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2300
2318
|
|
|
2301
|
-
try options.log.
|
|
2319
|
+
try options.log.damage(.{
|
|
2302
2320
|
battle.active(player),
|
|
2303
2321
|
stored,
|
|
2304
2322
|
.RecoilOf,
|
|
2305
2323
|
battle.active(player.foe()),
|
|
2306
|
-
);
|
|
2324
|
+
});
|
|
2307
2325
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2308
2326
|
}
|
|
2309
2327
|
|
|
2310
2328
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2311
2329
|
var side = battle.side(player);
|
|
2330
|
+
const ident = battle.active(player);
|
|
2312
2331
|
|
|
2313
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2332
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2314
2333
|
side.active.volatiles.Reflect = true;
|
|
2315
2334
|
|
|
2316
|
-
try options.log.start(
|
|
2335
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2317
2336
|
}
|
|
2318
2337
|
|
|
2319
2338
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2327,18 +2346,17 @@ pub const Effects = struct {
|
|
|
2327
2346
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2328
2347
|
} else {
|
|
2329
2348
|
if (Status.any(foe_stored.status)) {
|
|
2330
|
-
return options.log.fail(
|
|
2349
|
+
return options.log.fail(.{
|
|
2331
2350
|
foe_ident,
|
|
2332
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2333
|
-
);
|
|
2351
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2352
|
+
});
|
|
2334
2353
|
}
|
|
2335
|
-
// If
|
|
2354
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2336
2355
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2337
|
-
try options.chance.commit(player, .hit);
|
|
2338
2356
|
}
|
|
2339
2357
|
|
|
2340
2358
|
// Sleep Clause Mod
|
|
2341
|
-
if (
|
|
2359
|
+
if (pkmn.options.mod) {
|
|
2342
2360
|
for (foe.pokemon) |p| {
|
|
2343
2361
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2344
2362
|
}
|
|
@@ -2346,19 +2364,20 @@ pub const Effects = struct {
|
|
|
2346
2364
|
foe.active.volatiles.Recharging = false;
|
|
2347
2365
|
|
|
2348
2366
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2367
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2368
|
+
|
|
2349
2369
|
const last = battle.side(player).last_selected_move;
|
|
2350
|
-
try options.log.
|
|
2370
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2351
2371
|
}
|
|
2352
2372
|
|
|
2353
2373
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2354
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2374
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2355
2375
|
}
|
|
2356
2376
|
|
|
2357
2377
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2358
2378
|
var side = battle.side(player);
|
|
2359
2379
|
if (side.active.volatiles.Substitute) {
|
|
2360
|
-
|
|
2361
|
-
return;
|
|
2380
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2362
2381
|
}
|
|
2363
2382
|
|
|
2364
2383
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2370,17 +2389,16 @@ pub const Effects = struct {
|
|
|
2370
2389
|
// avoid having to convert to floating point)
|
|
2371
2390
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2372
2391
|
if (side.stored().hp < required_hp) {
|
|
2373
|
-
|
|
2374
|
-
return;
|
|
2392
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2375
2393
|
}
|
|
2376
2394
|
|
|
2377
2395
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2378
2396
|
side.stored().hp -= hp;
|
|
2379
2397
|
side.active.volatiles.substitute = hp + 1;
|
|
2380
2398
|
side.active.volatiles.Substitute = true;
|
|
2381
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2399
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2382
2400
|
if (hp > 0) {
|
|
2383
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2401
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2384
2402
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2385
2403
|
}
|
|
2386
2404
|
}
|
|
@@ -2389,13 +2407,11 @@ pub const Effects = struct {
|
|
|
2389
2407
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2390
2408
|
if (showdown) {
|
|
2391
2409
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2392
|
-
|
|
2393
|
-
try options.chance.commit(player, .hit);
|
|
2394
|
-
}
|
|
2410
|
+
_ = try checkHit(battle, player, move, options);
|
|
2395
2411
|
battle.last_damage = 0;
|
|
2396
2412
|
} else {
|
|
2397
|
-
try options.log.fail(battle.active(player), .None);
|
|
2398
|
-
try options.log.laststill();
|
|
2413
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2414
|
+
try options.log.laststill(.{});
|
|
2399
2415
|
}
|
|
2400
2416
|
}
|
|
2401
2417
|
|
|
@@ -2406,6 +2422,8 @@ pub const Effects = struct {
|
|
|
2406
2422
|
|
|
2407
2423
|
volatiles.Thrashing = true;
|
|
2408
2424
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2425
|
+
|
|
2426
|
+
options.chance.observe(.attacking, player, .started);
|
|
2409
2427
|
}
|
|
2410
2428
|
|
|
2411
2429
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2434,7 +2452,7 @@ pub const Effects = struct {
|
|
|
2434
2452
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2435
2453
|
}
|
|
2436
2454
|
|
|
2437
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2455
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2438
2456
|
}
|
|
2439
2457
|
|
|
2440
2458
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2444,13 +2462,14 @@ pub const Effects = struct {
|
|
|
2444
2462
|
if (side.active.volatiles.Binding) return;
|
|
2445
2463
|
side.active.volatiles.Binding = true;
|
|
2446
2464
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2447
|
-
// (Pokémon Showdown
|
|
2465
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2448
2466
|
if (!showdown) {
|
|
2449
2467
|
foe.active.volatiles.Recharging = false;
|
|
2450
2468
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2451
2469
|
|
|
2452
2470
|
side.active.volatiles.attacks =
|
|
2453
2471
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2472
|
+
options.chance.observe(.binding, player, .started);
|
|
2454
2473
|
}
|
|
2455
2474
|
|
|
2456
2475
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2464,87 +2483,87 @@ pub const Effects = struct {
|
|
|
2464
2483
|
switch (move.effect) {
|
|
2465
2484
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2466
2485
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2467
|
-
if (boosts.atk == 6) return
|
|
2486
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2468
2487
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2469
2488
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2470
|
-
const reason = if (move.effect == .Rage)
|
|
2489
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2471
2490
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2472
2491
|
boosts.atk -= 1;
|
|
2473
2492
|
if (showdown) {
|
|
2474
|
-
try log.boost(ident, reason, n);
|
|
2475
|
-
try log.boost(ident,
|
|
2493
|
+
try log.boost(.{ ident, reason, n });
|
|
2494
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2476
2495
|
if (move.effect == .Rage) return;
|
|
2477
2496
|
}
|
|
2478
|
-
return
|
|
2497
|
+
return log.fail(.{ ident, .None });
|
|
2479
2498
|
}
|
|
2480
|
-
|
|
2499
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2481
2500
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2482
2501
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2483
|
-
try log.boost(ident, reason, n);
|
|
2502
|
+
try log.boost(.{ ident, reason, n });
|
|
2484
2503
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2485
2504
|
if (showdown and move.effect == .Rage) return;
|
|
2486
2505
|
},
|
|
2487
2506
|
.DefenseUp1, .DefenseUp2 => {
|
|
2488
2507
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2489
|
-
if (boosts.def == 6) return
|
|
2508
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2490
2509
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2491
2510
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2492
2511
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2493
2512
|
boosts.def -= 1;
|
|
2494
2513
|
if (showdown) {
|
|
2495
|
-
try log.boost(ident, .Defense, n);
|
|
2496
|
-
try log.boost(ident, .Defense, -1);
|
|
2514
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2515
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2497
2516
|
}
|
|
2498
|
-
return
|
|
2517
|
+
return log.fail(.{ ident, .None });
|
|
2499
2518
|
}
|
|
2500
|
-
|
|
2519
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2501
2520
|
const stat = unmodifiedStats(battle, side).def;
|
|
2502
2521
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2503
|
-
try log.boost(ident, .Defense, n);
|
|
2522
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2504
2523
|
},
|
|
2505
2524
|
.SpeedUp2 => {
|
|
2506
2525
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2507
|
-
if (boosts.spe == 6) return
|
|
2526
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2508
2527
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2509
2528
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2510
2529
|
boosts.spe -= 1;
|
|
2511
2530
|
if (showdown) {
|
|
2512
|
-
try log.boost(ident, .Speed, 2);
|
|
2513
|
-
try log.boost(ident, .Speed, -1);
|
|
2531
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2532
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2514
2533
|
}
|
|
2515
|
-
return
|
|
2534
|
+
return log.fail(.{ ident, .None });
|
|
2516
2535
|
}
|
|
2517
|
-
|
|
2536
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2518
2537
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2519
2538
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2520
|
-
try log.boost(ident, .Speed, 2);
|
|
2539
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2521
2540
|
},
|
|
2522
2541
|
.SpecialUp1, .SpecialUp2 => {
|
|
2523
2542
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2524
|
-
if (boosts.spc == 6) return
|
|
2543
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2525
2544
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2526
2545
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2527
2546
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2528
2547
|
boosts.spc -= 1;
|
|
2529
2548
|
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);
|
|
2549
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2550
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2551
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2552
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2534
2553
|
}
|
|
2535
|
-
return
|
|
2554
|
+
return log.fail(.{ ident, .None });
|
|
2536
2555
|
}
|
|
2537
|
-
|
|
2556
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2538
2557
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2539
2558
|
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);
|
|
2559
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2560
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2542
2561
|
},
|
|
2543
2562
|
.EvasionUp1 => {
|
|
2544
2563
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2545
|
-
if (boosts.evasion == 6) return
|
|
2564
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2546
2565
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2547
|
-
try log.boost(ident, .Evasion, 1);
|
|
2566
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2548
2567
|
},
|
|
2549
2568
|
else => unreachable,
|
|
2550
2569
|
}
|
|
@@ -2559,7 +2578,7 @@ pub const Effects = struct {
|
|
|
2559
2578
|
const foe_ident = battle.active(player.foe());
|
|
2560
2579
|
const secondary = move.effect.isStatDownChance();
|
|
2561
2580
|
|
|
2562
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2581
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2563
2582
|
|
|
2564
2583
|
if (secondary) {
|
|
2565
2584
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2567,7 +2586,6 @@ pub const Effects = struct {
|
|
|
2567
2586
|
} else {
|
|
2568
2587
|
// checkHit already checks for Invulnerable
|
|
2569
2588
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2570
|
-
try options.chance.commit(player, .hit);
|
|
2571
2589
|
}
|
|
2572
2590
|
|
|
2573
2591
|
var stats = &foe.active.stats;
|
|
@@ -2577,82 +2595,82 @@ pub const Effects = struct {
|
|
|
2577
2595
|
switch (move.effect) {
|
|
2578
2596
|
.AttackDown1, .AttackDownChance => {
|
|
2579
2597
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2580
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2598
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2581
2599
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2582
2600
|
if (stats.atk == 1) {
|
|
2583
2601
|
boosts.atk += 1;
|
|
2584
2602
|
if (showdown) {
|
|
2585
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2586
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2603
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2604
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2587
2605
|
}
|
|
2588
|
-
return
|
|
2606
|
+
return log.fail(.{ foe_ident, .None });
|
|
2589
2607
|
}
|
|
2590
|
-
|
|
2608
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2591
2609
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2592
2610
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2593
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2611
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2594
2612
|
},
|
|
2595
2613
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2596
2614
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2597
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2615
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2598
2616
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2599
2617
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2600
2618
|
if (stats.def == 1) {
|
|
2601
2619
|
boosts.def += 1;
|
|
2602
2620
|
if (showdown) {
|
|
2603
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2604
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2621
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2622
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2605
2623
|
}
|
|
2606
|
-
return
|
|
2624
|
+
return log.fail(.{ foe_ident, .None });
|
|
2607
2625
|
}
|
|
2608
|
-
|
|
2626
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2609
2627
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2610
2628
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2611
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2629
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2612
2630
|
},
|
|
2613
2631
|
.SpeedDown1, .SpeedDownChance => {
|
|
2614
2632
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2615
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2633
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2616
2634
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2617
2635
|
if (stats.spe == 1) {
|
|
2618
2636
|
boosts.spe += 1;
|
|
2619
2637
|
if (showdown) {
|
|
2620
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2621
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2638
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2639
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2622
2640
|
}
|
|
2623
|
-
return
|
|
2641
|
+
return log.fail(.{ foe_ident, .None });
|
|
2624
2642
|
}
|
|
2625
|
-
|
|
2643
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2626
2644
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2627
2645
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2628
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2646
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2629
2647
|
assert(boosts.spe >= -6);
|
|
2630
2648
|
},
|
|
2631
2649
|
.SpecialDownChance => {
|
|
2632
2650
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2633
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2651
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2634
2652
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2635
2653
|
if (stats.spc == 1) {
|
|
2636
2654
|
boosts.spc += 1;
|
|
2637
2655
|
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);
|
|
2656
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2657
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2658
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2659
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2642
2660
|
}
|
|
2643
|
-
return
|
|
2661
|
+
return log.fail(.{ foe_ident, .None });
|
|
2644
2662
|
}
|
|
2645
|
-
|
|
2663
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2646
2664
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2647
2665
|
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);
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2667
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2650
2668
|
},
|
|
2651
2669
|
.AccuracyDown1 => {
|
|
2652
2670
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2653
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2671
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2654
2672
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2655
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2673
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2656
2674
|
},
|
|
2657
2675
|
else => unreachable,
|
|
2658
2676
|
}
|
|
@@ -2676,40 +2694,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2676
2694
|
}
|
|
2677
2695
|
}
|
|
2678
2696
|
|
|
2679
|
-
|
|
2697
|
+
fn isForced(active: anytype) bool {
|
|
2680
2698
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2681
2699
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2682
2700
|
}
|
|
2683
2701
|
|
|
2684
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2702
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2685
2703
|
var log = options.log;
|
|
2686
2704
|
var side = battle.side(who);
|
|
2687
2705
|
var volatiles = &side.active.volatiles;
|
|
2688
2706
|
const ident = battle.active(who);
|
|
2689
2707
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
try log.end(ident, .DisableSilent);
|
|
2708
|
+
if (volatiles.disable_move != 0) {
|
|
2709
|
+
volatiles.disable_move = 0;
|
|
2710
|
+
volatiles.disable_duration = 0;
|
|
2711
|
+
options.chance.observe(.disable, who, .None);
|
|
2712
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2696
2713
|
}
|
|
2697
2714
|
if (volatiles.Confusion) {
|
|
2698
2715
|
// volatiles.confusion is left unchanged
|
|
2699
2716
|
volatiles.Confusion = false;
|
|
2700
|
-
|
|
2717
|
+
options.chance.observe(.confusion, who, .None);
|
|
2718
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2701
2719
|
}
|
|
2702
2720
|
if (volatiles.Mist) {
|
|
2703
2721
|
volatiles.Mist = false;
|
|
2704
|
-
try log.end(ident, .MistSilent);
|
|
2722
|
+
try log.end(.{ ident, .MistSilent });
|
|
2705
2723
|
}
|
|
2706
2724
|
if (volatiles.FocusEnergy) {
|
|
2707
2725
|
volatiles.FocusEnergy = false;
|
|
2708
|
-
try log.end(ident, .FocusEnergySilent);
|
|
2726
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2709
2727
|
}
|
|
2710
2728
|
if (volatiles.LeechSeed) {
|
|
2711
2729
|
volatiles.LeechSeed = false;
|
|
2712
|
-
try log.end(ident, .LeechSeedSilent);
|
|
2730
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2713
2731
|
}
|
|
2714
2732
|
if (!showdown and volatiles.Toxic) {
|
|
2715
2733
|
volatiles.Toxic = false;
|
|
@@ -2717,24 +2735,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2717
2735
|
}
|
|
2718
2736
|
if (volatiles.LightScreen) {
|
|
2719
2737
|
volatiles.LightScreen = false;
|
|
2720
|
-
try log.end(ident, .LightScreenSilent);
|
|
2738
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2721
2739
|
}
|
|
2722
2740
|
if (volatiles.Reflect) {
|
|
2723
2741
|
volatiles.Reflect = false;
|
|
2724
|
-
try log.end(ident, .ReflectSilent);
|
|
2742
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2725
2743
|
}
|
|
2726
2744
|
if (showdown and volatiles.Toxic) {
|
|
2727
2745
|
volatiles.Toxic = false;
|
|
2728
2746
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2729
2747
|
volatiles.toxic = 0;
|
|
2730
|
-
try log.end(ident, .ToxicSilent);
|
|
2748
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2731
2749
|
}
|
|
2732
2750
|
}
|
|
2733
2751
|
|
|
2752
|
+
fn decrement(
|
|
2753
|
+
comptime field: chance.Action.Field,
|
|
2754
|
+
player: Player,
|
|
2755
|
+
options: anytype,
|
|
2756
|
+
n: anytype,
|
|
2757
|
+
) @TypeOf(n) {
|
|
2758
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2759
|
+
.started, .ended => 0,
|
|
2760
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2761
|
+
else => unreachable,
|
|
2762
|
+
} else n - 1;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2734
2765
|
pub const Rolls = struct {
|
|
2735
|
-
|
|
2766
|
+
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2736
2767
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2737
2768
|
player == .P1
|
|
2769
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2770
|
+
battle.rng.speedTie()
|
|
2738
2771
|
else if (showdown)
|
|
2739
2772
|
battle.rng.range(u8, 0, 2) == 0
|
|
2740
2773
|
else
|
|
@@ -2744,21 +2777,31 @@ pub const Rolls = struct {
|
|
|
2744
2777
|
return p1;
|
|
2745
2778
|
}
|
|
2746
2779
|
|
|
2747
|
-
|
|
2780
|
+
fn criticalHit(
|
|
2781
|
+
battle: anytype,
|
|
2782
|
+
player: Player,
|
|
2783
|
+
rate: u8,
|
|
2784
|
+
spurious: bool,
|
|
2785
|
+
options: anytype,
|
|
2786
|
+
) !bool {
|
|
2748
2787
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2749
2788
|
val == .true
|
|
2789
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2790
|
+
battle.rng.criticalHit(player, rate)
|
|
2750
2791
|
else if (showdown)
|
|
2751
2792
|
battle.rng.chance(u8, rate, 256)
|
|
2752
2793
|
else
|
|
2753
2794
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2754
2795
|
|
|
2755
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2796
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2756
2797
|
return crit;
|
|
2757
2798
|
}
|
|
2758
2799
|
|
|
2759
|
-
|
|
2800
|
+
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2760
2801
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2761
2802
|
val
|
|
2803
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2804
|
+
battle.rng.damage(player)
|
|
2762
2805
|
else roll: {
|
|
2763
2806
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2764
2807
|
while (true) {
|
|
@@ -2772,21 +2815,27 @@ pub const Rolls = struct {
|
|
|
2772
2815
|
return roll;
|
|
2773
2816
|
}
|
|
2774
2817
|
|
|
2775
|
-
|
|
2818
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2819
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2820
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2776
2821
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2777
2822
|
val == .true
|
|
2823
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2824
|
+
battle.rng.hit(player, acc)
|
|
2778
2825
|
else if (showdown)
|
|
2779
|
-
battle.rng.chance(
|
|
2826
|
+
battle.rng.chance(uN, acc, 256)
|
|
2780
2827
|
else
|
|
2781
|
-
battle.rng.next() <
|
|
2828
|
+
battle.rng.next() < acc;
|
|
2782
2829
|
|
|
2783
|
-
options.chance.hit(ok,
|
|
2830
|
+
try options.chance.hit(player, ok, acc);
|
|
2784
2831
|
return ok;
|
|
2785
2832
|
}
|
|
2786
2833
|
|
|
2787
|
-
|
|
2834
|
+
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2788
2835
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2789
2836
|
val == .true
|
|
2837
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2838
|
+
battle.rng.confused(player)
|
|
2790
2839
|
else if (showdown)
|
|
2791
2840
|
!battle.rng.chance(u8, 128, 256)
|
|
2792
2841
|
else
|
|
@@ -2796,9 +2845,11 @@ pub const Rolls = struct {
|
|
|
2796
2845
|
return cfz;
|
|
2797
2846
|
}
|
|
2798
2847
|
|
|
2799
|
-
|
|
2848
|
+
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2800
2849
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2801
2850
|
val == .true
|
|
2851
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2852
|
+
battle.rng.paralyzed(player)
|
|
2802
2853
|
else if (showdown)
|
|
2803
2854
|
battle.rng.chance(u8, 63, 256)
|
|
2804
2855
|
else
|
|
@@ -2808,9 +2859,11 @@ pub const Rolls = struct {
|
|
|
2808
2859
|
return par;
|
|
2809
2860
|
}
|
|
2810
2861
|
|
|
2811
|
-
|
|
2862
|
+
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2812
2863
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2813
2864
|
val == .true
|
|
2865
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2866
|
+
battle.rng.secondaryChance(player, 25)
|
|
2814
2867
|
else if (showdown)
|
|
2815
2868
|
battle.rng.chance(u8, 25, 256)
|
|
2816
2869
|
else
|
|
@@ -2820,11 +2873,13 @@ pub const Rolls = struct {
|
|
|
2820
2873
|
return proc;
|
|
2821
2874
|
}
|
|
2822
2875
|
|
|
2823
|
-
|
|
2876
|
+
fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2824
2877
|
const rate: u8 = if (low) 26 else 77;
|
|
2825
2878
|
|
|
2826
2879
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2827
2880
|
val == .true
|
|
2881
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2882
|
+
battle.rng.secondaryChance(player, rate)
|
|
2828
2883
|
else if (showdown)
|
|
2829
2884
|
battle.rng.chance(u8, rate, 256)
|
|
2830
2885
|
else
|
|
@@ -2834,11 +2889,13 @@ pub const Rolls = struct {
|
|
|
2834
2889
|
return proc;
|
|
2835
2890
|
}
|
|
2836
2891
|
|
|
2837
|
-
|
|
2892
|
+
fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
|
|
2838
2893
|
const rate: u8 = if (low) 52 else 103;
|
|
2839
2894
|
|
|
2840
2895
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2841
2896
|
val == .true
|
|
2897
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2898
|
+
battle.rng.secondaryChance(player, rate)
|
|
2842
2899
|
else if (showdown)
|
|
2843
2900
|
battle.rng.chance(u8, rate, 256)
|
|
2844
2901
|
else
|
|
@@ -2848,9 +2905,11 @@ pub const Rolls = struct {
|
|
|
2848
2905
|
return proc;
|
|
2849
2906
|
}
|
|
2850
2907
|
|
|
2851
|
-
|
|
2908
|
+
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2852
2909
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2853
2910
|
val == .true
|
|
2911
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2912
|
+
battle.rng.secondaryChance(player, 85)
|
|
2854
2913
|
else if (showdown)
|
|
2855
2914
|
battle.rng.chance(u8, 85, 256)
|
|
2856
2915
|
else
|
|
@@ -2860,14 +2919,14 @@ pub const Rolls = struct {
|
|
|
2860
2919
|
return proc;
|
|
2861
2920
|
}
|
|
2862
2921
|
|
|
2863
|
-
|
|
2922
|
+
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2864
2923
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2865
2924
|
val
|
|
2866
|
-
else if (
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2925
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2926
|
+
battle.rng.metronome(player)
|
|
2927
|
+
else if (showdown)
|
|
2928
|
+
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2929
|
+
else move: {
|
|
2871
2930
|
while (true) {
|
|
2872
2931
|
const r = battle.rng.next();
|
|
2873
2932
|
if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
|
|
@@ -2880,9 +2939,11 @@ pub const Rolls = struct {
|
|
|
2880
2939
|
return move;
|
|
2881
2940
|
}
|
|
2882
2941
|
|
|
2883
|
-
|
|
2942
|
+
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2884
2943
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2885
2944
|
val - 1
|
|
2945
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2946
|
+
battle.rng.psywave(player)
|
|
2886
2947
|
else if (showdown)
|
|
2887
2948
|
battle.rng.range(u8, 0, max)
|
|
2888
2949
|
else power: {
|
|
@@ -2897,11 +2958,14 @@ pub const Rolls = struct {
|
|
|
2897
2958
|
return power;
|
|
2898
2959
|
}
|
|
2899
2960
|
|
|
2900
|
-
|
|
2901
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2902
|
-
@intCast(val)
|
|
2961
|
+
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2962
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2963
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2964
|
+
else
|
|
2965
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2966
|
+
battle.rng.sleepDuration(player)
|
|
2903
2967
|
else if (showdown)
|
|
2904
|
-
|
|
2968
|
+
battle.rng.range(u3, 1, 8)
|
|
2905
2969
|
else duration: {
|
|
2906
2970
|
while (true) {
|
|
2907
2971
|
const r = battle.rng.next() & 7;
|
|
@@ -2910,60 +2974,74 @@ pub const Rolls = struct {
|
|
|
2910
2974
|
};
|
|
2911
2975
|
|
|
2912
2976
|
assert(duration >= 1 and duration <= 7);
|
|
2913
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2914
2978
|
return duration;
|
|
2915
2979
|
}
|
|
2916
2980
|
|
|
2917
|
-
|
|
2918
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2919
|
-
val
|
|
2981
|
+
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2982
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2983
|
+
if (val >= 1 and val <= 8) val else null
|
|
2984
|
+
else
|
|
2985
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2986
|
+
battle.rng.disableDuration(player)
|
|
2920
2987
|
else if (showdown)
|
|
2921
|
-
|
|
2988
|
+
battle.rng.range(u4, 1, 9)
|
|
2922
2989
|
else
|
|
2923
2990
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2924
2991
|
|
|
2925
2992
|
assert(duration >= 1 and duration <= 8);
|
|
2926
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2927
2994
|
return duration;
|
|
2928
2995
|
}
|
|
2929
2996
|
|
|
2930
|
-
|
|
2931
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2932
|
-
@intCast(val)
|
|
2997
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2998
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3000
|
+
else
|
|
3001
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3002
|
+
battle.rng.confusionDuration(player)
|
|
2933
3003
|
else if (showdown)
|
|
2934
|
-
|
|
3004
|
+
battle.rng.range(u3, 2, 6)
|
|
2935
3005
|
else
|
|
2936
3006
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2937
3007
|
|
|
2938
3008
|
assert(duration >= 2 and duration <= 5);
|
|
2939
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2940
3010
|
return duration;
|
|
2941
3011
|
}
|
|
2942
3012
|
|
|
2943
|
-
|
|
2944
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2945
|
-
@intCast(val)
|
|
3013
|
+
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3014
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3015
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3016
|
+
else
|
|
3017
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3018
|
+
battle.rng.attackingDuration(player)
|
|
2946
3019
|
else if (showdown)
|
|
2947
|
-
|
|
3020
|
+
battle.rng.range(u3, 2, 4)
|
|
2948
3021
|
else
|
|
2949
3022
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2950
3023
|
|
|
2951
3024
|
assert(duration >= 2 and duration <= 3);
|
|
2952
|
-
options.chance.duration(
|
|
3025
|
+
options.chance.duration(player, duration);
|
|
2953
3026
|
return duration;
|
|
2954
3027
|
}
|
|
2955
3028
|
|
|
2956
|
-
|
|
3029
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
3030
|
+
|
|
3031
|
+
fn distribution(
|
|
2957
3032
|
battle: anytype,
|
|
2958
3033
|
comptime effect: Move.Effect,
|
|
2959
3034
|
player: Player,
|
|
2960
3035
|
options: anytype,
|
|
2961
3036
|
) !u3 {
|
|
2962
3037
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2963
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2964
|
-
@intCast(val)
|
|
3038
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3039
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3040
|
+
else
|
|
3041
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3042
|
+
battle.rng.distribution(player)
|
|
2965
3043
|
else if (showdown)
|
|
2966
|
-
DISTRIBUTION[battle.rng.range(
|
|
3044
|
+
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2967
3045
|
else n: {
|
|
2968
3046
|
const r = (battle.rng.next() & 3);
|
|
2969
3047
|
break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
@@ -2971,7 +3049,7 @@ pub const Rolls = struct {
|
|
|
2971
3049
|
|
|
2972
3050
|
assert(n >= 2 and n <= 5);
|
|
2973
3051
|
if (effect == .Binding) {
|
|
2974
|
-
options.chance.duration(
|
|
3052
|
+
options.chance.duration(player, n);
|
|
2975
3053
|
} else {
|
|
2976
3054
|
try options.chance.multiHit(player, n);
|
|
2977
3055
|
}
|
|
@@ -2986,15 +3064,15 @@ pub const Rolls = struct {
|
|
|
2986
3064
|
options: anytype,
|
|
2987
3065
|
) !u4 {
|
|
2988
3066
|
// TODO: consider throwing error instead of rerolling?
|
|
2989
|
-
const
|
|
3067
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2990
3068
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2991
3069
|
val
|
|
2992
3070
|
else
|
|
2993
3071
|
null
|
|
2994
3072
|
else
|
|
2995
|
-
null
|
|
2996
|
-
|
|
2997
|
-
|
|
3073
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3074
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3075
|
+
else slot: {
|
|
2998
3076
|
if (showdown) {
|
|
2999
3077
|
if (check_pp == 0) {
|
|
3000
3078
|
var i: usize = moves.len;
|
|
@@ -3010,10 +3088,9 @@ pub const Rolls = struct {
|
|
|
3010
3088
|
while (i < moves.len and r > 0) : (i += 1) {
|
|
3011
3089
|
if (moves[i].pp > 0) {
|
|
3012
3090
|
r -= 1;
|
|
3013
|
-
if (r == 0) break;
|
|
3091
|
+
if (r == 0) break :slot @intCast(i + 1);
|
|
3014
3092
|
}
|
|
3015
3093
|
}
|
|
3016
|
-
break :slot @intCast(i + 1);
|
|
3017
3094
|
}
|
|
3018
3095
|
}
|
|
3019
3096
|
|
|
@@ -3075,11 +3152,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3075
3152
|
const side = battle.side(player);
|
|
3076
3153
|
const foe = battle.foe(player);
|
|
3077
3154
|
|
|
3078
|
-
|
|
3155
|
+
const active = &side.active;
|
|
3079
3156
|
const stored = side.stored();
|
|
3080
3157
|
|
|
3081
3158
|
// 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
|
|
3159
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3083
3160
|
//
|
|
3084
3161
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3085
3162
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3087,7 +3164,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3087
3164
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3088
3165
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3089
3166
|
// strictly correct as the player would not have been presented the option to move or
|
|
3090
|
-
// switch at all
|
|
3167
|
+
// switch at all
|
|
3091
3168
|
if (isForced(active)) {
|
|
3092
3169
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3093
3170
|
n += 1;
|
|
@@ -3106,7 +3183,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3106
3183
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3107
3184
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3108
3185
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3109
|
-
// move normally if sleeping/frozen/bound
|
|
3186
|
+
// move normally if sleeping/frozen/bound
|
|
3110
3187
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3111
3188
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3112
3189
|
{
|
|
@@ -3119,7 +3196,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3119
3196
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3120
3197
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3121
3198
|
// 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
|
|
3199
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3123
3200
|
if (limited) {
|
|
3124
3201
|
assert(showdown);
|
|
3125
3202
|
assert(side.last_selected_move != .None);
|
|
@@ -3129,7 +3206,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3129
3206
|
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
3130
3207
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
3131
3208
|
const struggle =
|
|
3132
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.
|
|
3209
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3133
3210
|
const s = if (struggle) 0 else slot;
|
|
3134
3211
|
out[n] = .{ .type = .Move, .data = s };
|
|
3135
3212
|
n += 1;
|
|
@@ -3144,14 +3221,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3144
3221
|
const m = active.moves[slot - 1];
|
|
3145
3222
|
if (m.id == .None) break;
|
|
3146
3223
|
if (m.pp == 0) continue;
|
|
3147
|
-
if (active.volatiles.
|
|
3224
|
+
if (active.volatiles.disable_move == slot) continue;
|
|
3148
3225
|
out[n] = .{ .type = .Move, .data = slot };
|
|
3149
3226
|
n += 1;
|
|
3150
3227
|
}
|
|
3151
3228
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3152
3229
|
if (n == before) {
|
|
3153
3230
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3154
|
-
if (!
|
|
3231
|
+
if (!pkmn.options.mod) {
|
|
3155
3232
|
while (slot <= 4) : (slot += 1) {
|
|
3156
3233
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3157
3234
|
}
|