@pkmn/engine 0.1.0-dev.deda8593 → 0.1.0-dev.e0a786f8

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 (79) 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 +47 -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 +110 -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 +53 -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 +15 -18
  19. package/build/pkg/gen1.js +28 -19
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +40 -90
  22. package/build/pkg/index.js +23 -70
  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 +52 -76
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +152 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +152 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +359 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +33 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +330 -85
  38. package/package.json +54 -135
  39. package/src/bin/install-pkmn-engine +10 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/c.zig +255 -0
  42. package/src/lib/bindings/node.zig +121 -0
  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 +8 -8
  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 +13 -9
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +45 -44
  51. package/src/lib/common/rational.zig +323 -157
  52. package/src/lib/common/rng.zig +5 -7
  53. package/src/lib/common/util.zig +60 -1
  54. package/src/lib/gen1/calc.zig +527 -161
  55. package/src/lib/gen1/chance.zig +366 -240
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +35 -38
  60. package/src/lib/gen1/helpers.zig +17 -554
  61. package/src/lib/gen1/mechanics.zig +280 -193
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +90 -503
  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 +26 -29
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +9 -18
  71. package/src/lib/gen2/mechanics.zig +88 -115
  72. package/src/lib/node.zig +3 -116
  73. package/src/lib/pkmn.zig +14 -7
  74. package/src/lib/wasm.zig +13 -9
  75. package/src/tools/display/dom.ts +93 -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/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(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
+
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,19 +614,14 @@ 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
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
- // on the targeting behavior of the confused Pokémon's selected move which results
595
- // in the wrong behavior with respect to the Substitute + Confusion glitch
596
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
597
- player
598
- else
599
- player.foe();
600
621
 
601
622
  const uncapped = battle.last_damage;
602
623
  // Skipping adjustDamage / randomizeDamage / checkHit
603
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
604
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
605
626
  if (showdown) battle.last_damage = uncapped;
606
627
 
@@ -617,7 +638,10 @@ fn beforeMove(
617
638
  volatiles.Thrashing = false;
618
639
  volatiles.Charging = false;
619
640
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
641
+
642
+ options.chance.observe(.attacking, player, .None);
643
+ options.chance.observe(.binding, player, .None);
644
+
621
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
646
  try log.cant(.{ ident, .Paralysis });
623
647
  return .done;
@@ -635,12 +659,8 @@ fn beforeMove(
635
659
  volatiles.state +%= battle.last_damage;
636
660
  }
637
661
 
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);
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
664
 
645
665
  if (volatiles.attacks != 0) {
646
666
  try log.activate(.{ ident, .Bide });
@@ -674,35 +694,40 @@ fn beforeMove(
674
694
  }
675
695
 
676
696
  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);
697
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
698
+
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
701
+
702
+ if (volatiles.attacks == 0) {
703
+ const overwritten = volatiles.Confusion;
704
+
705
+ volatiles.Thrashing = false;
706
+ volatiles.Confusion = true;
707
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
683
708
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
709
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
710
+ // ended (the engine's protocol is always from the omniscient perspective and clients
711
+ // can be expected to recognize this pattern and pass the information only to the
712
+ // correct player)
685
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
714
+ options.chance.observe(.confusion, player, if (overwritten)
715
+ if (pkmn.options.overwrite) .overwritten else .continuing
716
+ else
717
+ .started);
718
+ if (!overwritten) options.calc.confusion(player);
686
719
  }
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
- }
720
+
721
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
722
+ // used in useMove and doesn't have the notion of skipping canMove semantics
723
+ if (showdown) side.last_used_move = side.last_selected_move;
724
+
696
725
  return .skip_can;
697
726
  }
698
727
 
699
728
  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);
729
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
731
 
707
732
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
733
  if (showdown or battle.last_damage != 0) {
@@ -756,9 +781,14 @@ fn canMove(
756
781
 
757
782
  if (move.effect == .Thrashing) {
758
783
  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);
784
+ } else if (move.effect == .Binding) {
785
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
786
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
787
+ if (showdown) {
788
+ battle.foe(player).active.volatiles.Recharging = false;
789
+ } else {
790
+ try Effects.binding(battle, player, false, options);
791
+ }
762
792
  }
763
793
 
764
794
  return true;
@@ -795,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
826
  }
797
827
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
829
  var active = &side.active;
800
830
  const volatiles = &active.volatiles;
801
831
 
@@ -803,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
834
  if (showdown and volatiles.Transform) return;
805
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
806
839
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
840
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
841
  }
809
842
 
