@pkmn/engine 0.1.0-dev.15f64810 → 0.1.0-dev.1d311928

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 (78) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +44 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +97 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +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 +13 -17
  19. package/build/pkg/gen1.js +6 -10
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +38 -90
  22. package/build/pkg/index.js +6 -63
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +35 -69
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +141 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +142 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +349 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +18 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +256 -77
  38. package/package.json +52 -134
  39. package/src/bin/install-pkmn-engine +9 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/node.zig +121 -0
  42. package/src/lib/bindings/wasm.zig +52 -0
  43. package/src/lib/common/array.zig +77 -0
  44. package/src/lib/common/data.zig +8 -8
  45. package/src/lib/common/debug.zig +39 -8
  46. package/src/lib/common/js.zig +4 -2
  47. package/src/lib/common/optional.zig +13 -9
  48. package/src/lib/common/options.zig +4 -0
  49. package/src/lib/common/protocol.zig +45 -44
  50. package/src/lib/common/rational.zig +120 -27
  51. package/src/lib/common/rng.zig +5 -7
  52. package/src/lib/common/util.zig +50 -1
  53. package/src/lib/gen1/calc.zig +610 -186
  54. package/src/lib/gen1/chance.zig +366 -240
  55. package/src/lib/gen1/data/moves.zig +2 -4
  56. package/src/lib/gen1/data/species.zig +1 -3
  57. package/src/lib/gen1/data/types.zig +1 -2
  58. package/src/lib/gen1/data.zig +35 -38
  59. package/src/lib/gen1/helpers.zig +17 -560
  60. package/src/lib/gen1/mechanics.zig +290 -193
  61. package/src/lib/gen2/calc.zig +37 -27
  62. package/src/lib/gen2/chance.zig +90 -503
  63. package/src/lib/gen2/data/items.zig +1 -3
  64. package/src/lib/gen2/data/moves.zig +2 -4
  65. package/src/lib/gen2/data/species.zig +1 -3
  66. package/src/lib/gen2/data/types.zig +1 -2
  67. package/src/lib/gen2/data.zig +26 -29
  68. package/src/lib/gen2/generated.zig +18 -23
  69. package/src/lib/gen2/helpers.zig +9 -18
  70. package/src/lib/gen2/mechanics.zig +88 -115
  71. package/src/lib/node.zig +3 -116
  72. package/src/lib/pkmn.zig +14 -7
  73. package/src/lib/wasm.zig +18 -9
  74. package/src/tools/display/dom.ts +96 -0
  75. package/src/tools/display/ui.css +354 -0
  76. package/build/test/display.js +0 -400
  77. package/build/test/display.js.map +0 -1
  78. 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);
@@ -347,13 +342,13 @@ fn doTurn(
347
342
  )) |r| return r;
348
343
  if (!replace) {
349
344
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
345
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
346
  }
352
347
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
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;
@@ -369,10 +364,10 @@ fn doTurn(
369
364
  )) |r| return r;
370
365
  if (!replace) {
371
366
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
367
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
368
  }
374
369
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
371
  }
377
372
 
378
373
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -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,7 +614,9 @@ fn beforeMove(
588
614
  const def = foe.active.stats.def;
589
615
  foe.active.stats.def = active.stats.def;
590
616
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
592
620
  }
