@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.631762fb

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 (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -6
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +34 -65
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +23 -1
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -90
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +105 -103
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +52 -76
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +330 -85
  39. package/package.json +54 -135
  40. package/src/bin/install-pkmn-engine +10 -7
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +255 -0
  43. package/src/lib/bindings/node.zig +117 -0
  44. package/src/lib/bindings/wasm.zig +59 -0
  45. package/src/lib/common/array.zig +77 -0
  46. package/src/lib/common/data.zig +8 -8
  47. package/src/lib/common/debug.zig +39 -8
  48. package/src/lib/common/js.zig +4 -6
  49. package/src/lib/common/optional.zig +13 -9
  50. package/src/lib/common/options.zig +4 -0
  51. package/src/lib/common/protocol.zig +45 -44
  52. package/src/lib/common/rational.zig +338 -188
  53. package/src/lib/common/rng.zig +5 -7
  54. package/src/lib/common/util.zig +65 -1
  55. package/src/lib/gen1/calc.zig +529 -163
  56. package/src/lib/gen1/chance.zig +366 -240
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +1 -3
  59. package/src/lib/gen1/data/types.zig +2 -3
  60. package/src/lib/gen1/data.zig +35 -38
  61. package/src/lib/gen1/helpers.zig +17 -554
  62. package/src/lib/gen1/mechanics.zig +290 -194
  63. package/src/lib/gen2/calc.zig +37 -27
  64. package/src/lib/gen2/chance.zig +90 -503
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +7 -5
  69. package/src/lib/gen2/data.zig +28 -30
  70. package/src/lib/gen2/generated.zig +18 -23
  71. package/src/lib/gen2/helpers.zig +9 -18
  72. package/src/lib/gen2/mechanics.zig +89 -116
  73. package/src/lib/node.zig +4 -121
  74. package/src/lib/pkmn.zig +14 -7
  75. package/src/lib/wasm.zig +13 -9
  76. package/src/tools/display/dom.ts +93 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -400
  79. package/build/test/display.js.map +0 -1
  80. package/src/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()) });
683
698
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
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);
708
+
709
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
710
+ // ended (the engine's protocol is always from the omniscient perspective and clients
711
+ // can be expected to recognize this pattern and pass the information only to the
712
+ // correct player)
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.cpu.arch.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;
@@ -1379,7 +1446,16 @@ fn moveHit(
1379
1446
  break :miss true;
1380
1447
  }
1381
1448
  if (!showdown) {
1382
- if (move.effect == .Swift) return true;
1449
+ if (move.effect == .Swift) {
1450
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1451
+ // Showdown, hence why the block above doesn't include this code), we need the
1452
+ // damage roll to be committed. However, the commit code will not save this roll if
1453
+ // the glitch occurs and the move was not marked as having hit. To work around this
1454
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1455
+ // the hit as zero to ensure it doesn't actually get saved.
1456
+ try options.chance.hit(player, true, 0);
1457
+ return true;
1458
+ }
1383
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1384
1460
  }
1385
1461
 
@@ -1421,7 +1497,10 @@ fn moveHit(
1421
1497
 
1422
1498
  if (!miss) return true;
1423
1499
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1500
+
1424
1501
  side.active.volatiles.Binding = false;
1502
+ options.chance.observe(.binding, player, .None);
1503
+
1425
1504
  return false;
1426
1505
  }
1427
1506
 
@@ -1472,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1472
1551
  assert(!foe_volatiles.MultiHit);
1473
1552
  if (foe_volatiles.Bide) {
1474
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1475
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1554
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1476
1555
  if (foe_volatiles.state != 0) return Result.Error;
1477
1556
  }
1478
1557
 
@@ -1563,12 +1642,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1563
1642
 
1564
1643
  battle.turn += 1;
1565
1644
 
1566
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1567
1646
  try options.log.tie(.{});
1568
1647
  return Result.Tie;
1569
1648
  }
1570
1649
 
1571
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1572
1651
  try options.log.tie(.{});
1573
1652
  return Result.Tie;
1574
1653
  } else if (battle.turn >= 65535) {
@@ -1659,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1738
  }
1660
1739
  }
