@pkmn/engine 0.1.0-dev.f918c3bd → 0.1.0-dev.fb6653d6

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