@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.3158fa12

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 (82) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -18
  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/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +13 -17
  20. package/build/pkg/gen1.js +6 -10
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -91
  23. package/build/pkg/index.js +6 -63
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +36 -71
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +141 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +142 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +349 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +18 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +287 -89
  39. package/package.json +58 -132
  40. package/src/bin/install-pkmn-engine +30 -20
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/{binding → bindings}/node.zig +8 -21
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +4 -4
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +14 -10
  49. package/src/lib/common/options.zig +6 -0
  50. package/src/lib/common/protocol.zig +223 -215
  51. package/src/lib/common/rational.zig +128 -32
  52. package/src/lib/common/rng.zig +7 -9
  53. package/src/lib/common/util.zig +50 -1
  54. package/src/lib/gen1/calc.zig +686 -159
  55. package/src/lib/gen1/chance.zig +405 -280
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +2 -4
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +24 -27
  60. package/src/lib/gen1/helpers.zig +92 -548
  61. package/src/lib/gen1/mechanics.zig +517 -411
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +84 -496
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +19 -22
  69. package/src/lib/gen2/generated.zig +19 -26
  70. package/src/lib/gen2/helpers.zig +46 -25
  71. package/src/lib/gen2/mechanics.zig +367 -336
  72. package/src/lib/node.zig +18 -0
  73. package/src/lib/pkmn.zig +37 -15
  74. package/src/lib/wasm.zig +17 -0
  75. package/src/tools/display/dom.ts +96 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -400
  78. package/build/test/display.js.map +0 -1
  79. package/src/lib/binding/wasm.zig +0 -12
  80. package/src/lib/gen1/README.md +0 -894
  81. package/src/lib/gen2/README.md +0 -329
  82. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,41 +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 data = @import("data.zig");
10
+ const std = @import("std");
11
11
 
12
12
  const assert = std.debug.assert;
13
-
14
- const expectEqual = std.testing.expectEqual;
15
-
16
- const showdown = pkmn.options.showdown;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
13
  const Boost = protocol.Boost;
24
- const Cant = protocol.Cant;
25
- const CureStatus = protocol.CureStatus;
14
+ const Choice = common.Choice;
26
15
  const Damage = protocol.Damage;
27
- const End = protocol.End;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
28
18
  const Fail = protocol.Fail;
29
- const Heal = protocol.Heal;
30
- const Immune = protocol.Immune;
31
-
32
19
  const Gen12 = rng.Gen12;
33
-
34
- const ActivePokemon = data.ActivePokemon;
35
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
36
22
  const Move = data.Move;
37
23
  const MoveSlot = data.MoveSlot;
38
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
39
27
  const Side = data.Side;
40
28
  const Species = data.Species;
41
29
  const Stats = data.Stats;
@@ -75,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
75
63
  var p1 = battle.side(.P1);
76
64
  var p2 = battle.side(.P2);
77
65
 
78
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
79
- 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;
80
68
 
81
69
  if (try turnOrder(battle, c1, c2, options) == .P1) {
82
70
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -94,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
94
82
  const p1 = battle.side(.P1);
95
83
  const p2 = battle.side(.P2);
96
84
 
97
- var p1_slot = findFirstAlive(p1);
85
+ const p1_slot = findFirstAlive(p1);
98
86
  assert(!showdown or p1_slot == 1);
99
87
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
100
88
 
101
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
102
90
  assert(!showdown or p2_slot == 1);
103
91
  if (p2_slot == 0) return Result.Win;
104
92
 
@@ -170,7 +158,7 @@ fn selectMove(
170
158
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
171
159
  // set the selected move will not actually be used, it will just be reported as having
172
160
  // been used (this differs from how Pokémon Showdown works, but its impossible to
173
- // replicate the incorrect behavior with the correct mechanisms).
161
+ // replicate the incorrect behavior with the correct mechanisms)
174
162
  saveMove(battle, player, choice);
175
163
  } else {
176
164
  assert(choice.data == 0);
@@ -178,7 +166,10 @@ fn selectMove(
178
166
  if (foe_choice.type == .Switch) {
179
167
  const last = side.active.move(battle.lastMove(player).index);
180
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
181
- 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
+ }
182
173
  }
183
174
  }
184
175
  return null;
@@ -257,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
257
248
  active.moves = incoming.moves;
258
249
 
259
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
260
252
 
261
253
  foe.active.volatiles.Binding = false;
262
254
 
263
255
  try options.log.switched(.{ battle.active(player), incoming });
264
- options.chance.switched(player, side.order[0], out);
265
256
 
266
257
  if (showdown and incoming.status == Status.TOX) {
267
258
  incoming.status = Status.init(.PSN);
268
259
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
269
260
  // rather not clutter up turnOrder just for this (incorrect) log message
270
- try options.log.status(.{ battle.active(player), incoming.status, protocol.Status.Silent });
261
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
271
262
  }
272
263
  }
273
264
 
@@ -351,13 +342,13 @@ fn doTurn(
351
342
  )) |r| return r;
352
343
  if (!replace) {
353
344
  if (player_choice.type != .Switch) {
354
- if (try checkFaint(battle, foe_player, options)) |r| return r;
345
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
355
346
  }
356
347
  if (residual) try handleResidual(battle, player, options);
357
- if (try checkFaint(battle, player, options)) |r| return r;
348
+ if (try checkFaint(battle, player, false, options)) |r| return r;
358
349
  } else if (foe_choice.type == .Pass) return null;
359
350
 
360
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
361
352
 
362
353
  residual = true;
363
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -373,10 +364,10 @@ fn doTurn(
373
364
  )) |r| return r;
374
365
  if (!replace) {
375
366
  if (!calc and foe_choice.type != .Switch) {
376
- if (try checkFaint(battle, player, options)) |r| return r;
367
+ if (try checkFaint(battle, player, true, options)) |r| return r;
377
368
  }
378
369
  if (residual) try handleResidual(battle, foe_player, options);
379
- if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
380
371
  }
381
372
 