593
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +645,10 @@ fn beforeMove(
617
645
  volatiles.Thrashing = false;
618
646
  volatiles.Charging = false;
619
647
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
648
+
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
651
+
621
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
653
  try log.cant(.{ ident, .Paralysis });
623
654
  return .done;
@@ -635,12 +666,8 @@ fn beforeMove(
635
666
  volatiles.state +%= battle.last_damage;
636
667
  }
637
668
 
638
- if (options.calc.modify(player, .attacking)) |extend| {
639
- if (!extend) volatiles.attacks = 0;
640
- } else {
641
- volatiles.attacks -= 1;
642
- }
643
- try options.chance.attacking(player, volatiles.attacks);
669
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
670
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
671
 
645
672
  if (volatiles.attacks != 0) {
646
673
  try log.activate(.{ ident, .Bide });
@@ -674,35 +701,40 @@ fn beforeMove(
674
701
  }
675
702
 
676
703
  if (volatiles.Thrashing) {
677
- if (options.calc.modify(player, .attacking)) |extend| {
678
- if (!extend) volatiles.attacks = 0;
679
- } else {
680
- volatiles.attacks -= 1;
681
- }
682
- try options.chance.attacking(player, volatiles.attacks);
704
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
705
+
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
708
+
709
+ if (volatiles.attacks == 0) {
710
+ const overwritten = volatiles.Confusion;
711
+
712
+ volatiles.Thrashing = false;
713
+ volatiles.Confusion = true;
714
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
683
715
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
716
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
717
+ // ended (the engine's protocol is always from the omniscient perspective and clients
718
+ // can be expected to recognize this pattern and pass the information only to the
719
+ // correct player)
685
720
  try log.start(.{ battle.active(player), .ConfusionSilent });
721
+ options.chance.observe(.confusion, player, if (overwritten)
722
+ if (pkmn.options.overwrite) .overwritten else .continuing
723
+ else
724
+ .started);
725
+ if (!overwritten) options.calc.confusion(player);
686
726
  }
687
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active, player, options)) {
693
- try log.start(.{ battle.active(player), .ConfusionSilent });
694
- }
695
- }
727
+
728
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
729
+ // used in useMove and doesn't have the notion of skipping canMove semantics
730
+ if (showdown) side.last_used_move = side.last_selected_move;
731
+
696
732
  return .skip_can;
697
733
  }
698
734
 
