@pkmn/engine 0.1.0-dev.eaae4aa6 → 0.1.0-dev.f3aab252

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