382
373
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -411,7 +402,7 @@ fn executeMove(
411
402
  if (side.last_selected_move == .SKIP_TURN) {
412
403
  assert(!showdown);
413
404
  if (battle.foe(player).active.volatiles.Binding) {
414
- try options.log.cant(.{ battle.active(player), Cant.Bound });
405
+ try options.log.cant(.{ battle.active(player), .Bound });
415
406
  }
416
407
  return null;
417
408
  }
@@ -429,13 +420,13 @@ fn executeMove(
429
420
  mslot = @intCast(battle.lastMove(player).index);
430
421
  const stored = side.stored();
431
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
432
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
433
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
434
425
  side.last_selected_move == .HyperBeam or
435
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
436
427
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
437
428
  // thawed, in which case we will desync unless the last_selected_move happened to be at
438
- // 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
439
430
  if (!auto) {
440
431
  // side.active.moves(slot) is safe to check even though the slot in question might not
441
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -462,6 +453,25 @@ fn executeMove(
462
453
  .err => return @as(?Result, Result.Error),
463
454
  }
464
455
 
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;
473
+ }
474
+
465
475
  const can = skip_can or
466
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
467
477
  if (!can) return null;
@@ -488,36 +498,41 @@ fn beforeMove(
488
498
 
489
499
  if (Status.is(stored.status, .SLP)) {
490
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
491
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
492
- if (options.calc.modify(player, .sleep)) |extend| {
493
- 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,
494
509
  } else {
495
510
  stored.status -= 1;
496
511
  }
497
512
 
498
513
  const duration = Status.duration(stored.status);
499
- if (!Status.is(stored.status, .EXT)) {
500
- try options.chance.sleep(player, duration);
501
- }
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
502
517
 
503
518
  if (duration == 0) {
504
- try log.curestatus(.{ ident, before, CureStatus.Message });
519
+ try log.curestatus(.{ ident, before, .Message });
505
520
  stored.status = 0; // clears EXT if present
506
521
  } else {
507
- try log.cant(.{ ident, Cant.Sleep });
522
+ try log.cant(.{ ident, .Sleep });
508
523
  }
509
524
  side.last_used_move = .None;
510
525
  return .done;
511
526
  }
512
527
 
513
528
  if (Status.is(stored.status, .FRZ)) {
514
- try log.cant(.{ ident, Cant.Freeze });
529
+ try log.cant(.{ ident, .Freeze });
515
530
  side.last_used_move = .None;
516
531
  return .done;
517
532
  }
518
533
 
519
534
  if (skip or foe.active.volatiles.Binding) {
520
- try log.cant(.{ ident, Cant.Bound });
535
+ try log.cant(.{ ident, .Bound });
521
536
  return .done;
522
537
  }
523
538
 
@@ -525,27 +540,27 @@ fn beforeMove(
525
540
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
526
541
  // Pokémon can sometimes flinch multiple times from the same original hit
527
542
  if (!showdown) volatiles.Flinch = false;
528
- try log.cant(.{ ident, Cant.Flinch });
543
+ try log.cant(.{ ident, .Flinch });
529
544
  return .done;
530
545
  }
531
546
 
532
547
  if (volatiles.Recharging) {
533
548
  volatiles.Recharging = false;
534
- try log.cant(.{ ident, Cant.Recharge });
549
+ try log.cant(.{ ident, .Recharge });
535
550
  return .done;
536
551
  }
537
552
 
538
553
  if (volatiles.disable_duration > 0) {
539
- if (options.calc.modify(player, .disable)) |extend| {
540
- if (!extend) volatiles.disable_duration = 0;
541
- } else {
542
- volatiles.disable_duration -= 1;
543
- }
544
- try options.chance.disable(player, volatiles.disable_duration);
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
+ );
545
560
 
546
561
  if (volatiles.disable_duration == 0) {
547
562
  volatiles.disable_move = 0;
548
- try log.end(.{ ident, End.Disable });
563
+ try log.end(.{ ident, .Disable });
549
564
  }
550
565
  }
551
566
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -554,25 +569,30 @@ fn beforeMove(
554
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
555
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
556
571
  if (side.last_selected_move == .None) {
557
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
558
573
  return .err;
559
574
  }
560
575
 
561
576
  if (volatiles.Confusion) {
562
577
  assert(volatiles.confusion > 0);
563
-
564
- if (options.calc.modify(player, .confusion)) |extend| {
565
- 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,
566
585
  } else {
567
586
  volatiles.confusion -= 1;
568
587
  }
569
- try options.chance.confusion(player, volatiles.confusion);
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
570
590
 
571
591
  if (volatiles.confusion == 0) {
572
592
  volatiles.Confusion = false;
573
- try log.end(.{ ident, End.Confusion });
593
+ try log.end(.{ ident, .Confusion });
574
594
  } else {
575
- try log.activate(ident, .Confusion);
595
+ try log.activate(.{ ident, .Confusion });
576
596
 
577
597
  if (try Rolls.confused(battle, player, options)) {
578
598
  assert(!volatiles.MultiHit);
@@ -584,7 +604,9 @@ fn beforeMove(
584
604
  volatiles.Charging = false;
585
605
  volatiles.Binding = false;
586
606
  volatiles.Invulnerable = false;
587
- options.chance.clearDurations(player, null);
607
+
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
588
610
  {
589
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
590
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -592,7 +614,9 @@ fn beforeMove(
592
614
  const def = foe.active.stats.def;
593
615
  foe.active.stats.def = active.stats.def;
594
616
  defer foe.active.stats.def = def;
595
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
596
620
  }
597
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
598
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -621,9 +645,12 @@ fn beforeMove(
621
645
  volatiles.Thrashing = false;
622
646
  volatiles.Charging = false;
623
647
  volatiles.Binding = false;
624
- options.chance.clearDurations(player, null);
648
+
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
651
+
625
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
626
- try log.cant(.{ ident, Cant.Paralysis });
653
+ try log.cant(.{ ident, .Paralysis });
627
654
  return .done;
628
655
  }
629
656
 
@@ -639,28 +666,22 @@ fn beforeMove(
639
666
  volatiles.state +%= battle.last_damage;
640
667
  }
641
668
 
642
- assert(volatiles.attacks > 0);
643
-
644
- if (options.calc.modify(player, .attacking)) |extend| {
645
- if (!extend) volatiles.attacks = 0;
646
- } else {
647
- volatiles.attacks -= 1;
648
- }
649
- 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);
650
671
 
651
672
  if (volatiles.attacks != 0) {
652
- try log.activate(ident, .Bide);
673
+ try log.activate(.{ ident, .Bide });
653
674
  return .done;
654
675
  }
655
676
 
656
677
  volatiles.Bide = false;
657
- try log.end(.{ ident, End.Bide });
678
+ try log.end(.{ ident, .Bide });
658
679
 
659
680
  battle.last_damage = volatiles.state *% 2;
660
681
  volatiles.state = 0;
661
682
 
662
683
  if (battle.last_damage == 0) {
663
- try log.fail(.{ ident, Fail.None });
684
+ try log.fail(.{ ident, .None });
664
685
  return .done;
665
686
  }
666
687
 
@@ -680,39 +701,40 @@ fn beforeMove(
680
701
  }
681
702
 
682
703
  if (volatiles.Thrashing) {
683
- assert(volatiles.attacks > 0);
704
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
684
705
 
685
- if (options.calc.modify(player, .attacking)) |extend| {
686
- if (!extend) volatiles.attacks = 0;
687
- } else {
688
- volatiles.attacks -= 1;
689
- }
690
- 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);
691
708
 
692
- if (!showdown and handleThrashing(battle, active, player, options)) {
693
- try log.start(battle.active(player), .ConfusionSilent);
694
- }
695
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
696
- if (showdown) {
697
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
698
- // used in useMove and doesn't have the notion of skipping canMove semantics
699
- side.last_used_move = side.last_selected_move;
700
- if (handleThrashing(battle, active, player, options)) {
701
- try log.start(battle.active(player), .ConfusionSilent);
702
- }
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);
703
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
+
704
732
  return .skip_can;
705
733
  }
706
734
 
707
735
  if (volatiles.Binding) {
708
- assert(volatiles.attacks > 0);
709
-
710
- if (options.calc.modify(player, .binding)) |extend| {
711
- if (!extend) volatiles.attacks = 0;
712
- } else {
713
- volatiles.attacks -= 1;
714
- }
715
- try options.chance.binding(player, volatiles.attacks);
736
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
737
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
716
738
 
717
739
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
718
740
  if (showdown or battle.last_damage != 0) {
@@ -742,7 +764,6 @@ fn canMove(
742
764
  const player_ident = battle.active(player);
743
765
  const move = Move.get(side.last_selected_move);
744
766
 
745
- var skip = skip_pp;
746
767
  if (side.active.volatiles.Charging) {
747
768
  side.active.volatiles.Charging = false;
748
769
  side.active.volatiles.Invulnerable = false;
@@ -754,7 +775,7 @@ fn canMove(
754
775
  }
755
776
 
756
777
  side.last_used_move = side.last_selected_move;
757
- if (!skip) decrementPP(side, mslot, auto);
778
+ if (!skip_pp) decrementPP(side, mslot, auto);
758
779
 
759
780
  const target = if (move.target == .Self) player else player.foe();
760
781
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -767,9 +788,14 @@ fn canMove(
767
788
 
768
789
  if (move.effect == .Thrashing) {
769
790
  Effects.thrashing(battle, player, options);
770
- } else if (!showdown and move.effect == .Binding) {
771
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
772
- 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
+ }
773
799
  }
774
800
 
775
801
  return true;
@@ -806,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
806
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
807
833
  }
808
834
 
809
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
810
836
  var active = &side.active;
811
837
  const volatiles = &active.volatiles;
812
838
 
@@ -814,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
814
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
815
841
  if (showdown and volatiles.Transform) return;
816
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
817
846
  assert(mslot > 0 and mslot <= 4);
818
- 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;
819
848
  }
820
849
 
821
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -863,13 +892,13 @@ fn doMove(
863
892
  break :miss true;
864
893
  }
865
894
  break :miss (move.target != .Self and
866
- !moveHit(battle, player, move, &immune, &mist, options));
895
+ !try moveHit(battle, player, move, &immune, &mist, options));
867
896
  };
868
897
  assert(!immune or miss or (showdown and move.effect == .Binding));
869
898
 
870
899
  var late = showdown and move.effect != .Explode;
871
900
  const skip = status or immune;
872
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
873
902
  if (showdown and move.effect.isMulti()) {
874
903
  try Effects.multiHit(battle, player, move, options);
875
904
  hits = side.active.volatiles.attacks;
@@ -891,8 +920,9 @@ fn doMove(
891
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
892
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
893
922
  battle.last_damage = if (ohko) 65535 else 0;
894
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
895
- } 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);
896
926
  return @as(?Result, Result.Error);
897
927
  }
898
928
  if (battle.last_damage == 0) {
@@ -906,11 +936,12 @@ fn doMove(
906
936
  }
907
937
  }
908
938
 
909
- var zero = battle.last_damage == 0;
910
- miss = if (showdown or skip)
911
- miss
912
- else
913
- (!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
+ }
914
945
 
915
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
916
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -934,14 +965,35 @@ fn doMove(
934
965
  ohko = (!showdown or (!immune and !invulnerable)) and
935
966
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
936
967
  if (ohko) {
937
- try log.immune(.{ foe_ident, Immune.OHKO });
968
+ try log.immune(.{ foe_ident, .OHKO });
938
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
939
- try log.immune(.{ foe_ident, Immune.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
+ }
940
992
  } else if (mist) {
941
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
942
- try log.fail(.{ foe_ident, Fail.None });
993
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
994
+ try log.fail(.{ foe_ident, .None });
943
995
  } else {
944
- if (showdown or !zero) try options.chance.commit(player, .miss);
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
945
997
  try log.lastmiss(.{});
946
998
  try log.miss(.{battle.active(player)});
947
999
  }
@@ -956,17 +1008,15 @@ fn doMove(
956
1008
  try buildRage(battle, player.foe(), options);
957
1009
  } else if (showdown and move.effect == .Disable) {
958
1010
  try buildRage(battle, player.foe(), options);
959
- } else if (!showdown and immune and move.effect == .Binding) {
960
- try options.chance.commit(player, .binding);
961
1011
  }
962
1012
  return null;
963
- } else {
1013
+ } else if (!showdown) {
964
1014
  try options.chance.commit(player, .hit);
965
1015
  }
966
1016
 
967
1017
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
968
1018
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
969
- // it up above before damage calculation).
1019
+ // it up above before damage calculation)
970
1020
  if (!showdown and move.effect.isMulti()) {
971
1021
  try Effects.multiHit(battle, player, move, options);
972
1022
  hits = side.active.volatiles.attacks;
@@ -1005,32 +1055,30 @@ fn doMove(
1005
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1006
1056
  }
1007
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
1008
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
1009
1059
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1010
- if (move.effect == .Binding) {
1011
- try Effects.binding(battle, player, rewrap, options);
1012
- if (immune) {
1013
- battle.last_damage = 0;
1014
- assert(foe.stored().hp > 0);
1015
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1016
- if (sub) {
1017
- try log.activate(battle.active(player.foe()), .Substitute);
1018
- } else {
1019
- try log.damage(.{ battle.active(player.foe()), foe.stored(), Damage.None });
1020
- try buildRage(battle, player.foe(), options);
1021
- }
1022
- 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);
1023
1070
  }
1071
+ return null;
1024
1072
  }
1025
1073
  }
1026
1074
 
1027
1075
  // Substitute being broken nullifies the move's effect completely so even
1028
- // 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
1029
1077
  if (nullified) return null;
1030
1078
 
1031
1079
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1032
1080
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1033
- // 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
1034
1082
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1035
1083
 
1036
1084
  if (foe.stored().hp == 0) return null;
@@ -1045,7 +1093,7 @@ fn doMove(
1045
1093
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1046
1094
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1047
1095
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1048
- // puts the |-status| message before |-hitcount| instead of after.
1096
+ // puts the |-status| message before |-hitcount| instead of after
1049
1097
  if (move.effect == .Twineedle) {
1050
1098
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1051
1099
  } else if (move.effect == .Disable) {
@@ -1064,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1064
1112
  const side = battle.side(player);
1065
1113
 
1066
1114
  // Base speed is used for the critical hit calculation, even when Transform-ed
1067
- var rate = @as(u16, Species.chance(side.stored().species));
1115
+ var rate: u16 = Species.chance(side.stored().species);
1068
1116
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1069
1117
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1070
1118
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1071
1119
 
1072
- 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);
1073
1123
  }
1074
1124
 
1075
1125
  fn calcDamage(
@@ -1078,6 +1128,7 @@ fn calcDamage(
1078
1128
  target_player: Player,
1079
1129
  m: ?Move.Data,
1080
1130
  crit: bool,
1131
+ options: anytype,
1081
1132
  ) bool {
1082
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1083
1134
  const cfz = m == null;
@@ -1089,28 +1140,8 @@ fn calcDamage(
1089
1140
 
1090
1141
  const special = move.type.special();
1091
1142
 
1092
- // zig fmt: off
1093
- var atk: u32 =
1094
- if (crit)
1095
- if (special) side.stored().stats.spc
1096
- else side.stored().stats.atk
1097
- else
1098
- if (special) side.active.stats.spc
1099
- else side.active.stats.atk;
1100
- var def: u32 =
1101
- if (crit)
1102
- if (special) target.stored().stats.spc
1103
- else target.stored().stats.def
1104
- else
1105
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1106
- if (special)
1107
- target.active.stats.spc *
1108
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1109
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1110
- else
1111
- target.active.stats.def *
1112
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1113
- // zig fmt: on
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1114
1145
 
1115
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1116
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1118,12 +1149,12 @@ fn calcDamage(
1118
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1119
1150
  atk = @max((atk / 4) & 255, 1);
1120
1151
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1121
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1122
1153
  }
1123
1154
 
1124
- 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);
1125
1156
 
1126
- 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;
1127
1158
 
1128
1159
  if (def == 0) return false;
1129
1160
 
@@ -1135,11 +1166,41 @@ fn calcDamage(
1135
1166
  d = @min(997, d);
1136
1167
  d += 2;
1137
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
+
1138
1179
  battle.last_damage = @intCast(d);
1139
1180
 
1140
1181
  return true;
1141
1182
  }
1142
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
+
1143
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1144
1205
  const side = battle.side(player);
1145
1206
  const foe = battle.foe(player);
@@ -1184,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1184
1245
  const foe = battle.foe(player);
1185
1246
 
1186
1247
  if (!try checkHit(battle, player, move, options)) return null;
1187
- try options.chance.commit(player, .hit);
1188
1248
 
1189
1249
  battle.last_damage = switch (side.last_selected_move) {
1190
1250
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1195,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1195
1255
  .Psywave => power: {
1196
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1197
1257
  // GLITCH: Psywave infinite glitch loop
1198
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1199
1259
  break :power try Rolls.psywave(battle, player, max, options);
1200
1260
  },
1201
1261
  else => unreachable,
1202
1262
  };
1203
1263
 
1204
- 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;
1205
1265
 
1206
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1207
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1214,7 +1274,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1214
1274
  const foe = battle.foe(player);
1215
1275
 
1216
1276
  if (battle.last_damage == 0) {
1217
- try options.log.fail(.{ battle.active(player), Fail.None });
1277
+ try options.log.fail(.{ battle.active(player), .None });
1218
1278
  return null;
1219
1279
  }
1220
1280
 
@@ -1232,22 +1292,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1232
1292
  foe_last_selected_move.type == .Fighting);
1233
1293
 
1234
1294
  if (!used and !selected) {
1235
- try options.log.fail(.{ battle.active(player), Fail.None });
1295
+ try options.log.fail(.{ battle.active(player), .None });
1236
1296
  return null;
1237
1297
  }
1238
1298
 
1239
1299
  if (!used or !selected) {
1240
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1241
- if (!showdown) return Result.Error;
1242
- try options.log.fail(.{ battle.active(player), Fail.None });
1301
+ if (!pkmn.options.mod) return Result.Error;
1302
+ try options.log.fail(.{ battle.active(player), .None });
1243
1303
  return null;
1244
1304
  }
1245
1305
 
1246
- 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;
1247
1311
 
1248
- // Pokémon Showdown calls checkHit before Counter
1312
+ // Pokémon Showdown calls moveHit before Counter
1249
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1250
- try options.chance.commit(player, .hit);
1251
1314
 
1252
1315
  const sub = showdown and foe.active.volatiles.Substitute;
1253
1316
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1275,13 +1338,13 @@ fn applyDamage(
1275
1338
  subbed.active.volatiles.substitute = 0;
1276
1339
  subbed.active.volatiles.Substitute = false;
1277
1340
  // battle.last_damage is not updated with the amount of HP the Substitute had
1278
- try options.log.end(.{ battle.active(sub_player), End.Substitute });
1279
- options.calc.capped(target_player);
1341
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1280
1343
  return true;
1281
1344
  } else {
1282
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1283
1346
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1284
- try options.log.activate(battle.active(sub_player), .Substitute);
1347
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1285
1348
  return false;
1286
1349
  }
1287
1350
  }
@@ -1307,10 +1370,10 @@ fn mirrorMove(
1307
1370
  side.last_selected_move = foe.last_used_move;
1308
1371
 
1309
1372
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1310
- try options.log.fail(.{ battle.active(player), Fail.None });
1373
+ try options.log.fail(.{ battle.active(player), .None });
1311
1374
  return null;
1312
1375
  } else if (!showdown or foe.last_used_move != .Struggle) {
1313
- incrementPP(side, mslot);
1376
+ incrementPP(side, player, mslot, options);
1314
1377
  }
1315
1378
 
1316
1379
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1331,7 +1394,7 @@ fn metronome(
1331
1394
  var side = battle.side(player);
1332
1395
 
1333
1396
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1334
- incrementPP(side, mslot);
1397
+ incrementPP(side, player, mslot, options);
1335
1398
 
1336
1399
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1337
1400
  return null;
@@ -1343,20 +1406,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1343
1406
  var immune = false;
1344
1407
  var mist = false;
1345
1408
 
1346
- 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
+ }
1347
1413
 
1348
1414
  assert(!immune);
1349
1415
  if (mist) {
1350
1416
  assert(!showdown);
1351
1417
  const foe_ident = battle.active(player.foe());
1352
- try options.log.activate(foe_ident, .Mist);
1353
- try options.log.fail(.{ foe_ident, Fail.None });
1418
+ try options.log.activate(.{ foe_ident, .Mist });
1419
+ try options.log.fail(.{ foe_ident, .None });
1354
1420
  } else {
1355
- try options.chance.commit(player, .miss);
1356
1421
  try options.log.lastmiss(.{});
1357
1422
  try options.log.miss(.{battle.active(player)});
1358
1423
  }
1359
1424
 
1425
+ if (!showdown) try options.chance.commit(player, .miss);
1360
1426
  return false;
1361
1427
  }
1362
1428
 
@@ -1367,7 +1433,7 @@ fn moveHit(
1367
1433
  immune: *bool,
1368
1434
  mist: *bool,
1369
1435
  options: anytype,
1370
- ) bool {
1436
+ ) !bool {
1371
1437
  var side = battle.side(player);
1372
1438
  const foe = battle.foe(player);
1373
1439
 
@@ -1406,7 +1472,7 @@ fn moveHit(
1406
1472
  const overwritten = overwrite and state > 0;
1407
1473
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1408
1474
 
1409
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1475
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1410
1476
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1411
1477
  accuracy = accuracy * boost[0] / boost[1];
1412
1478
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1418,7 +1484,7 @@ fn moveHit(
1418
1484
  if (overwrite) side.active.volatiles.state = accuracy;
1419
1485
 
1420
1486
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1421
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1487
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1422
1488
  };
1423
1489
 
1424
1490
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1429,13 +1495,17 @@ fn moveHit(
1429
1495
 
1430
1496
  if (!miss) return true;
1431
1497
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1498
+
1432
1499
  side.active.volatiles.Binding = false;
1500
+ options.chance.observe(.binding, player, .None);
1501
+
1433
1502
  return false;
1434
1503
  }
1435
1504
 
1436
1505
  fn checkFaint(
1437
1506
  battle: anytype,
1438
1507
  player: Player,
1508
+ cap: bool,
1439
1509
  options: anytype,
1440
1510
  ) @TypeOf(options.log).Error!?Result {
1441
1511
  const side = battle.side(player);
@@ -1448,8 +1518,8 @@ fn checkFaint(
1448
1518
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1449
1519
  const more = player_out or foe_out;
1450
1520
 
1451
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1452
- 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;
1453
1523
 
1454
1524
  assert(!side.active.volatiles.MultiHit);
1455
1525
  assert(!foe.active.volatiles.MultiHit);
@@ -1470,7 +1540,7 @@ fn checkFaint(
1470
1540
  return .{ .p1 = foe_choice, .p2 = .Switch };
1471
1541
  }
1472
1542
 
1473
- 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 {
1474
1544
  var side = battle.side(player);
1475
1545
  var foe = battle.foe(player);
1476
1546
  assert(side.stored().hp == 0);
@@ -1479,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1479
1549
  assert(!foe_volatiles.MultiHit);
1480
1550
  if (foe_volatiles.Bide) {
1481
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1482
- 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;
1483
1553
  if (foe_volatiles.state != 0) return Result.Error;
1484
1554
  }
1485
1555
 
@@ -1504,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1504
1574
  }
1505
1575
 
1506
1576
  try options.log.faint(.{ battle.active(player), done });
1507
- options.calc.capped(player);
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);
1508
1580
  return null;
1509
1581
  }
1510
1582
 
@@ -1552,13 +1624,13 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1552
1624
  stored.hp -= amount;
1553
1625
 
1554
1626
  // As above, Pokémon Showdown uses damage [of] but its not relevant
1555
- if (amount > 0) try options.log.damage(.{ ident, stored, Damage.LeechSeed });
1627
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1556
1628
 
1557
1629
  const before = foe_stored.hp;
1558
1630
  // Uncapped damage is added back to the foe
1559
1631
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1560
1632
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1561
- if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
1633
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1562
1634
  }
1563
1635
  }
1564
1636
 
@@ -1568,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1568
1640
 
1569
1641
  battle.turn += 1;
1570
1642
 
1571
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1572
1644
  try options.log.tie(.{});
1573
1645
  return Result.Tie;
1574
1646
  }
1575
1647
 
1576
- if (showdown and battle.turn >= 1000) {
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1577
1649
  try options.log.tie(.{});
1578
1650
  return Result.Tie;
1579
1651
  } else if (battle.turn >= 65535) {
@@ -1650,7 +1722,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1650
1722
  side.last_selected_move;
1651
1723
  if (m != .None and m == last) {
1652
1724
  side.active.volatiles.Charging = false;
1653
- try options.log.cant(.{ ident, Cant.Disable, last });
1725
+ try options.log.cant(.{ ident, .Disable, last });
1654
1726
  return true;
1655
1727
  }
1656
1728
  }
@@ -1664,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1664
1736
  }
1665
1737
  }
1666
1738
 
1667
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1668
- var volatiles = &active.volatiles;
1669
- assert(volatiles.Thrashing);
1670
- if (volatiles.attacks > 0) return false;
1671
-
1672
- volatiles.Thrashing = false;
1673
- volatiles.Confusion = true;
1674
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1675
- return true;
1676
- }
1677
-
1678
1739
  fn onBegin(
1679
1740
  battle: anytype,
1680
1741
  player: Player,
@@ -1683,7 +1744,6 @@ fn onBegin(
1683
1744
  residual: *bool,
1684
1745
  options: anytype,
1685
1746
  ) !void {
1686
- assert(move.effect.onBegin());
1687
1747
  return switch (move.effect) {
1688
1748
  .Confusion => Effects.confusion(battle, player, move, options),
1689
1749
  .Conversion => Effects.conversion(battle, player, options),
@@ -1706,7 +1766,6 @@ fn onBegin(
1706
1766
  }
1707
1767
 
1708
1768
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1709
- assert(move.effect.onEnd());
1710
1769
  return switch (move.effect) {
1711
1770
  .Bide => Effects.bide(battle, player, options),
1712
1771
  // zig fmt: off
@@ -1728,7 +1787,6 @@ fn alwaysHappens(
1728
1787
  residual: *bool,
1729
1788
  options: anytype,
1730
1789
  ) !void {
1731
- assert(move.effect.alwaysHappens());
1732
1790
  return switch (move.effect) {
1733
1791
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1734
1792
  .Explode => Effects.explode(battle, player),
@@ -1764,10 +1822,12 @@ pub const Effects = struct {
1764
1822
 
1765
1823
  side.active.volatiles.Bide = true;
1766
1824
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1825
+
1767
1826
  side.active.volatiles.state = 0;
1768
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1828
+ options.chance.observe(.attacking, player, .started);
1769
1829
 
1770
- try options.log.start(battle.active(player), .Bide);
1830
+ try options.log.start(.{ battle.active(player), .Bide });
1771
1831
  }
1772
1832
 
1773
1833
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1784,7 +1844,7 @@ pub const Effects = struct {
1784
1844
  try options.log.curestatus(.{
1785
1845
  battle.active(player.foe()),
1786
1846
  foe_stored.status,
1787
- CureStatus.Message,
1847
+ .Message,
1788
1848
  });
1789
1849
  foe_stored.status = 0;
1790
1850
  }
@@ -1799,8 +1859,7 @@ pub const Effects = struct {
1799
1859
  foe_stored.status = Status.init(.BRN);
1800
1860
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1801
1861
 
1802
- const reason: protocol.Status = .None;
1803
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1862
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1804
1863
  }
1805
1864
 
1806
1865
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1822,26 +1881,22 @@ pub const Effects = struct {
1822
1881
  if (!try Rolls.confusionChance(battle, player, options)) return;
1823
1882
  } else {
1824
1883
  if (showdown) {
1825
- if (!try checkHit(battle, player, move, options)) {
1826
- return;
1827
- } else if (sub) {
1828
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1829
- }
1884
+ if (!try checkHit(battle, player, move, options)) return;
1885
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1830
1886
  } else {
1831
- if (sub) {
1832
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1833
- } else if (!try checkHit(battle, player, move, options)) {
1834
- return;
1835
- }
1887
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1888
+ if (!try checkHit(battle, player, move, options)) return;
1836
1889
  }
1837
- try options.chance.commit(player, .hit);
1838
1890
  }
1839
1891
 
1840
1892
  if (foe.active.volatiles.Confusion) return;
1841
1893
  foe.active.volatiles.Confusion = true;
1842
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1843
1894
 
1844
- 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 });
1845
1900
  }
1846
1901
 
1847
1902
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
@@ -1853,11 +1908,12 @@ pub const Effects = struct {
1853
1908
  }
1854
1909
 
1855
1910
  battle.side(player).active.types = foe.active.types;
1856
- return options.log.typechange(
1911
+ return options.log.start(.{
1857
1912
  battle.active(player),
1913
+ .TypeChange,
1858
1914
  foe.active.types,
1859
1915
  battle.active(player.foe()),
1860
- );
1916
+ });
1861
1917
  }
1862
1918
 
1863
1919
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1866,13 +1922,10 @@ pub const Effects = struct {
1866
1922
  const foe_ident = battle.active(player.foe());
1867
1923
 
1868
1924
  // Pokémon Showdown handles hit/miss earlier in doMove
1869
- if (!showdown) {
1870
- if (!try checkHit(battle, player, move, options)) return null;
1871
- try options.chance.commit(player, .hit);
1872
- }
1925
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1873
1926
 
1874
1927
  if (volatiles.disable_move != 0) {
1875
- try options.log.fail(.{ foe_ident, Fail.None });
1928
+ try options.log.fail(.{ foe_ident, .None });
1876
1929
  return null;
1877
1930
  }
1878
1931
 
@@ -1887,21 +1940,21 @@ pub const Effects = struct {
1887
1940
 
1888
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1889
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1890
- if (n == 0) {
1891
- try options.log.fail(.{ foe_ident, Fail.None });
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1944
+ try options.log.fail(.{ foe_ident, .None });
1892
1945
  return null;
1893
1946
  } else if (err) {
1894
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1895
- assert(!showdown);
1896
1948
  return Result.Error;
1897
1949
  }
1898
1950
 
1899
1951
  volatiles.disable_move =
1900
1952
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1901
1953
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1954
+ options.chance.observe(.disable, player.foe(), .started);
1902
1955
 
1903
1956
  const id = foe.active.move(volatiles.disable_move).id;
1904
- try options.log.startEffect(foe_ident, .Disable, id);
1957
+ try options.log.start(.{ foe_ident, .Disable, id });
1905
1958
  return null;
1906
1959
  }
1907
1960
 
@@ -1920,7 +1973,7 @@ pub const Effects = struct {
1920
1973
  if (stored.hp == stored.stats.hp) return;
1921
1974
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1922
1975
 
1923
- try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1976
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1924
1977
  }
1925
1978
 
1926
1979
  fn explode(battle: anytype, player: Player) !void {
@@ -1952,7 +2005,7 @@ pub const Effects = struct {
1952
2005
  if (side.active.volatiles.FocusEnergy) return;
1953
2006
  side.active.volatiles.FocusEnergy = true;
1954
2007
 
1955
- try options.log.start(battle.active(player), .FocusEnergy);
2008
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1956
2009
  }
1957
2010
 
1958
2011
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1969,12 +2022,12 @@ pub const Effects = struct {
1969
2022
  if (foe.active.types.includes(move.type)) return;
1970
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1971
2024
  // Freeze Clause Mod
1972
- 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;
1973
2026
 
1974
2027
  foe_stored.status = Status.init(.FRZ);
1975
2028
  // GLITCH: Hyper Beam recharging status is not cleared
1976
2029
 
1977
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2030
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1978
2031
  }
1979
2032
 
1980
2033
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1982,7 +2035,7 @@ pub const Effects = struct {
1982
2035
  var side = battle.side(player);
1983
2036
  var foe = battle.foe(player);
1984
2037
 
1985
- var side_stored = side.stored();
2038
+ const side_stored = side.stored();
1986
2039
  var foe_stored = foe.stored();
1987
2040
 
1988
2041
  const player_ident = battle.active(player);
@@ -1994,7 +2047,7 @@ pub const Effects = struct {
1994
2047
  side.active.stats = side_stored.stats;
1995
2048
  foe.active.stats = foe_stored.stats;
1996
2049
 
1997
- try log.activate(player_ident, .Haze);
2050
+ try log.activate(.{ player_ident, .Haze });
1998
2051
  try log.clearallboost(.{});
1999
2052
 
2000
2053
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
@@ -2003,24 +2056,26 @@ pub const Effects = struct {
2003
2056
  const p: Player = @enumFromInt(i);
2004
2057
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2005
2058
  if (p != player and Status.any(s.stored().status)) {
2006
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2059
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2007
2060
  s.stored().status = 0;
2061
+ options.chance.observe(.sleep, p, .None);
2008
2062
  } else if (showdown and s.stored().status == Status.TOX) {
2009
2063
  s.stored().status = Status.init(.PSN);
2010
- try log.status(.{ battle.active(p), s.stored().status, protocol.Status.None });
2064
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2011
2065
  }
2012
- try clearVolatiles(battle, p, p != player, options);
2066
+ try clearVolatiles(battle, p, options);
2013
2067
  }
2014
2068
  } else {
2015
2069
  if (Status.any(foe_stored.status)) {
2016
2070
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2017
2071
  foe.last_selected_move = .SKIP_TURN;
2072
+ options.chance.observe(.sleep, player.foe(), .None);
2018
2073
  }
2019
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2074
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2020
2075
  foe_stored.status = 0;
2021
2076
  }
2022
- try clearVolatiles(battle, player, false, options);
2023
- try clearVolatiles(battle, player.foe(), true, options);
2077
+ try clearVolatiles(battle, player, options);
2078
+ try clearVolatiles(battle, player.foe(), options);
2024
2079
  }
2025
2080
  }
2026
2081
 
@@ -2032,7 +2087,7 @@ pub const Effects = struct {
2032
2087
  // GLITCH: HP recovery move failure glitches
2033
2088
  const delta = stored.stats.hp - stored.hp;
2034
2089
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2035
- return options.log.fail(.{ ident, Fail.None });
2090
+ return options.log.fail(.{ ident, .None });
2036
2091
  }
2037
2092
 
2038
2093
  const rest = side.last_selected_move == .Rest;
@@ -2040,7 +2095,7 @@ pub const Effects = struct {
2040
2095
  // Adding the sleep status runs the sleep condition handler to roll duration
2041
2096
  if (showdown) battle.rng.advance(1);
2042
2097
  stored.status = Status.slf(2);
2043
- try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
2098
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2044
2099
  stored.hp = stored.stats.hp;
2045
2100
  } else {
2046
2101
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -2059,7 +2114,7 @@ pub const Effects = struct {
2059
2114
  if (showdown) {
2060
2115
  // Invulnerability trumps type immunity on Pokémon Showdown
2061
2116
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2062
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
2117
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2063
2118
  }
2064
2119
  if (!try checkHit(battle, player, move, options)) return;
2065
2120
  if (foe.active.volatiles.LeechSeed) return;
@@ -2071,21 +2126,19 @@ pub const Effects = struct {
2071
2126
  }
2072
2127
  }
2073
2128
 
2074
- try options.chance.commit(player, .hit);
2075
2129
  foe.active.volatiles.LeechSeed = true;
2076
2130
 
2077
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2131
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2078
2132
  }
2079
2133
 
2080
2134
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2081
2135
  var side = battle.side(player);
2136
+ const ident = battle.active(player);
2082
2137
 
2083
- if (side.active.volatiles.LightScreen) {
2084
- return options.log.fail(.{ battle.active(player), Fail.None });
2085
- }
2138
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2086
2139
  side.active.volatiles.LightScreen = true;
2087
2140
 
2088
- try options.log.start(battle.active(player), .LightScreen);
2141
+ try options.log.start(.{ ident, .LightScreen });
2089
2142
  }
2090
2143
 
2091
2144
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2113,17 +2166,15 @@ pub const Effects = struct {
2113
2166
  if (!has_mimic) {
2114
2167
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2115
2168
  if (!try checkHit(battle, player, move, options)) return;
2116
- try options.chance.commit(player, .hit);
2117
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2169
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2118
2170
  }
2119
2171
  }
2120
2172
  if (!try checkHit(battle, player, move, options)) return;
2121
- try options.chance.commit(player, .hit);
2122
2173
 
2123
2174
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2124
2175
  side.active.move(oslot).id = foe.active.move(rslot).id;
2125
2176
 
2126
- 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 });
2127
2178
  }