699
735
  if (volatiles.Binding) {
700
- if (options.calc.modify(player, .binding)) |extend| {
701
- if (!extend) volatiles.attacks = 0;
702
- } else {
703
- volatiles.attacks -= 1;
704
- }
705
- try options.chance.binding(player, volatiles.attacks);
736
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
737
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
738
 
707
739
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
740
  if (showdown or battle.last_damage != 0) {
@@ -756,9 +788,14 @@ fn canMove(
756
788
 
757
789
  if (move.effect == .Thrashing) {
758
790
  Effects.thrashing(battle, player, options);
759
- } else if (!showdown and move.effect == .Binding) {
760
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
761
- try Effects.binding(battle, player, false, options);
791
+ } else if (move.effect == .Binding) {
792
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
793
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
794
+ if (showdown) {
795
+ battle.foe(player).active.volatiles.Recharging = false;
796
+ } else {
797
+ try Effects.binding(battle, player, false, options);
798
+ }
762
799
  }
763
800
 
764
801
  return true;
@@ -795,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
833
  }
797
834
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
836
  var active = &side.active;
800
837
  const volatiles = &active.volatiles;
801
838
 
@@ -803,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
841
  if (showdown and volatiles.Transform) return;
805
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
806
846
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
847
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
848
  }
809
849
 
810
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -858,7 +898,7 @@ fn doMove(
858
898
 
859
899
  var late = showdown and move.effect != .Explode;
860
900
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
902
  if (showdown and move.effect.isMulti()) {
863
903
  try Effects.multiHit(battle, player, move, options);
864
904
  hits = side.active.volatiles.attacks;
@@ -880,8 +920,9 @@ fn doMove(
880
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
922
  battle.last_damage = if (ohko) 65535 else 0;
883
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
884
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
923
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
924
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
925
+ if (!showdown) try options.chance.commit(player, .err);
885
926
  return @as(?Result, Result.Error);
886
927
  }
887
928
  if (battle.last_damage == 0) {
@@ -895,11 +936,12 @@ fn doMove(
895
936
  }
896
937
  }
897
938
 
939
+ var missed: bool = undefined;
898
940
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
941
+ if (!showdown and !skip) {
942
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
943
+ miss = missed or zero;
944
+ }
903
945
 
904
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +968,32 @@ fn doMove(
926
968
  try log.immune(.{ foe_ident, .OHKO });
927
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
970
  try log.immune(.{ foe_ident, .None });
971
+ if (!showdown) {
972
+ if (move.effect == .Binding) {
973
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
974
+ // then simply clears it later on if the move happens to miss in moveHit.
975
+ // However, moveHit can return true and we'll still end up in the outer miss
976
+ // handling block because attacking into an immune Pokemon does zero damage
977
+ // (considered missing)
978
+ assert(zero);
979
+ // Thus we still need to actually save the hit result (but not the crit roll).
980
+ // Using .miss for this commit would accomplish what we want, but is somewhat
981
+ // misleading as its entirely possible the move actually did hit (and the target
982
+ // is now trapped) so we use the .binding value instead which is effectively
983
+ // identical, just doesn't claim to know about the actual results of the hit
984
+ // roll)
985
+ try options.chance.commit(player, .binding);
986
+ } else {
987
+ // The .glitch option allows for conditionally committing if we are in a
988
+ // division-by-zero freeze alternative timeline scenario
989
+ try options.chance.commit(player, .glitch);
990
+ }
991
+ }
929
992
  } else if (mist) {
930
993
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
994
  try log.fail(.{ foe_ident, .None });
932
995
  } else {
933
- if (!showdown and !zero) try options.chance.commit(player, .miss);
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
997
  try log.lastmiss(.{});
935
998
  try log.miss(.{battle.active(player)});
936
999
  }
@@ -945,8 +1008,6 @@ fn doMove(
945
1008
  try buildRage(battle, player.foe(), options);
946
1009
  } else if (showdown and move.effect == .Disable) {
947
1010
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
1011
  }
951
1012
  return null;
952
1013
  } else if (!showdown) {
@@ -994,22 +1055,20 @@ fn doMove(
994
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1056
  }
996
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
998
1059
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
999
- if (move.effect == .Binding) {
1000
- try Effects.binding(battle, player, rewrap, options);
1001
- if (immune) {
1002
- battle.last_damage = 0;
1003
- assert(foe.stored().hp > 0);
1004
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1005
- if (sub) {
1006
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
- } else {
1008
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
- try buildRage(battle, player.foe(), options);
1010
- }
1011
- return null;
1060
+ try Effects.binding(battle, player, rewrap, options);
1061
+ if (immune) {
1062
+ battle.last_damage = 0;
1063
+ assert(foe.stored().hp > 0);
1064
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1065
+ if (sub) {
1066
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1067
+ } else {
1068
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1069
+ try buildRage(battle, player.foe(), options);
1012
1070
  }
1071
+ return null;
1013
1072
  }
1014
1073
  }
1015
1074
 
@@ -1069,6 +1128,7 @@ fn calcDamage(
1069
1128
  target_player: Player,
1070
1129
  m: ?Move.Data,
1071
1130
  crit: bool,
1131
+ options: anytype,
1072
1132
  ) bool {
1073
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1074
1134
  const cfz = m == null;
@@ -1080,28 +1140,8 @@ fn calcDamage(
1080
1140
 
1081
1141
  const special = move.type.special();
1082
1142
 
1083
- // zig fmt: off
1084
- var atk: u32 =
1085
- if (crit)
1086
- if (special) side.stored().stats.spc
1087
- else side.stored().stats.atk
1088
- else
1089
- if (special) side.active.stats.spc
1090
- else side.active.stats.atk;
1091
- var def: u32 =
1092
- if (crit)
1093
- if (special) target.stored().stats.spc
1094
- else target.stored().stats.def
1095
- else
1096
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1097
- if (special)
1098
- target.active.stats.spc *
1099
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1100
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1101
- else
1102
- target.active.stats.def *
1103
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1104
- // zig fmt: on
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1105
1145
 
1106
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1107
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1109,7 +1149,7 @@ fn calcDamage(
1109
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1110
1150
  atk = @max((atk / 4) & 255, 1);
1111
1151
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1112
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1113
1153
  }
1114
1154
 
1115
1155
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1126,11 +1166,41 @@ fn calcDamage(
1126
1166
  d = @min(997, d);
1127
1167
  d += 2;
1128
1168
 
1169
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1170
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1171
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1172
+ // that results in counter intuitive probabilities in the common case
1173
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1174
+ atk = attack(side, !crit, special);
1175
+ def = defense(target, !crit, special, false);
1176
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1177
+ }
1178
+
1129
1179
  battle.last_damage = @intCast(d);
1130
1180
 
1131
1181
  return true;
1132
1182
  }
1133
1183
 
1184
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1185
+ return if (crit)
1186
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1187
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1188
+ }
1189
+
1190
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1191
+ return if (crit)
1192
+ if (special) target.stored().stats.spc else target.stored().stats.def
1193
+ else
1194
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1195
+ if (special)
1196
+ target.active.stats.spc *
1197
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1198
+ else
1199
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1200
+ target.active.stats.def *
1201
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1202
+ }
1203
+
1134
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1135
1205
  const side = battle.side(player);
1136
1206
  const foe = battle.foe(player);
@@ -1185,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1185
1255
  .Psywave => power: {
1186
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1187
1257
  // GLITCH: Psywave infinite glitch loop
1188
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1189
1259
  break :power try Rolls.psywave(battle, player, max, options);
1190
1260
  },
1191
1261
  else => unreachable,
1192
1262
  };
1193
1263
 
1194
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1264
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1195
1265
 
1196
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1197
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1228,12 +1298,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1228
1298
 
1229
1299
  if (!used or !selected) {
1230
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1231
- if (!showdown) return Result.Error;
1301
+ if (!pkmn.options.mod) return Result.Error;
1232
1302
  try options.log.fail(.{ battle.active(player), .None });
1233
1303
  return null;
1234
1304
  }
1235
1305
 
1236
- battle.last_damage *|= 2;
1306
+ // NOTE: llvm/llvm-project#58557
1307
+ battle.last_damage = if (comptime builtin.target.isWasm())
1308
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1309
+ else
1310
+ battle.last_damage *| 2;
1237
1311
 
1238
1312
  // Pokémon Showdown calls moveHit before Counter
1239
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1265,7 +1339,7 @@ fn applyDamage(
1265
1339
  subbed.active.volatiles.Substitute = false;
1266
1340
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1341
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1343
  return true;
1270
1344
  } else {
1271
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1373,7 @@ fn mirrorMove(
1299
1373
  try options.log.fail(.{ battle.active(player), .None });
1300
1374
  return null;
1301
1375
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1376
+ incrementPP(side, player, mslot, options);
1303
1377
  }
1304
1378
 
1305
1379
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1394,7 @@ fn metronome(
1320
1394
  var side = battle.side(player);
1321
1395
 
1322
1396
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1397
+ incrementPP(side, player, mslot, options);
1324
1398
 
1325
1399
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1400
  return null;
@@ -1421,13 +1495,17 @@ fn moveHit(
1421
1495
 
1422
1496
  if (!miss) return true;
1423
1497
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1498
+
1424
1499
  side.active.volatiles.Binding = false;
1500
+ options.chance.observe(.binding, player, .None);
1501
+
1425
1502
  return false;
1426
1503
  }
1427
1504
 
1428
1505
  fn checkFaint(
1429
1506
  battle: anytype,
1430
1507
  player: Player,
1508
+ cap: bool,
1431
1509
  options: anytype,
1432
1510
  ) @TypeOf(options.log).Error!?Result {
1433
1511
  const side = battle.side(player);
@@ -1440,8 +1518,8 @@ fn checkFaint(
1440
1518
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1441
1519
  const more = player_out or foe_out;
1442
1520
 
1443
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1444
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1521
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1522
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1445
1523
 
1446
1524
  assert(!side.active.volatiles.MultiHit);
1447
1525
  assert(!foe.active.volatiles.MultiHit);
@@ -1462,7 +1540,7 @@ fn checkFaint(
1462
1540
  return .{ .p1 = foe_choice, .p2 = .Switch };
1463
1541
  }
1464
1542
 
1465
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1543
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1466
1544
  var side = battle.side(player);
1467
1545
  var foe = battle.foe(player);
1468
1546
  assert(side.stored().hp == 0);
@@ -1471,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1471
1549
  assert(!foe_volatiles.MultiHit);
1472
1550
  if (foe_volatiles.Bide) {
1473
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1474
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1552
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1475
1553
  if (foe_volatiles.state != 0) return Result.Error;
1476
1554
  }
1477
1555
 
@@ -1496,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1496
1574
  }
1497
1575
 
1498
1576
  try options.log.faint(.{ battle.active(player), done });
1499
- options.calc.capped(player);
1577
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1578
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1579
+ if (cap) options.calc.capped(player);
1500
1580
  return null;
1501
1581
  }
1502
1582
 
@@ -1560,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1560
1640
 
1561
1641
  battle.turn += 1;
1562
1642
 
1563
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1564
1644
  try options.log.tie(.{});
1565
1645
  return Result.Tie;
1566
1646
  }
1567
1647
 
1568
- if (showdown and battle.turn >= 1000) {
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1569
1649
  try options.log.tie(.{});
1570
1650
  return Result.Tie;
1571
1651
  } else if (battle.turn >= 65535) {
@@ -1656,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1656
1736
  }
1657
1737
  }
1658
1738
 
1659
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1660
- var volatiles = &active.volatiles;
1661
- assert(volatiles.Thrashing);
1662
- if (volatiles.attacks > 0) return false;
1663
-
1664
- volatiles.Thrashing = false;
1665
- volatiles.Confusion = true;
1666
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1667
- return true;
1668
- }
1669
-
1670
1739
  fn onBegin(
1671
1740
  battle: anytype,
1672
1741
  player: Player,
@@ -1753,8 +1822,10 @@ pub const Effects = struct {
1753
1822
 
1754
1823
  side.active.volatiles.Bide = true;
1755
1824
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1825
+
1756
1826
  side.active.volatiles.state = 0;
1757
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1828
+ options.chance.observe(.attacking, player, .started);
1758
1829
 
1759
1830
  try options.log.start(.{ battle.active(player), .Bide });
1760
1831
  }
@@ -1820,7 +1891,10 @@ pub const Effects = struct {
1820
1891
 
1821
1892
  if (foe.active.volatiles.Confusion) return;
1822
1893
  foe.active.volatiles.Confusion = true;
1823
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1894
+
1895
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1896
+ options.chance.observe(.confusion, player.foe(), .started);
1897
+ options.calc.confusion(player.foe());
1824
1898
 
1825
1899
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1826
1900
  }
@@ -1866,18 +1940,18 @@ pub const Effects = struct {
1866
1940
 
1867
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1868
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1869
- if (n == 0) {
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1870
1944
  try options.log.fail(.{ foe_ident, .None });
1871
1945
  return null;
1872
1946
  } else if (err) {
1873
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1874
- assert(!showdown);
1875
1948
  return Result.Error;
1876
1949
  }
1877
1950
 
1878
1951
  volatiles.disable_move =
1879
1952
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1880
1953
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1954
+ options.chance.observe(.disable, player.foe(), .started);
1881
1955
 
1882
1956
  const id = foe.active.move(volatiles.disable_move).id;
1883
1957
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1948,7 +2022,7 @@ pub const Effects = struct {
1948
2022
  if (foe.active.types.includes(move.type)) return;
1949
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1950
2024
  // Freeze Clause Mod
1951
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2025
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1952
2026
 
1953
2027
  foe_stored.status = Status.init(.FRZ);
1954
2028
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1984,22 +2058,24 @@ pub const Effects = struct {
1984
2058
  if (p != player and Status.any(s.stored().status)) {
1985
2059
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1986
2060
  s.stored().status = 0;
2061
+ options.chance.observe(.sleep, p, .None);
1987
2062
  } else if (showdown and s.stored().status == Status.TOX) {
1988
2063
  s.stored().status = Status.init(.PSN);
1989
2064
  try log.status(.{ battle.active(p), s.stored().status, .None });
1990
2065
  }
1991
- try clearVolatiles(battle, p, p != player, options);
2066
+ try clearVolatiles(battle, p, options);
1992
2067
  }
1993
2068
  } else {
1994
2069
  if (Status.any(foe_stored.status)) {
1995
2070
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1996
2071
  foe.last_selected_move = .SKIP_TURN;
2072
+ options.chance.observe(.sleep, player.foe(), .None);
1997
2073
  }
1998
2074
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1999
2075
  foe_stored.status = 0;
2000
2076
  }
2001
- try clearVolatiles(battle, player, false, options);
2002
- try clearVolatiles(battle, player.foe(), true, options);
2077
+ try clearVolatiles(battle, player, options);
2078
+ try clearVolatiles(battle, player.foe(), options);
2003
2079
  }
2004
2080
  }
2005
2081
 
@@ -2287,7 +2363,7 @@ pub const Effects = struct {
2287
2363
  }
2288
2364
 
2289
2365
  // Sleep Clause Mod
2290
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2291
2367
  for (foe.pokemon) |p| {
2292
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2293
2369
  }
@@ -2295,6 +2371,8 @@ pub const Effects = struct {
2295
2371
  foe.active.volatiles.Recharging = false;
2296
2372
 
2297
2373
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2374
+ options.chance.observe(.sleep, player.foe(), .started);
2375
+
2298
2376
  const last = battle.side(player).last_selected_move;
2299
2377
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2300
2378
  }
@@ -2351,6 +2429,8 @@ pub const Effects = struct {
2351
2429
 
2352
2430
  volatiles.Thrashing = true;
2353
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2432
+
2433
+ options.chance.observe(.attacking, player, .started);
2354
2434
  }
2355
2435
 
2356
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2389,13 +2469,14 @@ pub const Effects = struct {
2389
2469
  if (side.active.volatiles.Binding) return;
2390
2470
  side.active.volatiles.Binding = true;
2391
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2392
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2393
2473
  if (!showdown) {
2394
2474
  foe.active.volatiles.Recharging = false;
2395
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2396
2476
 
2397
2477
  side.active.volatiles.attacks =
2398
2478
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2479
+ options.chance.observe(.binding, player, .started);
2399
2480
  }
2400
2481
 
2401
2482
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2625,22 +2706,22 @@ fn isForced(active: anytype) bool {
2625
2706
  active.volatiles.Thrashing or active.volatiles.Charging;
2626
2707
  }
2627
2708
 
2628
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2709
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2629
2710
  var log = options.log;
2630
2711
  var side = battle.side(who);
2631
2712
  var volatiles = &side.active.volatiles;
2632
2713
  const ident = battle.active(who);
2633
2714
 
2634
- options.chance.clearDurations(who, clear);
2635
-
2636
2715
  if (volatiles.disable_move != 0) {
2637
2716
  volatiles.disable_move = 0;
2638
2717
  volatiles.disable_duration = 0;
2718
+ options.chance.observe(.disable, who, .None);
2639
2719
  try log.end(.{ ident, .DisableSilent });
2640
2720
  }
2641
2721
  if (volatiles.Confusion) {
2642
2722
  // volatiles.confusion is left unchanged
2643
2723
  volatiles.Confusion = false;
2724
+ options.chance.observe(.confusion, who, .None);
2644
2725
  try log.end(.{ ident, .ConfusionSilent });
2645
2726
  }
2646
2727
  if (volatiles.Mist) {
@@ -2675,6 +2756,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2675
2756
  }
2676
2757
  }
2677
2758
 
2759
+ fn decrement(
2760
+ comptime field: chance.Action.Field,
2761
+ player: Player,
2762
+ options: anytype,
2763
+ n: anytype,
2764
+ ) @TypeOf(n) {
2765
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2766
+ .started, .ended => 0,
2767
+ .continuing => if (n > 1) n - 1 else n,
2768
+ else => unreachable,
2769
+ } else n - 1;
2770
+ }
2771
+
2678
2772
  pub const Rolls = struct {
2679
2773
  fn speedTie(battle: anytype, options: anytype) !bool {
2680
2774
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2872,9 +2966,10 @@ pub const Rolls = struct {
2872
2966
  }
2873
2967
 
2874
2968
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2875
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2876
- @intCast(val)
2877
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2969
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2970
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2971
+ else
2972
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2878
2973
  battle.rng.sleepDuration(player)
2879
2974
  else if (showdown)
2880
2975
  battle.rng.range(u3, 1, 8)
@@ -2886,14 +2981,15 @@ pub const Rolls = struct {
2886
2981
  };
2887
2982
 
2888
2983
  assert(duration >= 1 and duration <= 7);
2889
- options.chance.duration(.sleep, player, player.foe(), duration);
2984
+ options.chance.duration(player, duration);
2890
2985
  return duration;
2891
2986
  }
2892
2987
 
2893
2988
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2894
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2895
- val
2896
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2989
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2990
+ if (val >= 1 and val <= 8) val else null
2991
+ else
2992
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2897
2993
  battle.rng.disableDuration(player)
2898
2994
  else if (showdown)
2899
2995
  battle.rng.range(u4, 1, 9)
@@ -2901,29 +2997,31 @@ pub const Rolls = struct {
2901
2997
  @intCast((battle.rng.next() & 7) + 1);
2902
2998
 
2903
2999
  assert(duration >= 1 and duration <= 8);
2904
- options.chance.duration(.disable, player, player.foe(), duration);
3000
+ options.chance.duration(player, duration);
2905
3001
  return duration;
2906
3002
  }
2907
3003
 
2908
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2909
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2910
- @intCast(val)
2911
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2912
- battle.rng.confusionDuration(player, self)
3004
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3005
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3006
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3007
+ else
3008
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3009
+ battle.rng.confusionDuration(player)
2913
3010
  else if (showdown)
2914
3011
  battle.rng.range(u3, 2, 6)
2915
3012
  else
2916
3013
  @intCast((battle.rng.next() & 3) + 2);
2917
3014
 
2918
3015
  assert(duration >= 2 and duration <= 5);
2919
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3016
+ options.chance.duration(player, duration);
2920
3017
  return duration;
2921
3018
  }