810
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -858,7 +891,7 @@ fn doMove(
858
891
 
859
892
  var late = showdown and move.effect != .Explode;
860
893
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
895
  if (showdown and move.effect.isMulti()) {
863
896
  try Effects.multiHit(battle, player, move, options);
864
897
  hits = side.active.volatiles.attacks;
@@ -880,8 +913,9 @@ fn doMove(
880
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
915
  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)) {
916
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
917
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
918
+ if (!showdown) try options.chance.commit(player, .err);
885
919
  return @as(?Result, Result.Error);
886
920
  }
887
921
  if (battle.last_damage == 0) {
@@ -895,11 +929,12 @@ fn doMove(
895
929
  }
896
930
  }
897
931
 
932
+ var missed: bool = undefined;
898
933
  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);
934
+ if (!showdown and !skip) {
935
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
936
+ miss = missed or zero;
937
+ }
903
938
 
904
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +961,32 @@ fn doMove(
926
961
  try log.immune(.{ foe_ident, .OHKO });
927
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
963
  try log.immune(.{ foe_ident, .None });
964
+ if (!showdown) {
965
+ if (move.effect == .Binding) {
966
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
967
+ // then simply clears it later on if the move happens to miss in moveHit.
968
+ // However, moveHit can return true and we'll still end up in the outer miss
969
+ // handling block because attacking into an immune Pokemon does zero damage
970
+ // (considered missing)
971
+ assert(zero);
972
+ // Thus we still need to actually save the hit result (but not the crit roll).
973
+ // Using .miss for this commit would accomplish what we want, but is somewhat
974
+ // misleading as its entirely possible the move actually did hit (and the target
975
+ // is now trapped) so we use the .binding value instead which is effectively
976
+ // identical, just doesn't claim to know about the actual results of the hit
977
+ // roll)
978
+ try options.chance.commit(player, .binding);
979
+ } else {
980
+ // The .glitch option allows for conditionally committing if we are in a
981
+ // division-by-zero freeze alternative timeline scenario
982
+ try options.chance.commit(player, .glitch);
983
+ }
984
+ }
929
985
  } else if (mist) {
930
986
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
987
  try log.fail(.{ foe_ident, .None });
932
988
  } else {
933
- if (!showdown and !zero) try options.chance.commit(player, .miss);
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
990
  try log.lastmiss(.{});
935
991
  try log.miss(.{battle.active(player)});
936
992
  }
@@ -945,8 +1001,6 @@ fn doMove(
945
1001
  try buildRage(battle, player.foe(), options);
946
1002
  } else if (showdown and move.effect == .Disable) {
947
1003
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
1004
  }
951
1005
  return null;
952
1006
  } else if (!showdown) {
@@ -994,22 +1048,20 @@ fn doMove(
994
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1049
  }
996
1050
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1051
+ } else if (showdown and move.effect == .Binding) {
998
1052
  // 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;
1053
+ try Effects.binding(battle, player, rewrap, options);
1054
+ if (immune) {
1055
+ battle.last_damage = 0;
1056
+ assert(foe.stored().hp > 0);
1057
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1058
+ if (sub) {
1059
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1060
+ } else {
1061
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1062
+ try buildRage(battle, player.foe(), options);
1012
1063
  }
1064
+ return null;
1013
1065
  }
1014
1066
  }
1015
1067
 
@@ -1069,6 +1121,7 @@ fn calcDamage(
1069
1121
  target_player: Player,
1070
1122
  m: ?Move.Data,
1071
1123
  crit: bool,
1124
+ options: anytype,
1072
1125
  ) bool {
1073
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1074
1127
  const cfz = m == null;
@@ -1080,28 +1133,8 @@ fn calcDamage(
1080
1133
 
1081
1134
  const special = move.type.special();
1082
1135
 
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
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1105
1138
 
1106
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1107
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1109,7 +1142,7 @@ fn calcDamage(
1109
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1110
1143
  atk = @max((atk / 4) & 255, 1);
1111
1144
  // 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);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1113
1146
  }
1114
1147
 
1115
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1126,11 +1159,41 @@ fn calcDamage(
1126
1159
  d = @min(997, d);
1127
1160
  d += 2;
1128
1161
 
1162
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1163
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1164
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1165
+ // that results in counter intuitive probabilities in the common case
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1167
+ atk = attack(side, !crit, special);
1168
+ def = defense(target, !crit, special, false);
1169
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1170
+ }
1171
+
1129
1172
  battle.last_damage = @intCast(d);
1130
1173
 
1131
1174
  return true;
1132
1175
  }
1133
1176
 
1177
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1180
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1181
+ }
1182
+
1183
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) target.stored().stats.spc else target.stored().stats.def
1186
+ else
1187
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1188
+ if (special)
1189
+ target.active.stats.spc *
1190
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1191
+ else
1192
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1193
+ target.active.stats.def *
1194
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1195
+ }
1196
+
1134
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1135
1198
  const side = battle.side(player);