2128
2179
 
2129
2180
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2132,7 +2183,7 @@ pub const Effects = struct {
2132
2183
  if (side.active.volatiles.Mist) return;
2133
2184
  side.active.volatiles.Mist = true;
2134
2185
 
2135
- try options.log.start(battle.active(player), .Mist);
2186
+ try options.log.start(.{ battle.active(player), .Mist });
2136
2187
  }
2137
2188
 
2138
2189
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2159,7 +2210,7 @@ pub const Effects = struct {
2159
2210
  if (showdown) {
2160
2211
  // Invulnerability trumps type immunity on Pokémon Showdown
2161
2212
  if (immune and !foe.active.volatiles.Invulnerable) {
2162
- return log.immune(.{ foe_ident, Immune.None });
2213
+ return log.immune(.{ foe_ident, .None });
2163
2214
  } else if (!try checkHit(battle, player, move, options)) return;
2164
2215
  }
2165
2216
  if (Status.any(foe_stored.status)) {
@@ -2169,15 +2220,14 @@ pub const Effects = struct {
2169
2220
  });
2170
2221
  }
2171
2222
  if (!showdown) {
2172
- if (immune) return log.immune(.{ foe_ident, Immune.None });
2223
+ if (immune) return log.immune(.{ foe_ident, .None });
2173
2224
  if (!try checkHit(battle, player, move, options)) return;
2174
2225
  }