1661
1740
 
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
1741
  fn onBegin(
1674
1742
  battle: anytype,
1675
1743
  player: Player,
@@ -1756,8 +1824,10 @@ pub const Effects = struct {
1756
1824
 
1757
1825
  side.active.volatiles.Bide = true;
1758
1826
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1827
+
1759
1828
  side.active.volatiles.state = 0;
1760
1829
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1830
+ options.chance.observe(.attacking, player, .started);
1761
1831
 
1762
1832
  try options.log.start(.{ battle.active(player), .Bide });
1763
1833
  }
@@ -1823,7 +1893,10 @@ pub const Effects = struct {
1823
1893
 
1824
1894
  if (foe.active.volatiles.Confusion) return;
1825
1895
  foe.active.volatiles.Confusion = true;
1826
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1896
+
1897
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1898
+ options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1827
1900
 
1828
1901
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1829
1902
  }
@@ -1869,18 +1942,18 @@ pub const Effects = struct {
1869
1942
 
1870
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1871
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1872
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1873
1946
  try options.log.fail(.{ foe_ident, .None });
1874
1947
  return null;
1875
1948
  } else if (err) {
1876
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1877
- assert(!showdown);
1878
1950
  return Result.Error;
1879
1951
  }
1880
1952
 
1881
1953
  volatiles.disable_move =
1882
1954
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1883
1955
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1956
+ options.chance.observe(.disable, player.foe(), .started);
1884
1957
 
1885
1958
  const id = foe.active.move(volatiles.disable_move).id;
1886
1959
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1951,7 +2024,7 @@ pub const Effects = struct {
1951
2024
  if (foe.active.types.includes(move.type)) return;
1952
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1953
2026
  // Freeze Clause Mod
1954
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2027
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1955
2028
 
1956
2029
  foe_stored.status = Status.init(.FRZ);
1957
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1987,22 +2060,24 @@ pub const Effects = struct {
1987
2060
  if (p != player and Status.any(s.stored().status)) {
1988
2061
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1989
2062
  s.stored().status = 0;
2063
+ options.chance.observe(.sleep, p, .None);
1990
2064
  } else if (showdown and s.stored().status == Status.TOX) {
1991
2065
  s.stored().status = Status.init(.PSN);
1992
2066
  try log.status(.{ battle.active(p), s.stored().status, .None });
1993
2067
  }
1994
- try clearVolatiles(battle, p, p != player, options);
2068
+ try clearVolatiles(battle, p, options);
1995
2069
  }
1996
2070
  } else {
1997
2071
  if (Status.any(foe_stored.status)) {
1998
2072
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1999
2073
  foe.last_selected_move = .SKIP_TURN;
2074
+ options.chance.observe(.sleep, player.foe(), .None);
2000
2075
  }
2001
2076
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2002
2077
  foe_stored.status = 0;
2003
2078
  }
2004
- try clearVolatiles(battle, player, false, options);
2005
- try clearVolatiles(battle, player.foe(), true, options);
2079
+ try clearVolatiles(battle, player, options);
2080
+ try clearVolatiles(battle, player.foe(), options);
2006
2081
  }
2007
2082
  }
2008
2083
 
@@ -2290,7 +2365,7 @@ pub const Effects = struct {
2290
2365
  }
2291
2366
 
2292
2367
  // Sleep Clause Mod
2293
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2294
2369
  for (foe.pokemon) |p| {
2295
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2296
2371
  }
@@ -2298,6 +2373,8 @@ pub const Effects = struct {
2298
2373
  foe.active.volatiles.Recharging = false;
2299
2374
 
2300
2375
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2376
+ options.chance.observe(.sleep, player.foe(), .started);
2377
+
2301
2378
  const last = battle.side(player).last_selected_move;
2302
2379
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2303
2380
  }
@@ -2354,6 +2431,8 @@ pub const Effects = struct {
2354
2431
 
2355
2432
  volatiles.Thrashing = true;
2356
2433
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2434
+
2435
+ options.chance.observe(.attacking, player, .started);
2357
2436
  }
2358
2437
 
2359
2438
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2392,13 +2471,14 @@ pub const Effects = struct {
2392
2471
  if (side.active.volatiles.Binding) return;
2393
2472
  side.active.volatiles.Binding = true;
2394
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2395
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2396
2475
  if (!showdown) {
2397
2476
  foe.active.volatiles.Recharging = false;
2398
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2399
2478
 
2400
2479
  side.active.volatiles.attacks =
2401
2480
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2481
+ options.chance.observe(.binding, player, .started);
2402
2482
  }
2403
2483
 
2404
2484
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2628,22 +2708,22 @@ fn isForced(active: anytype) bool {
2628
2708
  active.volatiles.Thrashing or active.volatiles.Charging;
2629
2709
  }
2630
2710
 
2631
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2711
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2632
2712
  var log = options.log;
2633
2713
  var side = battle.side(who);
2634
2714
  var volatiles = &side.active.volatiles;
2635
2715
  const ident = battle.active(who);
2636
2716
 
2637
- options.chance.clearDurations(who, clear);
2638
-
2639
2717
  if (volatiles.disable_move != 0) {
2640
2718
  volatiles.disable_move = 0;
2641
2719
  volatiles.disable_duration = 0;
2720
+ options.chance.observe(.disable, who, .None);
2642
2721
  try log.end(.{ ident, .DisableSilent });
2643
2722
  }
2644
2723
  if (volatiles.Confusion) {
2645
2724
  // volatiles.confusion is left unchanged
2646
2725
  volatiles.Confusion = false;
2726
+ options.chance.observe(.confusion, who, .None);
2647
2727
  try log.end(.{ ident, .ConfusionSilent });
2648
2728
  }
2649
2729
  if (volatiles.Mist) {
@@ -2678,6 +2758,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2678
2758
  }
2679
2759
  }
