@pkmn/engine 0.1.0-dev.09d1baec → 0.1.0-dev.0cabf16e

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 (64) hide show
  1. package/LICENSE +0 -2
  2. package/README.md +15 -11
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +43 -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 +96 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon.d.ts +2 -0
  10. package/build/pkg/addon.js +35 -58
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data.d.ts +25 -4
  13. package/build/pkg/data.js.map +1 -1
  14. package/build/pkg/gen1.js +1 -1
  15. package/build/pkg/gen1.js.map +1 -1
  16. package/build/pkg/index.d.ts +22 -9
  17. package/build/pkg/index.js +15 -10
  18. package/build/pkg/index.js.map +1 -1
  19. package/build/pkg/protocol.d.ts +20 -1
  20. package/build/pkg/protocol.js +109 -107
  21. package/build/pkg/protocol.js.map +1 -1
  22. package/build/tools/debug.js +36 -70
  23. package/build/tools/debug.js.map +1 -1
  24. package/build/tools/display/debug.jsx +141 -0
  25. package/build/tools/display/debug.jsx.map +1 -0
  26. package/build/tools/display/index.js +132 -0
  27. package/build/tools/display/index.js.map +1 -0
  28. package/build/tools/display/ui.jsx +349 -0
  29. package/build/tools/display/ui.jsx.map +1 -0
  30. package/build/tools/display/util.js +18 -0
  31. package/build/tools/display/util.js.map +1 -0
  32. package/build.zig +187 -72
  33. package/package.json +48 -32
  34. package/src/bin/install-pkmn-engine +17 -7
  35. package/src/bin/pkmn-debug +1 -1
  36. package/src/lib/common/array.zig +77 -0
  37. package/src/lib/common/data.zig +8 -8
  38. package/src/lib/common/debug.zig +39 -8
  39. package/src/lib/common/js.zig +4 -2
  40. package/src/lib/common/optional.zig +13 -9
  41. package/src/lib/common/options.zig +4 -0
  42. package/src/lib/common/protocol.zig +51 -44
  43. package/src/lib/common/rational.zig +114 -24
  44. package/src/lib/common/rng.zig +10 -10
  45. package/src/lib/common/util.zig +50 -1
  46. package/src/lib/gen1/calc.zig +671 -125
  47. package/src/lib/gen1/chance.zig +384 -275
  48. package/src/lib/gen1/data/species.zig +1 -1
  49. package/src/lib/gen1/data.zig +31 -30
  50. package/src/lib/gen1/helpers.zig +89 -538
  51. package/src/lib/gen1/mechanics.zig +297 -189
  52. package/src/lib/gen2/calc.zig +34 -16
  53. package/src/lib/gen2/chance.zig +90 -502
  54. package/src/lib/gen2/data.zig +15 -14
  55. package/src/lib/gen2/helpers.zig +41 -13
  56. package/src/lib/gen2/mechanics.zig +158 -103
  57. package/src/lib/pkmn.zig +10 -16
  58. package/src/tools/display/dom.ts +96 -0
  59. package/src/tools/display/ui.css +359 -0
  60. package/build/test/display.js +0 -400
  61. package/build/test/display.js.map +0 -1
  62. package/src/lib/gen1/README.md +0 -894
  63. package/src/lib/gen2/README.md +0 -329
  64. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
10
  const data = @import("data.zig");
11
+ const chance = @import("chance.zig");
11
12
 
12
13
  const assert = std.debug.assert;
13
14
 
@@ -27,11 +28,9 @@ const Heal = protocol.Heal;
27
28
 
28
29
  const Gen12 = rng.Gen12;
29
30
 
30
- const ActivePokemon = data.ActivePokemon;
31
31
  const Effectiveness = data.Effectiveness;
32
32
  const Move = data.Move;
33
33
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
35
34
  const Side = data.Side;
36
35
  const Species = data.Species;
37
36
  const Stats = data.Stats;
@@ -166,7 +165,7 @@ fn selectMove(
166
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
166
  // set the selected move will not actually be used, it will just be reported as having
168
167
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
168
+ // replicate the incorrect behavior with the correct mechanisms)
170
169
  saveMove(battle, player, choice);
171
170
  } else {
172
171
  assert(choice.data == 0);
@@ -253,11 +252,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
252
  active.moves = incoming.moves;
254
253
 
255
254
  statusModify(incoming.status, &active.stats);
255
+ options.chance.switched(player, slot);
256
256
 
257
257
  foe.active.volatiles.Binding = false;
258
258
 
259
259
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
260
 
262
261
  if (showdown and incoming.status == Status.TOX) {
263
262
  incoming.status = Status.init(.PSN);
@@ -347,13 +346,13 @@ fn doTurn(
347
346
  )) |r| return r;
348
347
  if (!replace) {
349
348
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
349
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
350
  }
352
351
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
352
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
353
  } else if (foe_choice.type == .Pass) return null;