2175
- try options.chance.commit(player, .hit);
2176
2226
 
2177
2227
  foe_stored.status = Status.init(.PAR);
2178
2228
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2179
2229
 
2180
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2230
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2181
2231
  }
2182
2232
 
2183
2233
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2199,8 +2249,7 @@ pub const Effects = struct {
2199
2249
  foe_stored.status = Status.init(.PAR);
2200
2250
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2201
2251
 
2202
- const reason: protocol.Status = .None;
2203
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2252
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2204
2253
  }
2205
2254
 
2206
2255
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
@@ -2220,7 +2269,7 @@ pub const Effects = struct {
2220
2269
  return;
2221
2270
  } else if (foe.active.volatiles.Substitute) {
2222
2271
  if (move.effect != .Poison) return;
2223
- return log.fail(.{ foe_ident, Fail.None });
2272
+ return log.fail(.{ foe_ident, .None });
2224
2273
  } else if (Status.any(foe_stored.status)) {
2225
2274
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2226
2275
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
@@ -2236,12 +2285,11 @@ pub const Effects = struct {
2236
2285
  Fail.None });
2237
2286
  } else if (foe.active.types.includes(.Poison)) {
2238
2287
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2239
- return log.immune(.{ foe_ident, Immune.None });
2288
+ return log.immune(.{ foe_ident, .None });
2240
2289
  }