1136
1199
  const foe = battle.foe(player);
@@ -1185,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1185
1248
  .Psywave => power: {
1186
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1187
1250
  // GLITCH: Psywave infinite glitch loop
1188
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1189
1252
  break :power try Rolls.psywave(battle, player, max, options);
1190
1253
  },
1191
1254
  else => unreachable,
1192
1255
  };
1193
1256
 
1194
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1195
1258
 
1196
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1197
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1228,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1228
1291
 
1229
1292
  if (!used or !selected) {
1230
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1231
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return Result.Error;
1232
1295
  try options.log.fail(.{ battle.active(player), .None });
1233
1296
  return null;
1234
1297
  }
1235
1298
 
1236
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1237
1304
 
1238
1305
  // Pokémon Showdown calls moveHit before Counter
1239
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1265,7 +1332,7 @@ fn applyDamage(
1265
1332
  subbed.active.volatiles.Substitute = false;
1266
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(sub_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1336
  return true;
1270
1337
  } else {
1271
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1366,7 @@ fn mirrorMove(
1299
1366
  try options.log.fail(.{ battle.active(player), .None });
1300
1367
  return null;
1301
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1303
1370
  }
1304
1371
 
1305
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1387,7 @@ fn metronome(
1320
1387
  var side = battle.side(player);
1321
1388
 
1322
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1324
1391
 
1325
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1393
  return null;
@@ -1421,7 +1488,10 @@ fn moveHit(
1421
1488
 
1422
1489
  if (!miss) return true;
1423
1490
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1491
+
1424
1492
  side.active.volatiles.Binding = false;
1493
+ options.chance.observe(.binding, player, .None);
1494
+
1425
1495
  return false;
1426
1496
  }
1427
1497
 
@@ -1472,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1472
1542
  assert(!foe_volatiles.MultiHit);
1473
1543
  if (foe_volatiles.Bide) {
1474
1544
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1475
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1545
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1476
1546
  if (foe_volatiles.state != 0) return Result.Error;
1477
1547
  }
1478
1548
 
@@ -1563,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1563
1633
 
1564
1634
  battle.turn += 1;
1565
1635
 
1566
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1636
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1567
1637
  try options.log.tie(.{});
1568
1638
  return Result.Tie;
1569
1639
  }
1570
1640
 
1571
- if (showdown and battle.turn >= 1000) {
1641
+ if (pkmn.options.mod and battle.turn >= 1000) {
1572
1642
  try options.log.tie(.{});
1573
1643
  return Result.Tie;
1574
1644
  } else if (battle.turn >= 65535) {
@@ -1659,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1729
  }
1660
1730
  }
1661
1731
 
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
1732
  fn onBegin(
1674
1733
  battle: anytype,
1675
1734
  player: Player,
@@ -1756,8 +1815,10 @@ pub const Effects = struct {
1756
1815
 
1757
1816
  side.active.volatiles.Bide = true;
1758
1817
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1818
+
1759
1819
  side.active.volatiles.state = 0;
1760
1820
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1821
+ options.chance.observe(.attacking, player, .started);
1761
1822
 
1762
1823
  try options.log.start(.{ battle.active(player), .Bide });
1763
1824
  }
@@ -1823,7 +1884,10 @@ pub const Effects = struct {
1823
1884
 
1824
1885
  if (foe.active.volatiles.Confusion) return;
1825
1886
  foe.active.volatiles.Confusion = true;
1826
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1887
+
1888
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1889
+ options.chance.observe(.confusion, player.foe(), .started);
1890
+ options.calc.confusion(player.foe());
1827
1891
 
1828
1892
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1829
1893
  }
@@ -1869,18 +1933,18 @@ pub const Effects = struct {
1869
1933
 
1870
1934
  // Technically this is still considered simply a "miss" on the cartridge,
1871
1935
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1872
- if (n == 0) {
1936
+ if (n == 0 or (pkmn.options.mod and err)) {
1873
1937
  try options.log.fail(.{ foe_ident, .None });
1874
1938
  return null;
1875
1939
  } else if (err) {
1876
1940
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1877
- assert(!showdown);
1878
1941
  return Result.Error;
1879
1942
  }
1880
1943
 
1881
1944
  volatiles.disable_move =
1882
1945
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1883
1946
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1947
+ options.chance.observe(.disable, player.foe(), .started);
1884
1948
 
1885
1949
  const id = foe.active.move(volatiles.disable_move).id;
1886
1950
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1951,7 +2015,7 @@ pub const Effects = struct {
1951
2015
  if (foe.active.types.includes(move.type)) return;
1952
2016
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1953
2017
  // Freeze Clause Mod
1954
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2018
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1955
2019
 
1956
2020
  foe_stored.status = Status.init(.FRZ);
1957
2021
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1987,22 +2051,24 @@ pub const Effects = struct {
1987
2051
  if (p != player and Status.any(s.stored().status)) {
1988
2052
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1989
2053
  s.stored().status = 0;
2054
+ options.chance.observe(.sleep, p, .None);
1990
2055
  } else if (showdown and s.stored().status == Status.TOX) {
1991
2056
  s.stored().status = Status.init(.PSN);
1992
2057
  try log.status(.{ battle.active(p), s.stored().status, .None });
1993
2058
  }
1994
- try clearVolatiles(battle, p, p != player, options);
2059
+ try clearVolatiles(battle, p, options);
1995
2060
  }
1996
2061
  } else {
1997
2062
  if (Status.any(foe_stored.status)) {
1998
2063
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1999
2064
  foe.last_selected_move = .SKIP_TURN;
2065
+ options.chance.observe(.sleep, player.foe(), .None);
2000
2066
  }
2001
2067
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2002
2068
  foe_stored.status = 0;
2003
2069
  }
2004
- try clearVolatiles(battle, player, false, options);
2005
- try clearVolatiles(battle, player.foe(), true, options);
2070
+ try clearVolatiles(battle, player, options);
2071
+ try clearVolatiles(battle, player.foe(), options);
2006
2072
  }
2007
2073
  }
2008
2074
 
@@ -2290,7 +2356,7 @@ pub const Effects = struct {
2290
2356
  }
2291
2357
 
2292
2358
  // Sleep Clause Mod
2293
- if (showdown) {
2359
+ if (pkmn.options.mod) {
2294
2360
  for (foe.pokemon) |p| {
2295
2361
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2296
2362
  }
@@ -2298,6 +2364,8 @@ pub const Effects = struct {
2298
2364
  foe.active.volatiles.Recharging = false;
2299
2365
 
2300
2366
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2367
+ options.chance.observe(.sleep, player.foe(), .started);
2368
+
2301
2369
  const last = battle.side(player).last_selected_move;
2302
2370
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2303
2371
  }
@@ -2354,6 +2422,8 @@ pub const Effects = struct {
2354
2422
 
2355
2423
  volatiles.Thrashing = true;
2356
2424
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2425
+
2426
+ options.chance.observe(.attacking, player, .started);
2357
2427
  }
2358
2428
 
2359
2429
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2392,13 +2462,14 @@ pub const Effects = struct {
2392
2462
  if (side.active.volatiles.Binding) return;
2393
2463
  side.active.volatiles.Binding = true;
2394
2464
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2395
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2465
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2396
2466
  if (!showdown) {
2397
2467
  foe.active.volatiles.Recharging = false;
2398
2468
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2399
2469
 
2400
2470
  side.active.volatiles.attacks =
2401
2471
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2472
+ options.chance.observe(.binding, player, .started);
2402
2473
  }
2403
2474
 
2404
2475
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2628,22 +2699,22 @@ fn isForced(active: anytype) bool {
2628
2699
  active.volatiles.Thrashing or active.volatiles.Charging;
2629
2700
  }
2630
2701
 
2631
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2702
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2632
2703
  var log = options.log;
2633
2704
  var side = battle.side(who);
2634
2705
  var volatiles = &side.active.volatiles;
2635
2706
  const ident = battle.active(who);
2636
2707
 
2637
- options.chance.clearDurations(who, clear);
2638
-
2639
2708
  if (volatiles.disable_move != 0) {
2640
2709
  volatiles.disable_move = 0;
2641
2710
  volatiles.disable_duration = 0;
2711
+ options.chance.observe(.disable, who, .None);
2642
2712
  try log.end(.{ ident, .DisableSilent });
2643
2713
  }
2644
2714
  if (volatiles.Confusion) {
2645
2715
  // volatiles.confusion is left unchanged
2646
2716
  volatiles.Confusion = false;
2717
+ options.chance.observe(.confusion, who, .None);
2647
2718
  try log.end(.{ ident, .ConfusionSilent });
2648
2719
  }
2649
2720
  if (volatiles.Mist) {
@@ -2678,6 +2749,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2678
2749
  }
2679
2750
  }
2680
2751
 
2752
+ fn decrement(
2753
+ comptime field: chance.Action.Field,
2754
+ player: Player,
2755
+ options: anytype,
2756
+ n: anytype,
2757
+ ) @TypeOf(n) {
2758
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2759
+ .started, .ended => 0,
2760
+ .continuing => if (n > 1) n - 1 else n,
2761
+ else => unreachable,
2762
+ } else n - 1;
2763
+ }
2764
+
2681
2765
  pub const Rolls = struct {
2682
2766
  fn speedTie(battle: anytype, options: anytype) !bool {
2683
2767
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2875,9 +2959,10 @@ pub const Rolls = struct {
2875
2959
  }
2876
2960
 
2877
2961
  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"))
2962
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2963
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2964
+ else
2965
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
2966
  battle.rng.sleepDuration(player)
2882
2967
  else if (showdown)
2883
2968
  battle.rng.range(u3, 1, 8)
@@ -2889,14 +2974,15 @@ pub const Rolls = struct {
2889
2974
  };
2890
2975
 
2891
2976
  assert(duration >= 1 and duration <= 7);
2892
- options.chance.duration(.sleep, player, player.foe(), duration);
2977
+ options.chance.duration(player, duration);
2893
2978
  return duration;
2894
2979
  }
2895
2980
 
2896
2981
  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"))
2982
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 1 and val <= 8) val else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
2986
  battle.rng.disableDuration(player)
2901
2987
  else if (showdown)
2902
2988
  battle.rng.range(u4, 1, 9)
@@ -2904,29 +2990,31 @@ pub const Rolls = struct {
2904
2990
  @intCast((battle.rng.next() & 7) + 1);
2905
2991
 
2906
2992
  assert(duration >= 1 and duration <= 8);
2907
- options.chance.duration(.disable, player, player.foe(), duration);
2993
+ options.chance.duration(player, duration);
2908
2994
  return duration;
2909
2995
  }
2910
2996
 
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)
2997
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3002
+ battle.rng.confusionDuration(player)
2916
3003
  else if (showdown)
2917
3004
  battle.rng.range(u3, 2, 6)
2918
3005
  else
2919
3006
  @intCast((battle.rng.next() & 3) + 2);
2920
3007
 
2921
3008
  assert(duration >= 2 and duration <= 5);
2922
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3009
+ options.chance.duration(player, duration);
2923
3010
  return duration;
2924
3011
  }
2925
3012
 
2926
3013
  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"))
3014
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3015
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
3018
  battle.rng.attackingDuration(player)
2931
3019
  else if (showdown)
2932
3020
  battle.rng.range(u3, 2, 4)
@@ -2934,7 +3022,7 @@ pub const Rolls = struct {
2934
3022
  @intCast((battle.rng.next() & 1) + 2);
2935
3023
 
2936
3024
  assert(duration >= 2 and duration <= 3);
2937
- options.chance.duration(.attacking, player, player, duration);
3025
+ options.chance.duration(player, duration);
2938
3026
  return duration;
2939
3027
  }
2940
3028
 
@@ -2947,9 +3035,10 @@ pub const Rolls = struct {
2947
3035
  options: anytype,
2948
3036
  ) !u3 {
2949
3037
  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"))
3038
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3039
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3040
+ else
3041
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
3042
  battle.rng.distribution(player)
2954
3043
  else if (showdown)
2955
3044
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2960,7 +3049,7 @@ pub const Rolls = struct {
2960
3049
 
2961
3050
  assert(n >= 2 and n <= 5);
2962
3051
  if (effect == .Binding) {
2963
- options.chance.duration(.binding, player, player, n);
3052
+ options.chance.duration(player, n);
2964
3053
  } else {
2965
3054
  try options.chance.multiHit(player, n);
2966
3055
  }
@@ -2975,15 +3064,13 @@ pub const Rolls = struct {
2975
3064
  options: anytype,
2976
3065
  ) !u4 {
2977
3066
  // TODO: consider throwing error instead of rerolling?
2978
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3067
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2979
3068
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2980
3069
  val
2981
3070
  else
2982
3071
  null
2983
3072
  else
2984
- null;
2985
-
2986
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3073
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2987
3074
  battle.rng.moveSlot(player, moves, check_pp)
2988
3075
  else slot: {
2989
3076
  if (showdown) {
@@ -3141,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3228
  // Struggle (Pokémon Showdown would use 'move 1' here)
3142
3229
  if (n == before) {
3143
3230
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3144
- if (!showdown) {
3231
+ if (!pkmn.options.mod) {
3145
3232
  while (slot <= 4) : (slot += 1) {
3146
3233
  if (active.moves[slot - 1].pp != 0) return n;
3147
3234
  }