355
354
 
356
- options.chance.clearPending();
355
+ if (!showdown) options.chance.clearPending();
357
356
 
358
357
  residual = true;
359
358
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +368,10 @@ fn doTurn(
369
368
  )) |r| return r;
370
369
  if (!replace) {
371
370
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
371
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
372
  }
374
373
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
374
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
375
  }
377
376
 
378
377
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +430,7 @@ fn executeMove(
431
430
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
431
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
432
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // index 1 and the current Pokémon has the same move in its first slot.
433
+ // index 1 and the current Pokémon has the same move in its first slot
435
434
  if (!auto) {
436
435
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
436
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -484,17 +483,22 @@ fn beforeMove(
484
483
 
485
484
  if (Status.is(stored.status, .SLP)) {
486
485
  const before = stored.status;
486
+ const slf = Status.is(stored.status, .EXT);
487
487
  // 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;
488
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
489
+ .started, .ended => stored.status = 0,
490
+ .continuing => if (Status.duration(stored.status) > 1) {
491
+ stored.status -= 1;
492
+ },
493
+ else => unreachable,
490
494
  } else {
491
495
  stored.status -= 1;
492
496
  }
493
497
 
494
498
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
499
+ try options.chance.sleep(player, if (slf)
500
+ .None
501
+ else if (duration == 0) .ended else .continuing);
498
502
 
499
503
  if (duration == 0) {
500
504
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +536,12 @@ fn beforeMove(
532
536
  }
533
537
 
534
538
  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);
539
+ volatiles.disable_duration =
540
+ decrement(.disable, player, options, volatiles.disable_duration);
541
+ try options.chance.disable(
542
+ player,
543
+ if (volatiles.disable_duration == 0) .ended else .continuing,
544
+ );
541
545
 
542
546
  if (volatiles.disable_duration == 0) {
543
547
  volatiles.disable_move = 0;
@@ -556,13 +560,18 @@ fn beforeMove(
556
560
 
557
561
  if (volatiles.Confusion) {
558
562
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
563
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
564
+ .started, .ended => volatiles.confusion = 0,
565
+ .continuing => if (volatiles.confusion > 1) {
566
+ volatiles.confusion -= 1;
567
+ },
568
+ .overwritten => {},
569
+ else => unreachable,
562
570
  } else {
563
571
  volatiles.confusion -= 1;
564
572
  }
565
- try options.chance.confusion(player, volatiles.confusion);
573
+
574
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
575
 
567
576
  if (volatiles.confusion == 0) {
568
577
  volatiles.Confusion = false;
@@ -580,7 +589,9 @@ fn beforeMove(
580
589
  volatiles.Charging = false;
581
590
  volatiles.Binding = false;
582
591
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
592
+
593
+ options.chance.observe(.attacking, player, .None);
594
+ options.chance.observe(.binding, player, .None);
584
595
  {
585
596
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
597
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +599,9 @@ fn beforeMove(
588
599
  const def = foe.active.stats.def;
589
600
  foe.active.stats.def = active.stats.def;
590
601
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
602
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
603
+ return .err;
604
+ }
592
605
  }
593
606
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
607
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +630,10 @@ fn beforeMove(
617
630
  volatiles.Thrashing = false;
618
631
  volatiles.Charging = false;
619
632
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
633
+
634
+ options.chance.observe(.attacking, player, .None);
635
+ options.chance.observe(.binding, player, .None);
636
+
621
637
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
638
  try log.cant(.{ ident, .Paralysis });
623
639
  return .done;
@@ -635,12 +651,8 @@ fn beforeMove(
635
651
  volatiles.state +%= battle.last_damage;
636
652
  }
637
653
 
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);
654
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
655
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
656
 
645
657
  if (volatiles.attacks != 0) {
646
658
  try log.activate(.{ ident, .Bide });
@@ -674,35 +686,40 @@ fn beforeMove(
674
686
  }
675
687
 
676
688
  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);
689
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
683
690
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
691
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
692
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
693
+
694
+ if (volatiles.attacks == 0) {
695
+ const overwritten = volatiles.Confusion;
696
+
697
+ volatiles.Thrashing = false;
698
+ volatiles.Confusion = true;
699
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
700
+
701
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
702
+ // ended (the engine's protocol is always from the omniscient perspective and clients
703
+ // can be expected to recognize this pattern and pass the information only to the
704
+ // correct player)
685
705
  try log.start(.{ battle.active(player), .ConfusionSilent });
706
+ options.chance.observe(.confusion, player, if (overwritten)
707
+ if (pkmn.options.overwrite) .overwritten else .continuing
708
+ else
709
+ .started);
710
+ if (!overwritten) options.calc.confusion(player);
686
711
  }
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
- }
712
+
713
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
714
+ // used in useMove and doesn't have the notion of skipping canMove semantics
715
+ if (showdown) side.last_used_move = side.last_selected_move;
716
+
696
717
  return .skip_can;
697
718
  }
698
719
 
699
720
  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);
721
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
722
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
723
 
707
724
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
725
  if (showdown or battle.last_damage != 0) {
@@ -795,7 +812,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
812
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
813
  }
797
814
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
815
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
816
  var active = &side.active;
800
817
  const volatiles = &active.volatiles;
801
818
 
@@ -803,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
820
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
821
  if (showdown and volatiles.Transform) return;
805
822
 
823
+ const n = options.calc.overridden(player, .pp) orelse 1;
824
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
825
+
806
826
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
827
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
828
  }
809
829
 
810
830
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,13 +872,13 @@ fn doMove(
852
872
  break :miss true;
853
873
  }
854
874
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
875
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
876
  };
857
877
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
878
 
859
879
  var late = showdown and move.effect != .Explode;
860
880
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
881
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
882
  if (showdown and move.effect.isMulti()) {
863
883
  try Effects.multiHit(battle, player, move, options);
864
884
  hits = side.active.volatiles.attacks;
@@ -880,8 +900,9 @@ fn doMove(
880
900
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
901
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
902
  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)) {
903
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
904
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
905
+ if (!showdown) try options.chance.commit(player, .err);
885
906
  return @as(?Result, Result.Error);
886
907
  }
887
908
  if (battle.last_damage == 0) {
@@ -895,11 +916,12 @@ fn doMove(
895
916
  }
896
917
  }
897
918
 
919
+ var missed: bool = undefined;
898
920
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
921
+ if (!showdown and !skip) {
922
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
923
+ miss = missed or zero;
924
+ }
903
925
 
904
926
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
927
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +948,32 @@ fn doMove(
926
948
  try log.immune(.{ foe_ident, .OHKO });
927
949
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
950
  try log.immune(.{ foe_ident, .None });
951
+ if (!showdown) {
952
+ if (move.effect == .Binding) {
953
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
954
+ // then simply clears it later on if the move happens to miss in moveHit.
955
+ // However, moveHit can return true and we'll still end up in the outer miss
956
+ // handling block because attacking into an immune Pokemon does zero damage
957
+ // (considered missing)
958
+ assert(zero);
959
+ // Thus we still need to actually save the hit result (but not the crit roll).
960
+ // Using .miss for this commit would accomplish what we want, but is somewhat
961
+ // misleading as its entirely possible the move actually did hit (and the target
962
+ // is now trapped) so we use the .binding value instead which is effectively
963
+ // identical, just doesn't claim to know about the actual results of the hit
964
+ // roll)
965
+ try options.chance.commit(player, .binding);
966
+ } else {
967
+ // The .glitch option allows for conditionally committing if we are in a
968
+ // division-by-zero freeze alternative timeline scenario
969
+ try options.chance.commit(player, .glitch);
970
+ }
971
+ }
929
972
  } else if (mist) {
930
973
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
974
  try log.fail(.{ foe_ident, .None });
932
975
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
976
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
977
  try log.lastmiss(.{});
935
978
  try log.miss(.{battle.active(player)});
936
979
  }
@@ -945,17 +988,15 @@ fn doMove(
945
988
  try buildRage(battle, player.foe(), options);
946
989
  } else if (showdown and move.effect == .Disable) {
947
990
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
991
  }
951
992
  return null;
952
- } else {
993
+ } else if (!showdown) {
953
994
  try options.chance.commit(player, .hit);
954
995
  }
955
996
 
956
997
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
998
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
999
+ // it up above before damage calculation)
959
1000
  if (!showdown and move.effect.isMulti()) {
960
1001
  try Effects.multiHit(battle, player, move, options);
961
1002
  hits = side.active.volatiles.attacks;
@@ -994,32 +1035,30 @@ fn doMove(
994
1035
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1036
  }
996
1037
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1038
+ } else if (showdown and move.effect == .Binding) {
998
1039
  // 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;
1040
+ try Effects.binding(battle, player, rewrap, options);
1041
+ if (immune) {
1042
+ battle.last_damage = 0;
1043
+ assert(foe.stored().hp > 0);
1044
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1045
+ if (sub) {
1046
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1047
+ } else {
1048
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1049
+ try buildRage(battle, player.foe(), options);
1012
1050
  }
1051
+ return null;
1013
1052
  }
1014
1053
  }
1015
1054
 
1016
1055
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1056
+ // if an effect was intended to "always happen" it will still get skipped
1018
1057
  if (nullified) return null;
1019
1058
 
1020
1059
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1060
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1061
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1062
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1063
 
1025
1064
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1073,7 @@ fn doMove(
1034
1073
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1074
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1075
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1076
+ // puts the |-status| message before |-hitcount| instead of after
1038
1077
  if (move.effect == .Twineedle) {
1039
1078
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1079
  } else if (move.effect == .Disable) {
@@ -1058,7 +1097,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1097
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1098
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1099
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1100
+ const spurious =
1101
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1102
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1103
  }
1063
1104
 
1064
1105
  fn calcDamage(
@@ -1067,6 +1108,7 @@ fn calcDamage(
1067
1108
  target_player: Player,
1068
1109
  m: ?Move.Data,
1069
1110
  crit: bool,
1111
+ options: anytype,
1070
1112
  ) bool {
1071
1113
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1114
  const cfz = m == null;
@@ -1078,28 +1120,8 @@ fn calcDamage(
1078
1120
 
1079
1121
  const special = move.type.special();
1080
1122
 
1081
- // zig fmt: off
1082
- var atk: u32 =
1083
- if (crit)
1084
- if (special) side.stored().stats.spc
1085
- else side.stored().stats.atk
1086
- else
1087
- if (special) side.active.stats.spc
1088
- else side.active.stats.atk;
1089
- var def: u32 =
1090
- if (crit)
1091
- if (special) target.stored().stats.spc
1092
- else target.stored().stats.def
1093
- else
1094
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1095
- if (special)
1096
- target.active.stats.spc *
1097
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1098
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1099
- else
1100
- target.active.stats.def *
1101
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1102
- // zig fmt: on
1123
+ var atk = attack(side, crit, special);
1124
+ var def = defense(target, crit, special, cfz);
1103
1125
 
1104
1126
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1127
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1124,11 +1146,41 @@ fn calcDamage(
1124
1146
  d = @min(997, d);
1125
1147
  d += 2;
1126
1148
 
1149
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1150
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1151
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1152
+ // that results in counter intuitive probabilities in the common case
1153
+ if (pkmn.options.chance and !showdown) {
1154
+ atk = attack(side, !crit, special);
1155
+ def = defense(target, !crit, special, false);
1156
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1157
+ }
1158
+
1127
1159
  battle.last_damage = @intCast(d);
1128
1160
 
1129
1161
  return true;
1130
1162
  }
1131
1163
 
1164
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1165
+ return if (crit)
1166
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1167
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1168
+ }
1169
+
1170
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1171
+ return if (crit)
1172
+ if (special) target.stored().stats.spc else target.stored().stats.def
1173
+ else
1174
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1175
+ if (special)
1176
+ target.active.stats.spc *
1177
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1178
+ else
1179
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1180
+ target.active.stats.def *
1181
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1182
+ }
1183
+
1132
1184
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1185
  const side = battle.side(player);
1134
1186
  const foe = battle.foe(player);
@@ -1173,7 +1225,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1225
  const foe = battle.foe(player);
1174
1226
 
1175
1227
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1228
 
1178
1229
  battle.last_damage = switch (side.last_selected_move) {
1179
1230
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1285,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1285
 
1235
1286
  battle.last_damage *|= 2;
1236
1287
 
1237
- // Pokémon Showdown calls checkHit before Counter
1288
+ // Pokémon Showdown calls moveHit before Counter
1238
1289
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1290
 
1241
1291
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1292
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1315,7 @@ fn applyDamage(
1265
1315
  subbed.active.volatiles.Substitute = false;
1266
1316
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1317
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1318
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1319
  return true;
1270
1320
  } else {
1271
1321
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1349,7 @@ fn mirrorMove(
1299
1349
  try options.log.fail(.{ battle.active(player), .None });
1300
1350
  return null;
1301
1351
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1352
+ incrementPP(side, player, mslot, options);
1303
1353
  }
1304
1354
 
1305
1355
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1370,7 @@ fn metronome(
1320
1370
  var side = battle.side(player);
1321
1371
 
1322
1372
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1373
+ incrementPP(side, player, mslot, options);
1324
1374
 
1325
1375
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1376
  return null;
@@ -1332,7 +1382,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1382
  var immune = false;
1333
1383
  var mist = false;
1334
1384
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1385
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1386
+ if (!showdown) try options.chance.commit(player, .hit);
1387
+ return true;
1388
+ }
1336
1389
 
1337
1390
  assert(!immune);
1338
1391
  if (mist) {
@@ -1341,11 +1394,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1394
  try options.log.activate(.{ foe_ident, .Mist });
1342
1395
  try options.log.fail(.{ foe_ident, .None });
1343
1396
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1397
  try options.log.lastmiss(.{});
1346
1398
  try options.log.miss(.{battle.active(player)});
1347
1399
  }
1348
1400
 
1401
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1402
  return false;
1350
1403
  }
1351
1404
 
@@ -1356,7 +1409,7 @@ fn moveHit(
1356
1409
  immune: *bool,
1357
1410
  mist: *bool,
1358
1411
  options: anytype,
1359
- ) bool {
1412
+ ) !bool {
1360
1413
  var side = battle.side(player);
1361
1414
  const foe = battle.foe(player);
1362
1415
 
@@ -1407,7 +1460,7 @@ fn moveHit(
1407
1460
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1461
 
1409
1462
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1463
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1464
  };
1412
1465
 
1413
1466
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1471,17 @@ fn moveHit(
1418
1471
 
1419
1472
  if (!miss) return true;
1420
1473
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1474
+
1421
1475
  side.active.volatiles.Binding = false;
1476
+ options.chance.observe(.binding, player, .None);
1477
+
1422
1478
  return false;
1423
1479
  }
1424
1480
 
1425
1481
  fn checkFaint(
1426
1482
  battle: anytype,
1427
1483
  player: Player,
1484
+ cap: bool,
1428
1485
  options: anytype,
1429
1486
  ) @TypeOf(options.log).Error!?Result {
1430
1487
  const side = battle.side(player);
@@ -1437,8 +1494,8 @@ fn checkFaint(
1437
1494
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1495
  const more = player_out or foe_out;
1439
1496
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1497
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1498
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1499
 
1443
1500
  assert(!side.active.volatiles.MultiHit);
1444
1501
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1516,7 @@ fn checkFaint(
1459
1516
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1517
  }
1461
1518
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1519
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1520
  var side = battle.side(player);
1464
1521
  var foe = battle.foe(player);
1465
1522
  assert(side.stored().hp == 0);
@@ -1493,7 +1550,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1550
  }
1494
1551
 
1495
1552
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1553
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1554
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1555
+ if (cap) options.calc.capped(player);
1497
1556
  return null;
1498
1557
  }
1499
1558
 
@@ -1653,17 +1712,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1712
  }
1654
1713
  }
1655
1714
 
1656
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1657
- var volatiles = &active.volatiles;
1658
- assert(volatiles.Thrashing);
1659
- if (volatiles.attacks > 0) return false;
1660
-
1661
- volatiles.Thrashing = false;
1662
- volatiles.Confusion = true;
1663
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1664
- return true;
1665
- }
1666
-
1667
1715
  fn onBegin(
1668
1716
  battle: anytype,
1669
1717
  player: Player,
@@ -1750,8 +1798,10 @@ pub const Effects = struct {
1750
1798
 
1751
1799
  side.active.volatiles.Bide = true;
1752
1800
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1801
+
1753
1802
  side.active.volatiles.state = 0;
1754
1803
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1804
+ options.chance.observe(.attacking, player, .started);
1755
1805
 
1756
1806
  try options.log.start(.{ battle.active(player), .Bide });
1757
1807
  }
@@ -1813,12 +1863,14 @@ pub const Effects = struct {
1813
1863
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1864
  if (!try checkHit(battle, player, move, options)) return;
1815
1865
  }
1816
- try options.chance.commit(player, .hit);
1817
1866
  }
1818
1867
 
1819
1868
  if (foe.active.volatiles.Confusion) return;
1820
1869
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1870
+
1871
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1872
+ options.chance.observe(.confusion, player.foe(), .started);
1873
+ options.calc.confusion(player.foe());
1822
1874
 
1823
1875
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1876
  }
@@ -1846,10 +1898,7 @@ pub const Effects = struct {
1846
1898
  const foe_ident = battle.active(player.foe());
1847
1899
 
1848
1900
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1901
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1902
 
1854
1903
  if (volatiles.disable_move != 0) {
1855
1904
  try options.log.fail(.{ foe_ident, .None });
@@ -1879,6 +1928,7 @@ pub const Effects = struct {
1879
1928
  volatiles.disable_move =
1880
1929
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1930
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1931
+ options.chance.observe(.disable, player.foe(), .started);
1882
1932
 
1883
1933
  const id = foe.active.move(volatiles.disable_move).id;
1884
1934
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1985,22 +2035,24 @@ pub const Effects = struct {
1985
2035
  if (p != player and Status.any(s.stored().status)) {
1986
2036
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2037
  s.stored().status = 0;
2038
+ options.chance.observe(.sleep, p, .None);
1988
2039
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2040
  s.stored().status = Status.init(.PSN);
1990
2041
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2042
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2043
+ try clearVolatiles(battle, p, options);
1993
2044
  }
1994
2045
  } else {
1995
2046
  if (Status.any(foe_stored.status)) {
1996
2047
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2048
  foe.last_selected_move = .SKIP_TURN;
2049
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2050
  }
1999
2051
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2052
  foe_stored.status = 0;
2001
2053
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2054
+ try clearVolatiles(battle, player, options);
2055
+ try clearVolatiles(battle, player.foe(), options);
2004
2056
  }
2005
2057
  }
2006
2058
 
@@ -2051,7 +2103,6 @@ pub const Effects = struct {
2051
2103
  }
2052
2104
  }
2053
2105
 
2054
- try options.chance.commit(player, .hit);
2055
2106
  foe.active.volatiles.LeechSeed = true;
2056
2107
 
2057
2108
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2143,10 @@ pub const Effects = struct {
2092
2143
  if (!has_mimic) {
2093
2144
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2145
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2146
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2147
  }
2098
2148
  }
2099
2149
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2150
 
2102
2151
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2152
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2200,6 @@ pub const Effects = struct {
2151
2200
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2201
  if (!try checkHit(battle, player, move, options)) return;
2153
2202
  }
2154
- try options.chance.commit(player, .hit);
2155
2203
 
2156
2204
  foe_stored.status = Status.init(.PAR);
2157
2205
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2267,6 @@ pub const Effects = struct {
2219
2267
 
2220
2268
  if (move.effect == .Poison) {
2221
2269
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2270
  } else {
2224
2271
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2272
  return;
@@ -2288,9 +2335,8 @@ pub const Effects = struct {
2288
2335
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2336
  });
2290
2337
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2338
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2339
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2340
  }
2295
2341
 
2296
2342
  // Sleep Clause Mod
@@ -2302,6 +2348,8 @@ pub const Effects = struct {
2302
2348
  foe.active.volatiles.Recharging = false;
2303
2349
 
2304
2350
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2351
+ options.chance.observe(.sleep, player.foe(), .started);
2352
+
2305
2353
  const last = battle.side(player).last_selected_move;
2306
2354
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2355
  }
@@ -2343,9 +2391,7 @@ pub const Effects = struct {
2343
2391
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2392
  if (showdown) {
2345
2393
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2394
+ _ = try checkHit(battle, player, move, options);
2349
2395
  battle.last_damage = 0;
2350
2396
  } else {
2351
2397
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2406,8 @@ pub const Effects = struct {
2360
2406
 
2361
2407
  volatiles.Thrashing = true;
2362
2408
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2409
+
2410
+ options.chance.observe(.attacking, player, .started);
2363
2411
  }
2364
2412
 
2365
2413
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2405,6 +2453,7 @@ pub const Effects = struct {
2405
2453
 
2406
2454
  side.active.volatiles.attacks =
2407
2455
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2456
+ options.chance.observe(.binding, player, .started);
2408
2457
  }
2409
2458
 
2410
2459
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2570,6 @@ pub const Effects = struct {
2521
2570
  } else {
2522
2571
  // checkHit already checks for Invulnerable
2523
2572
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2573
  }
2526
2574
 
2527
2575
  var stats = &foe.active.stats;
@@ -2635,22 +2683,22 @@ fn isForced(active: anytype) bool {
2635
2683
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2684
  }
2637
2685
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2686
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2687
  var log = options.log;
2640
2688
  var side = battle.side(who);
2641
2689
  var volatiles = &side.active.volatiles;
2642
2690
  const ident = battle.active(who);
2643
2691
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2692
  if (volatiles.disable_move != 0) {
2647
2693
  volatiles.disable_move = 0;
2648
2694
  volatiles.disable_duration = 0;
2695
+ options.chance.observe(.disable, who, .None);
2649
2696
  try log.end(.{ ident, .DisableSilent });
2650
2697
  }
2651
2698
  if (volatiles.Confusion) {
2652
2699
  // volatiles.confusion is left unchanged
2653
2700
  volatiles.Confusion = false;
2701
+ options.chance.observe(.confusion, who, .None);
2654
2702
  try log.end(.{ ident, .ConfusionSilent });
2655
2703
  }
2656
2704
  if (volatiles.Mist) {
@@ -2685,10 +2733,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2733
  }
2686
2734
  }
2687
2735
 
2736
+ fn decrement(
2737
+ comptime field: chance.Action.Field,
2738
+ player: Player,
2739
+ options: anytype,
2740
+ n: anytype,
2741
+ ) @TypeOf(n) {
2742
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2743
+ .started, .ended => 0,
2744
+ .continuing => if (n > 1) n - 1 else n,
2745
+ else => unreachable,
2746
+ } else n - 1;
2747
+ }
2748
+
2688
2749
  pub const Rolls = struct {
2689
2750
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2751
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2691
2752
  player == .P1
2753
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2754
+ battle.rng.speedTie()
2692
2755
  else if (showdown)
2693
2756
  battle.rng.range(u8, 0, 2) == 0
2694
2757
  else
@@ -2698,21 +2761,31 @@ pub const Rolls = struct {
2698
2761
  return p1;
2699
2762
  }
2700
2763
 
2701
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2764
+ fn criticalHit(
2765
+ battle: anytype,
2766
+ player: Player,
2767
+ rate: u8,
2768
+ spurious: bool,
2769
+ options: anytype,
2770
+ ) !bool {
2702
2771
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2703
2772
  val == .true
2773
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2774
+ battle.rng.criticalHit(player, rate)
2704
2775
  else if (showdown)
2705
2776
  battle.rng.chance(u8, rate, 256)
2706
2777
  else
2707
2778
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2708
2779
 
2709
- try options.chance.criticalHit(player, crit, rate);
2780
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2710
2781
  return crit;
2711
2782
  }
2712
2783
 
2713
2784
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2714
2785
  const roll = if (options.calc.overridden(player, .damage)) |val|
2715
2786
  val
2787
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2788
+ battle.rng.damage(player)
2716
2789
  else roll: {
2717
2790
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2718
2791
  while (true) {
@@ -2726,21 +2799,27 @@ pub const Rolls = struct {
2726
2799
  return roll;
2727
2800
  }
2728
2801
 
2729
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2802
+ const uN = if (pkmn.options.miss) u8 else u9;
2803
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2804
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2730
2805
  const ok = if (options.calc.overridden(player, .hit)) |val|
2731
2806
  val == .true
2807
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2808
+ battle.rng.hit(player, acc)
2732
2809
  else if (showdown)
2733
- battle.rng.chance(u8, accuracy, 256)
2810
+ battle.rng.chance(uN, acc, 256)
2734
2811
  else
2735
- battle.rng.next() < accuracy;
2812
+ battle.rng.next() < acc;
2736
2813
 
2737
- options.chance.hit(ok, accuracy);
2814
+ try options.chance.hit(player, ok, acc);
2738
2815
  return ok;
2739
2816
  }
2740
2817
 
2741
2818
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2742
2819
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2743
2820
  val == .true
2821
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2822
+ battle.rng.confused(player)
2744
2823
  else if (showdown)
2745
2824
  !battle.rng.chance(u8, 128, 256)
2746
2825
  else
@@ -2753,6 +2832,8 @@ pub const Rolls = struct {
2753
2832
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2754
2833
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2755
2834
  val == .true
2835
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2836
+ battle.rng.paralyzed(player)
2756
2837
  else if (showdown)
2757
2838
  battle.rng.chance(u8, 63, 256)
2758
2839
  else
@@ -2765,6 +2846,8 @@ pub const Rolls = struct {
2765
2846
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2766
2847
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2767
2848
  val == .true
2849
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2850
+ battle.rng.secondaryChance(player, 25)
2768
2851
  else if (showdown)
2769
2852
  battle.rng.chance(u8, 25, 256)
2770
2853
  else
@@ -2779,6 +2862,8 @@ pub const Rolls = struct {
2779
2862
 
2780
2863
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2864
  val == .true
2865
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2866
+ battle.rng.secondaryChance(player, rate)
2782
2867
  else if (showdown)
2783
2868
  battle.rng.chance(u8, rate, 256)
2784
2869
  else
@@ -2793,6 +2878,8 @@ pub const Rolls = struct {
2793
2878
 
2794
2879
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2880
  val == .true
2881
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2882
+ battle.rng.secondaryChance(player, rate)
2796
2883
  else if (showdown)
2797
2884
  battle.rng.chance(u8, rate, 256)
2798
2885
  else
@@ -2805,6 +2892,8 @@ pub const Rolls = struct {
2805
2892
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2806
2893
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2807
2894
  val == .true
2895
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2896
+ battle.rng.secondaryChance(player, 85)
2808
2897
  else if (showdown)
2809
2898
  battle.rng.chance(u8, 85, 256)
2810
2899
  else
@@ -2817,6 +2906,8 @@ pub const Rolls = struct {
2817
2906
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2818
2907
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2819
2908
  val
2909
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2910
+ battle.rng.metronome(player)
2820
2911
  else if (showdown)
2821
2912
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2822
2913
  else move: {
@@ -2835,6 +2926,8 @@ pub const Rolls = struct {
2835
2926
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2836
2927
  const power = if (options.calc.overridden(player, .psywave)) |val|
2837
2928
  val - 1
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2930
+ battle.rng.psywave(player)
2838
2931
  else if (showdown)
2839
2932
  battle.rng.range(u8, 0, max)
2840
2933
  else power: {
@@ -2850,8 +2943,11 @@ pub const Rolls = struct {
2850
2943
  }
2851
2944
 
2852
2945
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2853
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2854
- @intCast(val)
2946
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2947
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2948
+ else
2949
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2950
+ battle.rng.sleepDuration(player)
2855
2951
  else if (showdown)
2856
2952
  battle.rng.range(u3, 1, 8)
2857
2953
  else duration: {
@@ -2862,46 +2958,55 @@ pub const Rolls = struct {
2862
2958
  };
2863
2959
 
2864
2960
  assert(duration >= 1 and duration <= 7);
2865
- options.chance.duration(.sleep, player, player.foe(), duration);
2961
+ options.chance.duration(player, duration);
2866
2962
  return duration;
2867
2963
  }
2868
2964
 
2869
2965
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2870
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2871
- val
2966
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2967
+ if (val >= 1 and val <= 8) val else null
2968
+ else
2969
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2970
+ battle.rng.disableDuration(player)
2872
2971
  else if (showdown)
2873
2972
  battle.rng.range(u4, 1, 9)
2874
2973
  else
2875
2974
  @intCast((battle.rng.next() & 7) + 1);
2876
2975
 
2877
2976
  assert(duration >= 1 and duration <= 8);
2878
- options.chance.duration(.disable, player, player.foe(), duration);
2977
+ options.chance.duration(player, duration);
2879
2978
  return duration;
2880
2979
  }
2881
2980
 
2882
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2883
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2884
- @intCast(val)
2981
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2982
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2986
+ battle.rng.confusionDuration(player)
2885
2987
  else if (showdown)
2886
2988
  battle.rng.range(u3, 2, 6)
2887
2989
  else
2888
2990
  @intCast((battle.rng.next() & 3) + 2);
2889
2991
 
2890
2992
  assert(duration >= 2 and duration <= 5);
2891
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2993
+ options.chance.duration(player, duration);
2892
2994
  return duration;
2893
2995
  }
2894
2996
 
2895
2997
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2896
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2897
- @intCast(val)
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3002
+ battle.rng.attackingDuration(player)
2898
3003
  else if (showdown)
2899
3004
  battle.rng.range(u3, 2, 4)
2900
3005
  else
2901
3006
  @intCast((battle.rng.next() & 1) + 2);
2902
3007
 
2903
3008
  assert(duration >= 2 and duration <= 3);
2904
- options.chance.duration(.attacking, player, player, duration);
3009
+ options.chance.duration(player, duration);
2905
3010
  return duration;
2906
3011
  }
2907
3012
 
@@ -2914,8 +3019,11 @@ pub const Rolls = struct {
2914
3019
  options: anytype,
2915
3020
  ) !u3 {
2916
3021
  const roll = if (effect == .Binding) .duration else .multi_hit;
2917
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2918
- @intCast(val)
3022
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3023
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3024
+ else
3025
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3026
+ battle.rng.distribution(player)
2919
3027
  else if (showdown)
2920
3028
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2921
3029
  else n: {
@@ -2925,7 +3033,7 @@ pub const Rolls = struct {
2925
3033
 
2926
3034
  assert(n >= 2 and n <= 5);
2927
3035
  if (effect == .Binding) {
2928
- options.chance.duration(.binding, player, player, n);
3036
+ options.chance.duration(player, n);
2929
3037
  } else {
2930
3038
  try options.chance.multiHit(player, n);
2931
3039
  }
@@ -2940,15 +3048,15 @@ pub const Rolls = struct {
2940
3048
  options: anytype,
2941
3049
  ) !u4 {
2942
3050
  // TODO: consider throwing error instead of rerolling?
2943
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3051
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2944
3052
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2945
3053
  val
2946
3054
  else
2947
3055
  null
2948
3056
  else
2949
- null;
2950
-
2951
- const slot: u4 = overridden orelse slot: {
3057
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3058
+ battle.rng.moveSlot(player, moves, check_pp)
3059
+ else slot: {
2952
3060
  if (showdown) {
2953
3061
  if (check_pp == 0) {
2954
3062
  var i: usize = moves.len;
@@ -3032,7 +3140,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3032
3140
  const stored = side.stored();
3033
3141
 
3034
3142
  // While players are not given any input options on the cartridge in these cases,
3035
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3143
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3036
3144
  //
3037
3145
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3038
3146
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3040,7 +3148,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3040
3148
  // passing due to being forced into using a move. Instead of introducing another option
3041
3149
  // we simply repurpose Move with no move slot, even though pedantically this is not
3042
3150
  // strictly correct as the player would not have been presented the option to move or
3043
- // switch at all.
3151
+ // switch at all
3044
3152
  if (isForced(active)) {
3045
3153
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3046
3154
  n += 1;
@@ -3059,7 +3167,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3059
3167
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3060
3168
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3061
3169
  // either disables all other moves in the case of limited or requires you to select a
3062
- // move normally if sleeping/frozen/bound.
3170
+ // move normally if sleeping/frozen/bound
3063
3171
  if (!showdown and (limited or foe.active.volatiles.Binding or
3064
3172
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3065
3173
  {
@@ -3072,7 +3180,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3072
3180
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3073
3181
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3074
3182
  // Move will not result in forcing the subsequent use unless the user also had the
3075
- // proc-ed move in their moveset) and disabling all other moves.
3183
+ // proc-ed move in their moveset) and disabling all other moves
3076
3184
  if (limited) {
3077
3185
  assert(showdown);
3078
3186
  assert(side.last_selected_move != .None);