2680
2760
 
2761
+ fn decrement(
2762
+ comptime field: chance.Action.Field,
2763
+ player: Player,
2764
+ options: anytype,
2765
+ n: anytype,
2766
+ ) @TypeOf(n) {
2767
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2768
+ .started, .ended => 0,
2769
+ .continuing => if (n > 1) n - 1 else n,
2770
+ else => unreachable,
2771
+ } else n - 1;
2772
+ }
2773
+
2681
2774
  pub const Rolls = struct {
2682
2775
  fn speedTie(battle: anytype, options: anytype) !bool {
2683
2776
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2875,9 +2968,10 @@ pub const Rolls = struct {
2875
2968
  }
2876
2969
 
2877
2970
  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"))
2971
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2972
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2973
+ else
2974
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
2975
  battle.rng.sleepDuration(player)
2882
2976
  else if (showdown)
2883
2977
  battle.rng.range(u3, 1, 8)
@@ -2889,14 +2983,15 @@ pub const Rolls = struct {
2889
2983
  };
2890
2984
 
2891
2985
  assert(duration >= 1 and duration <= 7);
2892
- options.chance.duration(.sleep, player, player.foe(), duration);
2986
+ options.chance.duration(player, duration);
2893
2987
  return duration;
2894
2988
  }
2895
2989
 
2896
2990
  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"))
2991
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2992
+ if (val >= 1 and val <= 8) val else null
2993
+ else
2994
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
2995
  battle.rng.disableDuration(player)
2901
2996
  else if (showdown)
2902
2997
  battle.rng.range(u4, 1, 9)
@@ -2904,29 +2999,31 @@ pub const Rolls = struct {
2904
2999
  @intCast((battle.rng.next() & 7) + 1);
2905
3000
 
2906
3001
  assert(duration >= 1 and duration <= 8);
2907
- options.chance.duration(.disable, player, player.foe(), duration);
3002
+ options.chance.duration(player, duration);
2908
3003
  return duration;
2909
3004
  }
2910
3005
 
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)
3006
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3007
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3008
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3009
+ else
3010
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3011
+ battle.rng.confusionDuration(player)
2916
3012
  else if (showdown)
2917
3013
  battle.rng.range(u3, 2, 6)
2918
3014
  else
2919
3015
  @intCast((battle.rng.next() & 3) + 2);
2920
3016
 
2921
3017
  assert(duration >= 2 and duration <= 5);
2922
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3018
+ options.chance.duration(player, duration);
2923
3019
  return duration;
2924
3020
  }
2925
3021
 
2926
3022
  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"))
3023
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3024
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3025
+ else
3026
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
3027
  battle.rng.attackingDuration(player)
2931
3028
  else if (showdown)
2932
3029
  battle.rng.range(u3, 2, 4)
@@ -2934,7 +3031,7 @@ pub const Rolls = struct {
2934
3031
  @intCast((battle.rng.next() & 1) + 2);
2935
3032
 
2936
3033
  assert(duration >= 2 and duration <= 3);
2937
- options.chance.duration(.attacking, player, player, duration);
3034
+ options.chance.duration(player, duration);
2938
3035
  return duration;
2939
3036
  }
2940
3037
 
@@ -2947,9 +3044,10 @@ pub const Rolls = struct {
2947
3044
  options: anytype,
2948
3045
  ) !u3 {
2949
3046
  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"))
3047
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3048
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3049
+ else
3050
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
3051
  battle.rng.distribution(player)
2954
3052
  else if (showdown)
2955
3053
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2960,7 +3058,7 @@ pub const Rolls = struct {
2960
3058
 
2961
3059
  assert(n >= 2 and n <= 5);
2962
3060
  if (effect == .Binding) {
2963
- options.chance.duration(.binding, player, player, n);
3061
+ options.chance.duration(player, n);
2964
3062
  } else {
2965
3063
  try options.chance.multiHit(player, n);
2966
3064
  }
@@ -2975,15 +3073,13 @@ pub const Rolls = struct {
2975
3073
  options: anytype,
2976
3074
  ) !u4 {
2977
3075
  // TODO: consider throwing error instead of rerolling?
2978
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2979
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2980
3078
  val
2981
3079
  else
2982
3080
  null
2983
3081
  else
2984
- null;
2985
-
2986
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2987
3083
  battle.rng.moveSlot(player, moves, check_pp)
2988
3084
  else slot: {
2989
3085
  if (showdown) {
@@ -3141,7 +3237,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3142
3238
  if (n == before) {
3143
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3144
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3145
3241
  while (slot <= 4) : (slot += 1) {
3146
3242
  if (active.moves[slot - 1].pp != 0) return n;
3147
3243
  }