2922
3019
 
2923
3020
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2924
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2925
- @intCast(val)
2926
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3021
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3022
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3023
+ else
3024
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2927
3025
  battle.rng.attackingDuration(player)
2928
3026
  else if (showdown)
2929
3027
  battle.rng.range(u3, 2, 4)
@@ -2931,7 +3029,7 @@ pub const Rolls = struct {
2931
3029
  @intCast((battle.rng.next() & 1) + 2);
2932
3030
 
2933
3031
  assert(duration >= 2 and duration <= 3);
2934
- options.chance.duration(.attacking, player, player, duration);
3032
+ options.chance.duration(player, duration);
2935
3033
  return duration;
2936
3034
  }
2937
3035
 
@@ -2944,9 +3042,10 @@ pub const Rolls = struct {
2944
3042
  options: anytype,
2945
3043
  ) !u3 {
2946
3044
  const roll = if (effect == .Binding) .duration else .multi_hit;
2947
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2948
- @intCast(val)
2949
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3045
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3046
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3047
+ else
3048
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2950
3049
  battle.rng.distribution(player)
2951
3050
  else if (showdown)
2952
3051
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2957,7 +3056,7 @@ pub const Rolls = struct {
2957
3056
 
2958
3057
  assert(n >= 2 and n <= 5);
2959
3058
  if (effect == .Binding) {
2960
- options.chance.duration(.binding, player, player, n);
3059
+ options.chance.duration(player, n);
2961
3060
  } else {
2962
3061
  try options.chance.multiHit(player, n);
2963
3062
  }
@@ -2972,15 +3071,13 @@ pub const Rolls = struct {
2972
3071
  options: anytype,
2973
3072
  ) !u4 {
2974
3073
  // TODO: consider throwing error instead of rerolling?
2975
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2976
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2977
3076
  val
2978
3077
  else
2979
3078
  null
2980
3079
  else
2981
- null;
2982
-
2983
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3080
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2984
3081
  battle.rng.moveSlot(player, moves, check_pp)
2985
3082
  else slot: {
2986
3083
  if (showdown) {
@@ -3138,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3138
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3139
3236
  if (n == before) {
3140
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3141
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3142
3239
  while (slot <= 4) : (slot += 1) {
3143
3240
  if (active.moves[slot - 1].pp != 0) return n;
3144
3241
  }