2241
2290
 
2242
2291
  if (move.effect == .Poison) {
2243
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2244
- try options.chance.commit(player, .hit);
2245
2293
  } else {
2246
2294
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2247
2295
  return;
@@ -2255,7 +2303,7 @@ pub const Effects = struct {
2255
2303
  foe.active.volatiles.toxic = 0;
2256
2304
  }
2257
2305
 
2258
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2306
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2259
2307
  }
2260
2308
 
2261
2309
  fn rage(battle: anytype, player: Player) !void {
@@ -2278,7 +2326,7 @@ pub const Effects = struct {
2278
2326
  try options.log.damage(.{
2279
2327
  battle.active(player),
2280
2328
  stored,
2281
- Damage.RecoilOf,
2329
+ .RecoilOf,
2282
2330
  battle.active(player.foe()),
2283
2331
  });
2284
2332
  if (showdown and stored.hp == 0) residual.* = false;
@@ -2286,13 +2334,12 @@ pub const Effects = struct {
2286
2334
 
2287
2335
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2288
2336
  var side = battle.side(player);
2337
+ const ident = battle.active(player);
2289
2338
 
2290
- if (side.active.volatiles.Reflect) {
2291
- return options.log.fail(.{ battle.active(player), Fail.None });
2292
- }
2339
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2293
2340
  side.active.volatiles.Reflect = true;
2294
2341
 
2295
- try options.log.start(battle.active(player), .Reflect);
2342
+ try options.log.start(.{ ident, .Reflect });
2296
2343
  }
2297
2344
 
2298
2345
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2311,13 +2358,12 @@ pub const Effects = struct {
2311
2358
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2312
2359
  });
2313
2360
  }
