@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.683fba11

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 (76) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +44 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +97 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +44 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -3
  12. package/build/pkg/addon.js +34 -65
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/data.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +13 -17
  19. package/build/pkg/gen1.js +6 -10
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +38 -90
  22. package/build/pkg/index.js +6 -63
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +35 -68
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +141 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +141 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +349 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +18 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +254 -77
  38. package/package.json +52 -31
  39. package/src/bin/install-pkmn-engine +8 -6
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/common/array.zig +77 -0
  42. package/src/lib/common/data.zig +8 -8
  43. package/src/lib/common/debug.zig +39 -8
  44. package/src/lib/common/js.zig +4 -2
  45. package/src/lib/common/optional.zig +13 -9
  46. package/src/lib/common/options.zig +4 -0
  47. package/src/lib/common/protocol.zig +45 -44
  48. package/src/lib/common/rational.zig +53 -51
  49. package/src/lib/common/rng.zig +5 -7
  50. package/src/lib/common/util.zig +50 -1
  51. package/src/lib/gen1/calc.zig +509 -152
  52. package/src/lib/gen1/chance.zig +366 -240
  53. package/src/lib/gen1/data/moves.zig +2 -4
  54. package/src/lib/gen1/data/species.zig +1 -3
  55. package/src/lib/gen1/data/types.zig +1 -2
  56. package/src/lib/gen1/data.zig +35 -38
  57. package/src/lib/gen1/helpers.zig +17 -554
  58. package/src/lib/gen1/mechanics.zig +280 -185
  59. package/src/lib/gen2/calc.zig +37 -27
  60. package/src/lib/gen2/chance.zig +90 -503
  61. package/src/lib/gen2/data/items.zig +1 -3
  62. package/src/lib/gen2/data/moves.zig +2 -4
  63. package/src/lib/gen2/data/species.zig +1 -3
  64. package/src/lib/gen2/data/types.zig +1 -2
  65. package/src/lib/gen2/data.zig +26 -29
  66. package/src/lib/gen2/generated.zig +18 -23
  67. package/src/lib/gen2/helpers.zig +9 -18
  68. package/src/lib/gen2/mechanics.zig +69 -107
  69. package/src/lib/node.zig +2 -4
  70. package/src/lib/pkmn.zig +11 -5
  71. package/src/lib/wasm.zig +36 -1
  72. package/src/tools/display/dom.ts +96 -0
  73. package/src/tools/display/ui.css +354 -0
  74. package/build/test/display.js +0 -400
  75. package/build/test/display.js.map +0 -1
  76. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,37 +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;
14
+ const Choice = common.Choice;
24
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
25
18
  const Fail = protocol.Fail;
26
- const Heal = protocol.Heal;
27
-
28
19
  const Gen12 = rng.Gen12;
29
-
30
- const ActivePokemon = data.ActivePokemon;
31
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
32
22
  const Move = data.Move;
33
23
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
35
27
  const Side = data.Side;
36
28
  const Species = data.Species;
37
29
  const Stats = data.Stats;
@@ -174,7 +166,10 @@ fn selectMove(
174
166
  if (foe_choice.type == .Switch) {
175
167
  const last = side.active.move(battle.lastMove(player).index);
176
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
177
- 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
+ }
178
173
  }
179
174
  }
180
175
  return null;
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
248
  active.moves = incoming.moves;
254
249
 
255
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
256
252
 
257
253
  foe.active.volatiles.Binding = false;
258
254
 
259
255
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
256
 