2314
- // 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
2315
2362
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2316
- try options.chance.commit(player, .hit);
2317
2363
  }
2318
2364
 
2319
2365
  // Sleep Clause Mod
2320
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2321
2367
  for (foe.pokemon) |p| {
2322
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2323
2369
  }
@@ -2325,18 +2371,20 @@ pub const Effects = struct {
2325
2371
  foe.active.volatiles.Recharging = false;
2326
2372
 
2327
2373
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2374
+ options.chance.observe(.sleep, player.foe(), .started);
2375
+
2328
2376
  const last = battle.side(player).last_selected_move;
2329
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.From, last });
2377
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2330
2378
  }
2331
2379
 
2332
2380
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2333
- try options.log.activate(battle.active(player), .Splash);
2381
+ try options.log.activate(.{ battle.active(player), .Splash });
2334
2382
  }
2335
2383
 
2336
2384
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2337
2385
  var side = battle.side(player);
2338
2386
  if (side.active.volatiles.Substitute) {
2339
- return options.log.fail(.{ battle.active(player), Fail.Substitute });
2387
+ return options.log.fail(.{ battle.active(player), .Substitute });
2340
2388
  }
2341
2389
 
2342
2390
  assert(side.stored().stats.hp <= 1023);
@@ -2348,16 +2396,16 @@ pub const Effects = struct {
2348
2396
  // avoid having to convert to floating point)
2349
2397
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2350
2398
  if (side.stored().hp < required_hp) {
2351
- return options.log.fail(.{ battle.active(player), Fail.Weak });
2399
+ return options.log.fail(.{ battle.active(player), .Weak });
2352
2400
  }
2353
2401
 
2354
2402
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2355
2403
  side.stored().hp -= hp;
2356
2404
  side.active.volatiles.substitute = hp + 1;
2357
2405
  side.active.volatiles.Substitute = true;
2358
- try options.log.start(battle.active(player), .Substitute);
2406
+ try options.log.start(.{ battle.active(player), .Substitute });
2359
2407
  if (hp > 0) {
2360
- try options.log.damage(.{ battle.active(player), side.stored(), Damage.None });
2408
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2361
2409
  if (showdown and side.stored().hp == 0) residual.* = false;
2362
2410
  }
2363
2411
  }
@@ -2366,12 +2414,10 @@ pub const Effects = struct {
2366
2414
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2367
2415
  if (showdown) {
2368
2416
  if (battle.side(player).last_selected_move == .Teleport) return;
2369
- if (try checkHit(battle, player, move, options)) {
2370
- try options.chance.commit(player, .hit);
2371
- }
2417
+ _ = try checkHit(battle, player, move, options);
2372
2418
  battle.last_damage = 0;
2373
2419
  } else {
2374
- try options.log.fail(.{ battle.active(player), Fail.None });
2420
+ try options.log.fail(.{ battle.active(player), .None });
2375
2421
  try options.log.laststill(.{});
2376
2422
  }
2377
2423
  }
@@ -2383,6 +2429,8 @@ pub const Effects = struct {
2383
2429
 
2384
2430
  volatiles.Thrashing = true;
2385
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2432
+
2433
+ options.chance.observe(.attacking, player, .started);
2386
2434
  }
2387
2435
 
2388
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2421,13 +2469,14 @@ pub const Effects = struct {
2421
2469
  if (side.active.volatiles.Binding) return;
2422
2470
  side.active.volatiles.Binding = true;
2423
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2424
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2425
2473
  if (!showdown) {
2426
2474
  foe.active.volatiles.Recharging = false;
2427
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2428
2476
 
2429
2477
  side.active.volatiles.attacks =
2430
2478
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2479
+ options.chance.observe(.binding, player, .started);
2431
2480
  }
2432
2481
 
2433
2482
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,20 +2490,20 @@ pub const Effects = struct {
2441
2490
  switch (move.effect) {
2442
2491
  .AttackUp1, .AttackUp2, .Rage => {
2443
2492
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2444
- if (boosts.atk == 6) return log.fail(.{ ident, Fail.None });
2493
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2445
2494
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2446
2495
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2447
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2496
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2448
2497
  if (stats.atk == MAX_STAT_VALUE) {
2449
2498
  boosts.atk -= 1;
2450
2499
  if (showdown) {
2451
2500
  try log.boost(.{ ident, reason, n });
2452
- try log.boost(.{ ident, Boost.Attack, -1 });
2501
+ try log.boost(.{ ident, .Attack, -1 });
2453
2502
  if (move.effect == .Rage) return;
2454
2503
  }
2455
- return log.fail(.{ ident, Fail.None });
2504
+ return log.fail(.{ ident, .None });
2456
2505
  }
2457
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2506
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2458
2507
  const stat = unmodifiedStats(battle, side).atk;
2459
2508
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2460
2509
  try log.boost(.{ ident, reason, n });
@@ -2463,65 +2512,65 @@ pub const Effects = struct {
2463
2512
  },
2464
2513
  .DefenseUp1, .DefenseUp2 => {
2465
2514
  assert(boosts.def >= -6 and boosts.def <= 6);
2466
- if (boosts.def == 6) return log.fail(.{ ident, Fail.None });
2515
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2467
2516
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2468
2517
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2469
2518
  if (stats.def == MAX_STAT_VALUE) {
2470
2519
  boosts.def -= 1;
2471
2520
  if (showdown) {
2472
- try log.boost(.{ ident, Boost.Defense, n });
2473
- try log.boost(.{ ident, Boost.Defense, -1 });
2521
+ try log.boost(.{ ident, .Defense, n });
2522
+ try log.boost(.{ ident, .Defense, -1 });
2474
2523
  }
2475
- return log.fail(.{ ident, Fail.None });
2524
+ return log.fail(.{ ident, .None });
2476
2525
  }
2477
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2526
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2478
2527
  const stat = unmodifiedStats(battle, side).def;
2479
2528
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2480
- try log.boost(.{ ident, Boost.Defense, n });
2529
+ try log.boost(.{ ident, .Defense, n });
2481
2530
  },
2482
2531
  .SpeedUp2 => {
2483
2532
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2484
- if (boosts.spe == 6) return log.fail(.{ ident, Fail.None });
2533
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2485
2534
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2486
2535
  if (stats.spe == MAX_STAT_VALUE) {
2487
2536
  boosts.spe -= 1;
2488
2537
  if (showdown) {
2489
- try log.boost(.{ ident, Boost.Speed, 2 });
2490
- try log.boost(.{ ident, Boost.Speed, -1 });
2538
+ try log.boost(.{ ident, .Speed, 2 });
2539
+ try log.boost(.{ ident, .Speed, -1 });
2491
2540
  }
2492
- return log.fail(.{ ident, Fail.None });
2541
+ return log.fail(.{ ident, .None });
2493
2542
  }
2494
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2543
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2495
2544
  const stat = unmodifiedStats(battle, side).spe;
2496
2545
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2497
- try log.boost(.{ ident, Boost.Speed, 2 });
2546
+ try log.boost(.{ ident, .Speed, 2 });
2498
2547
  },
2499
2548
  .SpecialUp1, .SpecialUp2 => {
2500
2549
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2501
- if (boosts.spc == 6) return log.fail(.{ ident, Fail.None });
2550
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2502
2551
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2503
2552
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2504
2553
  if (stats.spc == MAX_STAT_VALUE) {
2505
2554
  boosts.spc -= 1;
2506
2555
  if (showdown) {
2507
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2508
- try log.boost(.{ ident, Boost.SpecialAttack, -1 });
2509
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2510
- try log.boost(.{ ident, Boost.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 });
2511
2560
  }
2512
- return log.fail(.{ ident, Fail.None });
2561
+ return log.fail(.{ ident, .None });
2513
2562
  }
2514
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2563
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2515
2564
  const stat = unmodifiedStats(battle, side).spc;
2516
2565
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2517
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2518
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2566
+ try log.boost(.{ ident, .SpecialAttack, n });
2567
+ try log.boost(.{ ident, .SpecialDefense, n });
2519
2568
  },
2520
2569
  .EvasionUp1 => {
2521
2570
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2522
- if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2571
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2523
2572
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2524
- try log.boost(.{ ident, Boost.Evasion, 1 });
2573
+ try log.boost(.{ ident, .Evasion, 1 });
2525
2574
  },
2526
2575
  else => unreachable,
2527
2576
  }
@@ -2536,9 +2585,7 @@ pub const Effects = struct {
2536
2585
  const foe_ident = battle.active(player.foe());
2537
2586
  const secondary = move.effect.isStatDownChance();
2538
2587
 
2539
- if (foe.active.volatiles.Substitute) {
2540
- return if (!secondary) log.fail(.{ foe_ident, Fail.None });
2541
- }
2588
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2542
2589
 
2543
2590
  if (secondary) {
2544
2591
  const proc = try Rolls.unboost(battle, player, options);
@@ -2546,7 +2593,6 @@ pub const Effects = struct {
2546
2593
  } else {
2547
2594
  // checkHit already checks for Invulnerable
2548
2595
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2549
- try options.chance.commit(player, .hit);
2550
2596
  }
2551
2597
 
2552
2598
  var stats = &foe.active.stats;
@@ -2556,82 +2602,82 @@ pub const Effects = struct {
2556
2602
  switch (move.effect) {
2557
2603
  .AttackDown1, .AttackDownChance => {
2558
2604
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2559
- if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2605
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2560
2606
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2561
2607
  if (stats.atk == 1) {
2562
2608
  boosts.atk += 1;
2563
2609
  if (showdown) {
2564
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2565
- try log.boost(.{ foe_ident, Boost.Attack, 1 });
2610
+ try log.boost(.{ foe_ident, .Attack, -1 });
2611
+ try log.boost(.{ foe_ident, .Attack, 1 });
2566
2612
  }
2567
- return log.fail(.{ foe_ident, Fail.None });
2613
+ return log.fail(.{ foe_ident, .None });
2568
2614
  }
2569
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2615
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2570
2616
  const stat = unmodifiedStats(battle, foe).atk;
2571
2617
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2572
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2618
+ try log.boost(.{ foe_ident, .Attack, -1 });
2573
2619
  },
2574
2620
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2575
2621
  assert(boosts.def >= -6 and boosts.def <= 6);
2576
- if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2622
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2577
2623
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2578
2624
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2579
2625
  if (stats.def == 1) {
2580
2626
  boosts.def += 1;
2581
2627
  if (showdown) {
2582
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2583
- try log.boost(.{ foe_ident, Boost.Defense, 1 });
2628
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2629
+ try log.boost(.{ foe_ident, .Defense, 1 });
2584
2630
  }
2585
- return log.fail(.{ foe_ident, Fail.None });
2631
+ return log.fail(.{ foe_ident, .None });
2586
2632
  }
2587
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2633
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2588
2634
  const stat = unmodifiedStats(battle, foe).def;
2589
2635
  stats.def = @max(1, stat * mod[0] / mod[1]);
2590
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2636
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2591
2637
  },
2592
2638
  .SpeedDown1, .SpeedDownChance => {
2593
2639
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2594
- if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2640
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2595
2641
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2596
2642
  if (stats.spe == 1) {
2597
2643
  boosts.spe += 1;
2598
2644
  if (showdown) {
2599
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2600
- try log.boost(.{ foe_ident, Boost.Speed, 1 });
2645
+ try log.boost(.{ foe_ident, .Speed, -1 });
2646
+ try log.boost(.{ foe_ident, .Speed, 1 });
2601
2647
  }
2602
- return log.fail(.{ foe_ident, Fail.None });
2648
+ return log.fail(.{ foe_ident, .None });
2603
2649
  }
2604
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2650
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2605
2651
  const stat = unmodifiedStats(battle, foe).spe;
2606
2652
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2607
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2653
+ try log.boost(.{ foe_ident, .Speed, -1 });
2608
2654
  assert(boosts.spe >= -6);
2609
2655
  },
2610
2656
  .SpecialDownChance => {
2611
2657
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2612
- if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2658
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2613
2659
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2614
2660
  if (stats.spc == 1) {
2615
2661
  boosts.spc += 1;
2616
2662
  if (showdown) {
2617
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2618
- try log.boost(.{ foe_ident, Boost.SpecialAttack, 1 });
2619
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2620
- try log.boost(.{ foe_ident, Boost.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 });
2621
2667
  }
2622
- return log.fail(.{ foe_ident, Fail.None });
2668
+ return log.fail(.{ foe_ident, .None });
2623
2669
  }
2624
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2670
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2625
2671
  const stat = unmodifiedStats(battle, foe).spc;
2626
2672
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2627
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2628
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2673
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2674
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2629
2675
  },
2630
2676
  .AccuracyDown1 => {
2631
2677
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2632
- if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2678
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2633
2679
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2634
- try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2680
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2635
2681
  },
2636
2682
  else => unreachable,
2637
2683
  }
@@ -2660,35 +2706,35 @@ fn isForced(active: anytype) bool {
2660
2706
  active.volatiles.Thrashing or active.volatiles.Charging;
2661
2707
  }
2662
2708
 
2663
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2709
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2664
2710
  var log = options.log;
2665
2711
  var side = battle.side(who);
2666
2712
  var volatiles = &side.active.volatiles;
2667
2713
  const ident = battle.active(who);
2668
2714
 
2669
- options.chance.clearDurations(who, clear);
2670
-
2671
2715
  if (volatiles.disable_move != 0) {
2672
2716
  volatiles.disable_move = 0;
2673
2717
  volatiles.disable_duration = 0;
2674
- try log.end(.{ ident, End.DisableSilent });
2718
+ options.chance.observe(.disable, who, .None);
2719
+ try log.end(.{ ident, .DisableSilent });
2675
2720
  }
2676
2721
  if (volatiles.Confusion) {
2677
2722
  // volatiles.confusion is left unchanged
2678
2723
  volatiles.Confusion = false;
2679
- try log.end(.{ ident, End.ConfusionSilent });
2724
+ options.chance.observe(.confusion, who, .None);
2725
+ try log.end(.{ ident, .ConfusionSilent });
2680
2726
  }
2681
2727
  if (volatiles.Mist) {
2682
2728
  volatiles.Mist = false;
2683
- try log.end(.{ ident, End.MistSilent });
2729
+ try log.end(.{ ident, .MistSilent });
2684
2730
  }
2685
2731
  if (volatiles.FocusEnergy) {
2686
2732
  volatiles.FocusEnergy = false;
2687
- try log.end(.{ ident, End.FocusEnergySilent });
2733
+ try log.end(.{ ident, .FocusEnergySilent });
2688
2734
  }
2689
2735
  if (volatiles.LeechSeed) {
2690
2736
  volatiles.LeechSeed = false;
2691
- try log.end(.{ ident, End.LeechSeedSilent });
2737
+ try log.end(.{ ident, .LeechSeedSilent });
2692
2738
  }
2693
2739
  if (!showdown and volatiles.Toxic) {
2694
2740
  volatiles.Toxic = false;
@@ -2696,24 +2742,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2696
2742
  }
2697
2743
  if (volatiles.LightScreen) {
2698
2744
  volatiles.LightScreen = false;
2699
- try log.end(.{ ident, End.LightScreenSilent });
2745
+ try log.end(.{ ident, .LightScreenSilent });
2700
2746
  }
2701
2747
  if (volatiles.Reflect) {
2702
2748
  volatiles.Reflect = false;
2703
- try log.end(.{ ident, End.ReflectSilent });
2749
+ try log.end(.{ ident, .ReflectSilent });
2704
2750
  }
2705
2751
  if (showdown and volatiles.Toxic) {
2706
2752
  volatiles.Toxic = false;
2707
2753
  // Pokémon Showdown erroneously clears the toxic counter
2708
2754
  volatiles.toxic = 0;
2709
- try log.end(.{ ident, End.ToxicSilent });
2755
+ try log.end(.{ ident, .ToxicSilent });
2710
2756
  }
2711
2757
  }
2712
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
+
2713
2772
  pub const Rolls = struct {
2714
2773
  fn speedTie(battle: anytype, options: anytype) !bool {
2715
2774
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2716
2775
  player == .P1
2776
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2777
+ battle.rng.speedTie()
2717
2778
  else if (showdown)
2718
2779
  battle.rng.range(u8, 0, 2) == 0
2719
2780
  else
@@ -2723,21 +2784,31 @@ pub const Rolls = struct {
2723
2784
  return p1;
2724
2785
  }
2725
2786
 
2726
- 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 {
2727
2794
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2728
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2797
+ battle.rng.criticalHit(player, rate)
2729
2798
  else if (showdown)
2730
2799
  battle.rng.chance(u8, rate, 256)
2731
2800
  else
2732
2801
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2733
2802
 
2734
- try options.chance.criticalHit(player, crit, rate);
2803
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2735
2804
  return crit;
2736
2805
  }
2737
2806
 
2738
2807
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2739
2808
  const roll = if (options.calc.overridden(player, .damage)) |val|
2740
2809
  val
2810
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2811
+ battle.rng.damage(player)
2741
2812
  else roll: {
2742
2813
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2743
2814
  while (true) {
@@ -2751,21 +2822,27 @@ pub const Rolls = struct {
2751
2822
  return roll;
2752
2823
  }
2753
2824
 
2754
- 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;
2755
2828
  const ok = if (options.calc.overridden(player, .hit)) |val|
2756
2829
  val == .true
2830
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2831
+ battle.rng.hit(player, acc)
2757
2832
  else if (showdown)
2758
- battle.rng.chance(u8, accuracy, 256)
2833
+ battle.rng.chance(uN, acc, 256)
2759
2834
  else
2760
- battle.rng.next() < accuracy;
2835
+ battle.rng.next() < acc;
2761
2836
 
2762
- options.chance.hit(ok, accuracy);
2837
+ try options.chance.hit(player, ok, acc);
2763
2838
  return ok;
2764
2839
  }
2765
2840
 
2766
2841
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2767
2842
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2768
2843
  val == .true
2844
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2845
+ battle.rng.confused(player)
2769
2846
  else if (showdown)
2770
2847
  !battle.rng.chance(u8, 128, 256)
2771
2848
  else
@@ -2778,6 +2855,8 @@ pub const Rolls = struct {
2778
2855
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2779
2856
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2780
2857
  val == .true
2858
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2859
+ battle.rng.paralyzed(player)
2781
2860
  else if (showdown)
2782
2861
  battle.rng.chance(u8, 63, 256)
2783
2862
  else
@@ -2790,6 +2869,8 @@ pub const Rolls = struct {
2790
2869
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2791
2870
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2792
2871
  val == .true
2872
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2873
+ battle.rng.secondaryChance(player, 25)
2793
2874
  else if (showdown)
2794
2875
  battle.rng.chance(u8, 25, 256)
2795
2876
  else
@@ -2804,6 +2885,8 @@ pub const Rolls = struct {
2804
2885
 
2805
2886
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2806
2887
  val == .true
2888
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2889
+ battle.rng.secondaryChance(player, rate)
2807
2890
  else if (showdown)
2808
2891
  battle.rng.chance(u8, rate, 256)
2809
2892
  else
@@ -2818,6 +2901,8 @@ pub const Rolls = struct {
2818
2901
 
2819
2902
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2820
2903
  val == .true
2904
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2905
+ battle.rng.secondaryChance(player, rate)
2821
2906
  else if (showdown)
2822
2907
  battle.rng.chance(u8, rate, 256)
2823
2908
  else
@@ -2830,6 +2915,8 @@ pub const Rolls = struct {
2830
2915
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2831
2916
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2832
2917
  val == .true
2918
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2919
+ battle.rng.secondaryChance(player, 85)
2833
2920
  else if (showdown)
2834
2921
  battle.rng.chance(u8, 85, 256)
2835
2922
  else
@@ -2842,6 +2929,8 @@ pub const Rolls = struct {
2842
2929
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2843
2930
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2844
2931
  val
2932
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2933
+ battle.rng.metronome(player)
2845
2934
  else if (showdown)
2846
2935
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2847
2936
  else move: {
@@ -2860,6 +2949,8 @@ pub const Rolls = struct {
2860
2949
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2861
2950
  const power = if (options.calc.overridden(player, .psywave)) |val|
2862
2951
  val - 1
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2953
+ battle.rng.psywave(player)
2863
2954
  else if (showdown)
2864
2955
  battle.rng.range(u8, 0, max)
2865
2956
  else power: {
@@ -2875,8 +2966,11 @@ pub const Rolls = struct {
2875
2966
  }
2876
2967
 
2877
2968
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2878
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2879
- @intCast(val)
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)
2880
2974
  else if (showdown)
2881
2975
  battle.rng.range(u3, 1, 8)
2882
2976
  else duration: {
@@ -2887,46 +2981,55 @@ pub const Rolls = struct {
2887
2981
  };
2888
2982
 
2889
2983
  assert(duration >= 1 and duration <= 7);
2890
- options.chance.duration(.sleep, player, player.foe(), duration);
2984
+ options.chance.duration(player, duration);
2891
2985
  return duration;
2892
2986
  }
2893
2987
 
2894
2988
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2896
- val
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)
2897
2994
  else if (showdown)
2898
2995
  battle.rng.range(u4, 1, 9)
2899
2996
  else
2900
2997
  @intCast((battle.rng.next() & 7) + 1);
2901
2998
 
2902
2999
  assert(duration >= 1 and duration <= 8);
2903
- options.chance.duration(.disable, player, player.foe(), duration);
3000
+ options.chance.duration(player, duration);
2904
3001
  return duration;
2905
3002
  }
2906
3003
 
2907
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2908
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2909
- @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)
2910
3010
  else if (showdown)
2911
3011
  battle.rng.range(u3, 2, 6)
2912
3012
  else
2913
3013
  @intCast((battle.rng.next() & 3) + 2);
2914
3014
 
2915
3015
  assert(duration >= 2 and duration <= 5);
2916
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3016
+ options.chance.duration(player, duration);
2917
3017
  return duration;
2918
3018
  }
2919
3019
 
2920
3020
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2921
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2922
- @intCast(val)
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)
2923
3026
  else if (showdown)
2924
3027
  battle.rng.range(u3, 2, 4)
2925
3028
  else
2926
3029
  @intCast((battle.rng.next() & 1) + 2);
2927
3030
 
2928
3031
  assert(duration >= 2 and duration <= 3);
2929
- options.chance.duration(.attacking, player, player, duration);
3032
+ options.chance.duration(player, duration);
2930
3033
  return duration;
2931
3034
  }
2932
3035
 
@@ -2939,8 +3042,11 @@ pub const Rolls = struct {
2939
3042
  options: anytype,
2940
3043
  ) !u3 {
2941
3044
  const roll = if (effect == .Binding) .duration else .multi_hit;
2942
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2943
- @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)
2944
3050
  else if (showdown)
2945
3051
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2946
3052
  else n: {
@@ -2950,7 +3056,7 @@ pub const Rolls = struct {
2950
3056
 
2951
3057
  assert(n >= 2 and n <= 5);
2952
3058
  if (effect == .Binding) {
2953
- options.chance.duration(.binding, player, player, n);
3059
+ options.chance.duration(player, n);
2954
3060
  } else {
2955
3061
  try options.chance.multiHit(player, n);
2956
3062
  }
@@ -2965,15 +3071,15 @@ pub const Rolls = struct {
2965
3071
  options: anytype,
2966
3072
  ) !u4 {
2967
3073
  // TODO: consider throwing error instead of rerolling?
2968
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2969
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2970
3076
  val
2971
3077
  else
2972
3078
  null
2973
3079
  else
2974
- null;
2975
-
2976
- 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: {
2977
3083
  if (showdown) {
2978
3084
  if (check_pp == 0) {
2979
3085
  var i: usize = moves.len;
@@ -3053,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3053
3159
  const side = battle.side(player);
3054
3160
  const foe = battle.foe(player);
3055
3161
 
3056
- var active = &side.active;
3162
+ const active = &side.active;
3057
3163
  const stored = side.stored();
3058
3164
 
3059
3165
  // While players are not given any input options on the cartridge in these cases,
3060
- // 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
3061
3167
  //
3062
3168
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3063
3169
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3065,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3065
3171
  // passing due to being forced into using a move. Instead of introducing another option
3066
3172
  // we simply repurpose Move with no move slot, even though pedantically this is not
3067
3173
  // strictly correct as the player would not have been presented the option to move or
3068
- // switch at all.
3174
+ // switch at all
3069
3175
  if (isForced(active)) {
3070
3176
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3071
3177
  n += 1;
@@ -3084,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3084
3190
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3085
3191
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3086
3192
  // either disables all other moves in the case of limited or requires you to select a
3087
- // move normally if sleeping/frozen/bound.
3193
+ // move normally if sleeping/frozen/bound
3088
3194
  if (!showdown and (limited or foe.active.volatiles.Binding or
3089
3195
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3090
3196
  {
@@ -3097,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3097
3203
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3098
3204
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3099
3205
  // Move will not result in forcing the subsequent use unless the user also had the
3100
- // proc-ed move in their moveset) and disabling all other moves.
3206
+ // proc-ed move in their moveset) and disabling all other moves
3101
3207
  if (limited) {
3102
3208
  assert(showdown);
3103
3209
  assert(side.last_selected_move != .None);
@@ -3129,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3129
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3130
3236
  if (n == before) {
3131
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3132
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3133
3239
  while (slot <= 4) : (slot += 1) {
3134
3240
  if (active.moves[slot - 1].pp != 0) return n;
3135
3241
  }