262
257
  if (showdown and incoming.status == Status.TOX) {
263
258
  incoming.status = Status.init(.PSN);
@@ -353,7 +348,7 @@ fn doTurn(
353
348
  if (try checkFaint(battle, player, false, options)) |r| return r;
354
349
  } else if (foe_choice.type == .Pass) return null;
355
350
 
356
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
357
352
 
358
353
  residual = true;
359
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -425,7 +420,7 @@ fn executeMove(
425
420
  mslot = @intCast(battle.lastMove(player).index);
426
421
  const stored = side.stored();
427
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
428
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
429
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
430
425
  side.last_selected_move == .HyperBeam or
431
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -458,6 +453,25 @@ fn executeMove(
458
453
  .err => return @as(?Result, Result.Error),
459
454
  }
460
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(battle.lastMove(player).index).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
+
461
475
  const can = skip_can or
462
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
477
  if (!can) return null;
@@ -484,17 +498,22 @@ fn beforeMove(
484
498
 
485
499
  if (Status.is(stored.status, .SLP)) {
486
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
487
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
488
- if (options.calc.modify(player, .sleep)) |extend| {
489
- 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,
490
509
  } else {
491
510
  stored.status -= 1;
492
511
  }
493
512
 
494
513
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
498
517
 
499
518
  if (duration == 0) {
500
519
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +551,12 @@ fn beforeMove(
532
551
  }
533
552
 
534
553
  if (volatiles.disable_duration > 0) {
535
- if (options.calc.modify(player, .disable)) |extend| {
536
- if (!extend) volatiles.disable_duration = 0;
537
- } else {
538
- volatiles.disable_duration -= 1;
539
- }
540
- 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
+ );
541
560
 
542
561
  if (volatiles.disable_duration == 0) {
543
562
  volatiles.disable_move = 0;
@@ -550,19 +569,24 @@ fn beforeMove(
550
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
551
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
552
571
  if (side.last_selected_move == .None) {
553
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
554
573
  return .err;
555
574
  }
556
575
 
557
576
  if (volatiles.Confusion) {
558
577
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- 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,
562
585
  } else {
563
586
  volatiles.confusion -= 1;
564
587
  }
565
- try options.chance.confusion(player, volatiles.confusion);
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
590
 
567
591
  if (volatiles.confusion == 0) {
568
592
  volatiles.Confusion = false;
@@ -580,7 +604,9 @@ fn beforeMove(
580
604
  volatiles.Charging = false;
581
605
  volatiles.Binding = false;
582
606
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
607
+
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
584
610
  {
585
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +614,9 @@ fn beforeMove(
588
614
  const def = foe.active.stats.def;
589
615
  foe.active.stats.def = active.stats.def;
590
616
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
592
620
  }
593
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +645,10 @@ fn beforeMove(
617
645
  volatiles.Thrashing = false;
618
646
  volatiles.Charging = false;
619
647
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
648
+
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
651
+
621
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
653
  try log.cant(.{ ident, .Paralysis });
623
654
  return .done;
@@ -635,12 +666,8 @@ fn beforeMove(
635
666
  volatiles.state +%= battle.last_damage;
636
667
  }
637
668
 
638
- if (options.calc.modify(player, .attacking)) |extend| {
639
- if (!extend) volatiles.attacks = 0;
640
- } else {
641
- volatiles.attacks -= 1;
642
- }
643
- 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);
644
671
 
645
672
  if (volatiles.attacks != 0) {
646
673
  try log.activate(.{ ident, .Bide });
@@ -674,35 +701,40 @@ fn beforeMove(
674
701
  }
675
702
 
676
703
  if (volatiles.Thrashing) {
677
- if (options.calc.modify(player, .attacking)) |extend| {
678
- if (!extend) volatiles.attacks = 0;
679
- } else {
680
- volatiles.attacks -= 1;
681
- }
682
- try options.chance.attacking(player, volatiles.attacks);
704
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
705
+
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
683
708
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
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)
685
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);
686
726
  }
687
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active, player, options)) {
693
- try log.start(.{ battle.active(player), .ConfusionSilent });
694
- }
695
- }
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
+
696
732
  return .skip_can;
697
733
  }
698
734
 
699
735
  if (volatiles.Binding) {
700
- if (options.calc.modify(player, .binding)) |extend| {
701
- if (!extend) volatiles.attacks = 0;
702
- } else {
703
- volatiles.attacks -= 1;
704
- }
705
- 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);
706
738
 
707
739
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
740
  if (showdown or battle.last_damage != 0) {
@@ -756,9 +788,14 @@ fn canMove(
756
788
 
757
789
  if (move.effect == .Thrashing) {
758
790
  Effects.thrashing(battle, player, options);
759
- } else if (!showdown and move.effect == .Binding) {
760
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
761
- 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
+ }
762
799
  }
763
800
 
764
801
  return true;
@@ -795,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
833
  }
797
834
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
836
  var active = &side.active;
800
837
  const volatiles = &active.volatiles;
801
838
 
@@ -803,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
841
  if (showdown and volatiles.Transform) return;
805
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
806
846
  assert(mslot > 0 and mslot <= 4);
807
- 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;
808
848
  }
809
849
 
810
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -858,7 +898,7 @@ fn doMove(
858
898
 
859
899
  var late = showdown and move.effect != .Explode;
860
900
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
902
  if (showdown and move.effect.isMulti()) {
863
903
  try Effects.multiHit(battle, player, move, options);
864
904
  hits = side.active.volatiles.attacks;
@@ -880,8 +920,9 @@ fn doMove(
880
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
922
  battle.last_damage = if (ohko) 65535 else 0;
883
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
884
- } 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);
885
926
  return @as(?Result, Result.Error);
886
927
  }
887
928
  if (battle.last_damage == 0) {
@@ -895,11 +936,12 @@ fn doMove(
895
936
  }
896
937
  }
897
938
 
939
+ var missed: bool = undefined;
898
940
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
941
+ if (!showdown and !skip) {
942
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
943
+ miss = missed or zero;
944
+ }
903
945
 
904
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +968,32 @@ fn doMove(
926
968
  try log.immune(.{ foe_ident, .OHKO });
927
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
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
+ }
929
992
  } else if (mist) {
930
993
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
994
  try log.fail(.{ foe_ident, .None });
932
995
  } else {
933
- if (!showdown and !zero) try options.chance.commit(player, .miss);
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
997
  try log.lastmiss(.{});
935
998
  try log.miss(.{battle.active(player)});
936
999
  }
@@ -945,8 +1008,6 @@ fn doMove(
945
1008
  try buildRage(battle, player.foe(), options);
946
1009
  } else if (showdown and move.effect == .Disable) {
947
1010
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
1011
  }
951
1012
  return null;
952
1013
  } else if (!showdown) {
@@ -994,22 +1055,20 @@ fn doMove(
994
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1056
  }
996
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
998
1059
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
999
- if (move.effect == .Binding) {
1000
- try Effects.binding(battle, player, rewrap, options);
1001
- if (immune) {
1002
- battle.last_damage = 0;
1003
- assert(foe.stored().hp > 0);
1004
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1005
- if (sub) {
1006
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
- } else {
1008
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
- try buildRage(battle, player.foe(), options);
1010
- }
1011
- 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);
1012
1070
  }
1071
+ return null;
1013
1072
  }
1014
1073
  }
1015
1074
 
@@ -1069,6 +1128,7 @@ fn calcDamage(
1069
1128
  target_player: Player,
1070
1129
  m: ?Move.Data,
1071
1130
  crit: bool,
1131
+ options: anytype,
1072
1132
  ) bool {
1073
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1074
1134
  const cfz = m == null;
@@ -1080,28 +1140,8 @@ fn calcDamage(
1080
1140
 
1081
1141
  const special = move.type.special();
1082
1142
 
1083
- // zig fmt: off
1084
- var atk: u32 =
1085
- if (crit)
1086
- if (special) side.stored().stats.spc
1087
- else side.stored().stats.atk
1088
- else
1089
- if (special) side.active.stats.spc
1090
- else side.active.stats.atk;
1091
- var def: u32 =
1092
- if (crit)
1093
- if (special) target.stored().stats.spc
1094
- else target.stored().stats.def
1095
- else
1096
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1097
- if (special)
1098
- target.active.stats.spc *
1099
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1100
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1101
- else
1102
- target.active.stats.def *
1103
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1104
- // zig fmt: on
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1105
1145
 
1106
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1107
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1109,7 +1149,7 @@ fn calcDamage(
1109
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1110
1150
  atk = @max((atk / 4) & 255, 1);
1111
1151
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1112
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1113
1153
  }
1114
1154
 
1115
1155
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1126,11 +1166,41 @@ fn calcDamage(
1126
1166
  d = @min(997, d);
1127
1167
  d += 2;
1128
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) {
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
+
1129
1179
  battle.last_damage = @intCast(d);
1130
1180
 
1131
1181
  return true;
1132
1182
  }
1133
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
+
1134
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1135
1205
  const side = battle.side(player);
1136
1206
  const foe = battle.foe(player);
@@ -1185,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1185
1255
  .Psywave => power: {
1186
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1187
1257
  // GLITCH: Psywave infinite glitch loop
1188
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1189
1259
  break :power try Rolls.psywave(battle, player, max, options);
1190
1260
  },
1191
1261
  else => unreachable,
1192
1262
  };
1193
1263
 
1194
- 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;
1195
1265
 
1196
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1197
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1228,12 +1298,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1228
1298
 
1229
1299
  if (!used or !selected) {
1230
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1231
- if (!showdown) return Result.Error;
1301
+ if (!pkmn.options.mod) return Result.Error;
1232
1302
  try options.log.fail(.{ battle.active(player), .None });
1233
1303
  return null;
1234
1304
  }
1235
1305
 
1236
- battle.last_damage *|= 2;
1306
+ // NOTE: llvm/llvm-project#58557
1307
+ if (comptime builtin.target.isWasm()) {
1308
+ battle.last_damage = @max(battle.last_damage *% 2, std.math.maxInt(u16));
1309
+ } else {
1310
+ battle.last_damage *|= 2;
1311
+ }
1237
1312
 
1238
1313
  // Pokémon Showdown calls moveHit before Counter
1239
1314
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1265,7 +1340,7 @@ fn applyDamage(
1265
1340
  subbed.active.volatiles.Substitute = false;
1266
1341
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1342
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(sub_player);
1343
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1344
  return true;
1270
1345
  } else {
1271
1346
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1374,7 @@ fn mirrorMove(
1299
1374
  try options.log.fail(.{ battle.active(player), .None });
1300
1375
  return null;
1301
1376
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1377
+ incrementPP(side, player, mslot, options);
1303
1378
  }
1304
1379
 
1305
1380
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1395,7 @@ fn metronome(
1320
1395
  var side = battle.side(player);
1321
1396
 
1322
1397
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1398
+ incrementPP(side, player, mslot, options);
1324
1399
 
1325
1400
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1401
  return null;
@@ -1421,7 +1496,10 @@ fn moveHit(
1421
1496
 
1422
1497
  if (!miss) return true;
1423
1498
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1499
+
1424
1500
  side.active.volatiles.Binding = false;
1501
+ options.chance.observe(.binding, player, .None);
1502
+
1425
1503
  return false;
1426
1504
  }
1427
1505
 
@@ -1472,7 +1550,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1472
1550
  assert(!foe_volatiles.MultiHit);
1473
1551
  if (foe_volatiles.Bide) {
1474
1552
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1475
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1553
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1476
1554
  if (foe_volatiles.state != 0) return Result.Error;
1477
1555
  }
1478
1556
 
@@ -1563,12 +1641,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1563
1641
 
1564
1642
  battle.turn += 1;
1565
1643
 
1566
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1644
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1567
1645
  try options.log.tie(.{});
1568
1646
  return Result.Tie;
1569
1647
  }
1570
1648
 
1571
- if (showdown and battle.turn >= 1000) {
1649
+ if (pkmn.options.mod and battle.turn >= 1000) {
1572
1650
  try options.log.tie(.{});
1573
1651
  return Result.Tie;
1574
1652
  } else if (battle.turn >= 65535) {
@@ -1659,17 +1737,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1737
  }
1660
1738
  }
1661
1739
 
1662
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1663
- var volatiles = &active.volatiles;
1664
- assert(volatiles.Thrashing);
1665
- if (volatiles.attacks > 0) return false;
1666
-
1667
- volatiles.Thrashing = false;
1668
- volatiles.Confusion = true;
1669
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1670
- return true;
1671
- }
1672
-
1673
1740
  fn onBegin(
1674
1741
  battle: anytype,
1675
1742
  player: Player,
@@ -1756,8 +1823,10 @@ pub const Effects = struct {
1756
1823
 
1757
1824
  side.active.volatiles.Bide = true;
1758
1825
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1826
+
1759
1827
  side.active.volatiles.state = 0;
1760
1828
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1829
+ options.chance.observe(.attacking, player, .started);
1761
1830
 
1762
1831
  try options.log.start(.{ battle.active(player), .Bide });
1763
1832
  }
@@ -1823,7 +1892,10 @@ pub const Effects = struct {
1823
1892
 
1824
1893
  if (foe.active.volatiles.Confusion) return;
1825
1894
  foe.active.volatiles.Confusion = true;
1826
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1895
+
1896
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1897
+ options.chance.observe(.confusion, player.foe(), .started);
1898
+ options.calc.confusion(player.foe());
1827
1899
 
1828
1900
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1829
1901
  }
@@ -1869,18 +1941,18 @@ pub const Effects = struct {
1869
1941
 
1870
1942
  // Technically this is still considered simply a "miss" on the cartridge,
1871
1943
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1872
- if (n == 0) {
1944
+ if (n == 0 or (pkmn.options.mod and err)) {
1873
1945
  try options.log.fail(.{ foe_ident, .None });
1874
1946
  return null;
1875
1947
  } else if (err) {
1876
1948
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1877
- assert(!showdown);
1878
1949
  return Result.Error;
1879
1950
  }
1880
1951
 
1881
1952
  volatiles.disable_move =
1882
1953
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1883
1954
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1955
+ options.chance.observe(.disable, player.foe(), .started);
1884
1956
 
1885
1957
  const id = foe.active.move(volatiles.disable_move).id;
1886
1958
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1951,7 +2023,7 @@ pub const Effects = struct {
1951
2023
  if (foe.active.types.includes(move.type)) return;
1952
2024
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1953
2025
  // Freeze Clause Mod
1954
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2026
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1955
2027
 
1956
2028
  foe_stored.status = Status.init(.FRZ);
1957
2029
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1987,22 +2059,24 @@ pub const Effects = struct {
1987
2059
  if (p != player and Status.any(s.stored().status)) {
1988
2060
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1989
2061
  s.stored().status = 0;
2062
+ options.chance.observe(.sleep, p, .None);
1990
2063
  } else if (showdown and s.stored().status == Status.TOX) {
1991
2064
  s.stored().status = Status.init(.PSN);
1992
2065
  try log.status(.{ battle.active(p), s.stored().status, .None });
1993
2066
  }
1994
- try clearVolatiles(battle, p, p != player, options);
2067
+ try clearVolatiles(battle, p, options);
1995
2068
  }
1996
2069
  } else {
1997
2070
  if (Status.any(foe_stored.status)) {
1998
2071
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1999
2072
  foe.last_selected_move = .SKIP_TURN;
2073
+ options.chance.observe(.sleep, player.foe(), .None);
2000
2074
  }
2001
2075
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2002
2076
  foe_stored.status = 0;
2003
2077
  }
2004
- try clearVolatiles(battle, player, false, options);
2005
- try clearVolatiles(battle, player.foe(), true, options);
2078
+ try clearVolatiles(battle, player, options);
2079
+ try clearVolatiles(battle, player.foe(), options);
2006
2080
  }
2007
2081
  }
2008
2082
 
@@ -2290,7 +2364,7 @@ pub const Effects = struct {
2290
2364
  }
2291
2365
 
2292
2366
  // Sleep Clause Mod
2293
- if (showdown) {
2367
+ if (pkmn.options.mod) {
2294
2368
  for (foe.pokemon) |p| {
2295
2369
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2296
2370
  }
@@ -2298,6 +2372,8 @@ pub const Effects = struct {
2298
2372
  foe.active.volatiles.Recharging = false;
2299
2373
 
2300
2374
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2375
+ options.chance.observe(.sleep, player.foe(), .started);
2376
+
2301
2377
  const last = battle.side(player).last_selected_move;
2302
2378
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2303
2379
  }
@@ -2354,6 +2430,8 @@ pub const Effects = struct {
2354
2430
 
2355
2431
  volatiles.Thrashing = true;
2356
2432
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2433
+
2434
+ options.chance.observe(.attacking, player, .started);
2357
2435
  }
2358
2436
 
2359
2437
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2392,13 +2470,14 @@ pub const Effects = struct {
2392
2470
  if (side.active.volatiles.Binding) return;
2393
2471
  side.active.volatiles.Binding = true;
2394
2472
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2395
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2473
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2396
2474
  if (!showdown) {
2397
2475
  foe.active.volatiles.Recharging = false;
2398
2476
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2399
2477
 
2400
2478
  side.active.volatiles.attacks =
2401
2479
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2480
+ options.chance.observe(.binding, player, .started);
2402
2481
  }
2403
2482
 
2404
2483
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2628,22 +2707,22 @@ fn isForced(active: anytype) bool {
2628
2707
  active.volatiles.Thrashing or active.volatiles.Charging;
2629
2708
  }
2630
2709
 
2631
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2710
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2632
2711
  var log = options.log;
2633
2712
  var side = battle.side(who);
2634
2713
  var volatiles = &side.active.volatiles;
2635
2714
  const ident = battle.active(who);
2636
2715
 
2637
- options.chance.clearDurations(who, clear);
2638
-
2639
2716
  if (volatiles.disable_move != 0) {
2640
2717
  volatiles.disable_move = 0;
2641
2718
  volatiles.disable_duration = 0;
2719
+ options.chance.observe(.disable, who, .None);
2642
2720
  try log.end(.{ ident, .DisableSilent });
2643
2721
  }
2644
2722
  if (volatiles.Confusion) {
2645
2723
  // volatiles.confusion is left unchanged
2646
2724
  volatiles.Confusion = false;
2725
+ options.chance.observe(.confusion, who, .None);
2647
2726
  try log.end(.{ ident, .ConfusionSilent });
2648
2727
  }
2649
2728
  if (volatiles.Mist) {
@@ -2678,6 +2757,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2678
2757
  }
2679
2758
  }
2680
2759
 
2760
+ fn decrement(
2761
+ comptime field: chance.Action.Field,
2762
+ player: Player,
2763
+ options: anytype,
2764
+ n: anytype,
2765
+ ) @TypeOf(n) {
2766
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2767
+ .started, .ended => 0,
2768
+ .continuing => if (n > 1) n - 1 else n,
2769
+ else => unreachable,
2770
+ } else n - 1;
2771
+ }
2772
+
2681
2773
  pub const Rolls = struct {
2682
2774
  fn speedTie(battle: anytype, options: anytype) !bool {
2683
2775
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2875,9 +2967,10 @@ pub const Rolls = struct {
2875
2967
  }
2876
2968
 
2877
2969
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2878
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2879
- @intCast(val)
2880
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2970
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2971
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2972
+ else
2973
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
2974
  battle.rng.sleepDuration(player)
2882
2975
  else if (showdown)
2883
2976
  battle.rng.range(u3, 1, 8)
@@ -2889,14 +2982,15 @@ pub const Rolls = struct {
2889
2982
  };
2890
2983
 
2891
2984
  assert(duration >= 1 and duration <= 7);
2892
- options.chance.duration(.sleep, player, player.foe(), duration);
2985
+ options.chance.duration(player, duration);
2893
2986
  return duration;
2894
2987
  }
2895
2988
 
2896
2989
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2897
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2898
- val
2899
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2990
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2991
+ if (val >= 1 and val <= 8) val else null
2992
+ else
2993
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
2994
  battle.rng.disableDuration(player)
2901
2995
  else if (showdown)
2902
2996
  battle.rng.range(u4, 1, 9)
@@ -2904,29 +2998,31 @@ pub const Rolls = struct {
2904
2998
  @intCast((battle.rng.next() & 7) + 1);
2905
2999
 
2906
3000
  assert(duration >= 1 and duration <= 8);
2907
- options.chance.duration(.disable, player, player.foe(), duration);
3001
+ options.chance.duration(player, duration);
2908
3002
  return duration;
2909
3003
  }
2910
3004
 
2911
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2912
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2913
- @intCast(val)
2914
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2915
- battle.rng.confusionDuration(player, self)
3005
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3006
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3007
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3008
+ else
3009
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3010
+ battle.rng.confusionDuration(player)
2916
3011
  else if (showdown)
2917
3012
  battle.rng.range(u3, 2, 6)
2918
3013
  else
2919
3014
  @intCast((battle.rng.next() & 3) + 2);
2920
3015
 
2921
3016
  assert(duration >= 2 and duration <= 5);
2922
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3017
+ options.chance.duration(player, duration);
2923
3018
  return duration;
2924
3019
  }
2925
3020
 
2926
3021
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2927
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2928
- @intCast(val)
2929
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3022
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3023
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3024
+ else
3025
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
3026
  battle.rng.attackingDuration(player)
2931
3027
  else if (showdown)
2932
3028
  battle.rng.range(u3, 2, 4)
@@ -2934,7 +3030,7 @@ pub const Rolls = struct {
2934
3030
  @intCast((battle.rng.next() & 1) + 2);
2935
3031
 
2936
3032
  assert(duration >= 2 and duration <= 3);
2937
- options.chance.duration(.attacking, player, player, duration);
3033
+ options.chance.duration(player, duration);
2938
3034
  return duration;
2939
3035
  }
2940
3036
 
@@ -2947,9 +3043,10 @@ pub const Rolls = struct {
2947
3043
  options: anytype,
2948
3044
  ) !u3 {
2949
3045
  const roll = if (effect == .Binding) .duration else .multi_hit;
2950
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2951
- @intCast(val)
2952
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3046
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3047
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3048
+ else
3049
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
3050
  battle.rng.distribution(player)
2954
3051
  else if (showdown)
2955
3052
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2960,7 +3057,7 @@ pub const Rolls = struct {
2960
3057
 
2961
3058
  assert(n >= 2 and n <= 5);
2962
3059
  if (effect == .Binding) {
2963
- options.chance.duration(.binding, player, player, n);
3060
+ options.chance.duration(player, n);
2964
3061
  } else {
2965
3062
  try options.chance.multiHit(player, n);
2966
3063
  }
@@ -2975,15 +3072,13 @@ pub const Rolls = struct {
2975
3072
  options: anytype,
2976
3073
  ) !u4 {
2977
3074
  // TODO: consider throwing error instead of rerolling?
2978
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3075
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2979
3076
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2980
3077
  val
2981
3078
  else
2982
3079
  null
2983
3080
  else
2984
- null;
2985
-
2986
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3081
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2987
3082
  battle.rng.moveSlot(player, moves, check_pp)
2988
3083
  else slot: {
2989
3084
  if (showdown) {
@@ -3141,7 +3236,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3236
  // Struggle (Pokémon Showdown would use 'move 1' here)
3142
3237
  if (n == before) {
3143
3238
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3144
- if (!showdown) {
3239
+ if (!pkmn.options.mod) {
3145
3240
  while (slot <= 4) : (slot += 1) {
3146
3241
  if (active.moves[slot - 1].pp != 0) return n;
3147
3242
  }