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

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 (40) 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 +189 -72
  15. package/package.json +17 -16
  16. package/src/bin/install-pkmn-engine +17 -7
  17. package/src/lib/common/data.zig +8 -8
  18. package/src/lib/common/debug.zig +2 -2
  19. package/src/lib/common/js.zig +4 -2
  20. package/src/lib/common/optional.zig +13 -9
  21. package/src/lib/common/options.zig +2 -0
  22. package/src/lib/common/protocol.zig +51 -44
  23. package/src/lib/common/rational.zig +114 -24
  24. package/src/lib/common/rng.zig +10 -10
  25. package/src/lib/common/util.zig +50 -1
  26. package/src/lib/gen1/calc.zig +508 -128
  27. package/src/lib/gen1/chance.zig +383 -276
  28. package/src/lib/gen1/data/species.zig +1 -1
  29. package/src/lib/gen1/data.zig +29 -28
  30. package/src/lib/gen1/helpers.zig +96 -536
  31. package/src/lib/gen1/mechanics.zig +266 -160
  32. package/src/lib/gen2/calc.zig +34 -16
  33. package/src/lib/gen2/chance.zig +91 -500
  34. package/src/lib/gen2/data.zig +15 -14
  35. package/src/lib/gen2/helpers.zig +41 -9
  36. package/src/lib/gen2/mechanics.zig +158 -97
  37. package/src/lib/pkmn.zig +10 -16
  38. package/src/test/display/pkmn.html.tmpl +5 -5
  39. package/src/lib/gen1/README.md +0 -894
  40. package/src/lib/gen2/README.md +0 -329
@@ -4,10 +4,12 @@ const pkmn = @import("../pkmn.zig");
4
4
 
5
5
  const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
+ const optional = @import("../common/optional.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
10
 
10
11
  const data = @import("data.zig");
12
+ const chance = @import("chance.zig");
11
13
 
12
14
  const assert = std.debug.assert;
13
15
 
@@ -166,7 +168,7 @@ fn selectMove(
166
168
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
169
  // set the selected move will not actually be used, it will just be reported as having
168
170
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
171
+ // replicate the incorrect behavior with the correct mechanisms)
170
172
  saveMove(battle, player, choice);
171
173
  } else {
172
174
  assert(choice.data == 0);
@@ -253,11 +255,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
255
  active.moves = incoming.moves;
254
256
 
255
257
  statusModify(incoming.status, &active.stats);
258
+ options.chance.switched(player, slot);
256
259
 
257
260
  foe.active.volatiles.Binding = false;
258
261
 
259
262
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
263
 
262
264
  if (showdown and incoming.status == Status.TOX) {
263
265
  incoming.status = Status.init(.PSN);
@@ -347,13 +349,13 @@ fn doTurn(
347
349
  )) |r| return r;
348
350
  if (!replace) {
349
351
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
352
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
353
  }
352
354
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
355
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
356
  } else if (foe_choice.type == .Pass) return null;
355
357
 
356
- options.chance.clearPending();
358
+ if (!showdown) options.chance.clearPending();
357
359
 
358
360
  residual = true;
359
361
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +371,10 @@ fn doTurn(
369
371
  )) |r| return r;
370
372
  if (!replace) {
371
373
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
374
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
375
  }
374
376
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
377
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
378
  }
377
379
 
378
380
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +433,7 @@ fn executeMove(
431
433
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
435
  // 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.
436
+ // index 1 and the current Pokémon has the same move in its first slot
435
437
  if (!auto) {
436
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -484,17 +486,22 @@ fn beforeMove(
484
486
 
485
487
  if (Status.is(stored.status, .SLP)) {
486
488
  const before = stored.status;
489
+ const slf = Status.is(stored.status, .EXT);
487
490
  // 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;
491
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
492
+ .started, .ended => stored.status = 0,
493
+ .continuing => if (Status.duration(stored.status) > 1) {
494
+ stored.status -= 1;
495
+ },
496
+ else => unreachable,
490
497
  } else {
491
498
  stored.status -= 1;
492
499
  }
493
500
 
494
501
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
502
+ try options.chance.sleep(player, if (slf)
503
+ .None
504
+ else if (duration == 0) .ended else .continuing);
498
505
 
499
506
  if (duration == 0) {
500
507
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +539,12 @@ fn beforeMove(
532
539
  }
533
540
 
534
541
  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);
542
+ volatiles.disable_duration =
543
+ decrement(.disable, player, options, volatiles.disable_duration);
544
+ try options.chance.disable(
545
+ player,
546
+ if (volatiles.disable_duration == 0) .ended else .continuing,
547
+ );
541
548
 
542
549
  if (volatiles.disable_duration == 0) {
543
550
  volatiles.disable_move = 0;
@@ -556,13 +563,19 @@ fn beforeMove(
556
563
 
557
564
  if (volatiles.Confusion) {
558
565
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
566
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
+ .started => volatiles.confusion -= 1,
568
+ .continuing => if (volatiles.confusion > 1) {
569
+ volatiles.confusion -= 1;
570
+ },
571
+ .ended => volatiles.confusion = 0,
572
+ .overwritten => {},
573
+ else => unreachable,
562
574
  } else {
563
575
  volatiles.confusion -= 1;
564
576
  }
565
- try options.chance.confusion(player, volatiles.confusion);
577
+
578
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
579
 
567
580
  if (volatiles.confusion == 0) {
568
581
  volatiles.Confusion = false;
@@ -580,7 +593,9 @@ fn beforeMove(
580
593
  volatiles.Charging = false;
581
594
  volatiles.Binding = false;
582
595
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
596
+
597
+ options.chance.observe(.attacking, player, .None);
598
+ options.chance.observe(.binding, player, .None);
584
599
  {
585
600
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
601
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +603,9 @@ fn beforeMove(
588
603
  const def = foe.active.stats.def;
589
604
  foe.active.stats.def = active.stats.def;
590
605
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
606
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
607
+ return .err;
608
+ }
592
609
  }
593
610
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
611
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +634,10 @@ fn beforeMove(
617
634
  volatiles.Thrashing = false;
618
635
  volatiles.Charging = false;
619
636
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
637
+
638
+ options.chance.observe(.attacking, player, .None);
639
+ options.chance.observe(.binding, player, .None);
640
+
621
641
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
642
  try log.cant(.{ ident, .Paralysis });
623
643
  return .done;
@@ -635,12 +655,8 @@ fn beforeMove(
635
655
  volatiles.state +%= battle.last_damage;
636
656
  }
637
657
 
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);
658
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
659
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
660
 
645
661
  if (volatiles.attacks != 0) {
646
662
  try log.activate(.{ ident, .Bide });
@@ -674,35 +690,36 @@ fn beforeMove(
674
690
  }
675
691
 
676
692
  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);
693
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
694
+
695
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
696
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
697
+
698
+ if (volatiles.attacks == 0) {
699
+ const overwritten = volatiles.Confusion;
683
700
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
701
+ volatiles.Thrashing = false;
702
+ volatiles.Confusion = true;
703
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
704
+
705
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
706
+ // ended (the engine's protocol is always from the omniscient perspective and clients
707
+ // can be expected to recognize this pattern and pass the information only to the
708
+ // correct player)
685
709
  try log.start(.{ battle.active(player), .ConfusionSilent });
710
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
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;
@@ -1014,12 +1055,12 @@ fn doMove(
1014
1055
  }
1015
1056
 
1016
1057
  // 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.
1058
+ // if an effect was intended to "always happen" it will still get skipped
1018
1059
  if (nullified) return null;
1019
1060
 
1020
1061
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1062
  // 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.
1063
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1064
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1065
 
1025
1066
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1075,7 @@ fn doMove(
1034
1075
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1076
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1077
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1078
+ // puts the |-status| message before |-hitcount| instead of after
1038
1079
  if (move.effect == .Twineedle) {
1039
1080
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1081
  } else if (move.effect == .Disable) {
@@ -1058,7 +1099,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1099
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1100
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1101
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1102
+ const spurious =
1103
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1104
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1105
  }
1063
1106
 
1064
1107
  fn calcDamage(
@@ -1067,6 +1110,7 @@ fn calcDamage(
1067
1110
  target_player: Player,
1068
1111
  m: ?Move.Data,
1069
1112
  crit: bool,
1113
+ options: anytype,
1070
1114
  ) bool {
1071
1115
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1116
  const cfz = m == null;
@@ -1078,28 +1122,8 @@ fn calcDamage(
1078
1122
 
1079
1123
  const special = move.type.special();
1080
1124
 
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
1125
+ var atk = attack(side, crit, special);
1126
+ var def = defense(target, crit, special, cfz);
1103
1127
 
1104
1128
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1129
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1124,11 +1148,41 @@ fn calcDamage(
1124
1148
  d = @min(997, d);
1125
1149
  d += 2;
1126
1150
 
1151
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1152
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1153
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1154
+ // that results in counter intuitive probabilities in the common case
1155
+ if (pkmn.options.chance and !showdown) {
1156
+ atk = attack(side, !crit, special);
1157
+ def = defense(target, !crit, special, false);
1158
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1159
+ }
1160
+
1127
1161
  battle.last_damage = @intCast(d);
1128
1162
 
1129
1163
  return true;
1130
1164
  }
1131
1165
 
1166
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1167
+ return if (crit)
1168
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1169
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1170
+ }
1171
+
1172
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1173
+ return if (crit)
1174
+ if (special) target.stored().stats.spc else target.stored().stats.def
1175
+ else
1176
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1177
+ if (special)
1178
+ target.active.stats.spc *
1179
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1180
+ else
1181
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1182
+ target.active.stats.def *
1183
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1184
+ }
1185
+
1132
1186
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1187
  const side = battle.side(player);
1134
1188
  const foe = battle.foe(player);
@@ -1173,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1227
  const foe = battle.foe(player);
1174
1228
 
1175
1229
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1230
 
1178
1231
  battle.last_damage = switch (side.last_selected_move) {
1179
1232
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1287,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1287
 
1235
1288
  battle.last_damage *|= 2;
1236
1289
 
1237
- // Pokémon Showdown calls checkHit before Counter
1290
+ // Pokémon Showdown calls moveHit before Counter
1238
1291
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1292
 
1241
1293
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1294
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1317,7 @@ fn applyDamage(
1265
1317
  subbed.active.volatiles.Substitute = false;
1266
1318
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1319
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1320
+ options.calc.capped(sub_player);
1269
1321
  return true;
1270
1322
  } else {
1271
1323
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1351,7 @@ fn mirrorMove(
1299
1351
  try options.log.fail(.{ battle.active(player), .None });
1300
1352
  return null;
1301
1353
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1354
+ incrementPP(side, player, mslot, options);
1303
1355
  }
1304
1356
 
1305
1357
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1372,7 @@ fn metronome(
1320
1372
  var side = battle.side(player);
1321
1373
 
1322
1374
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1375
+ incrementPP(side, player, mslot, options);
1324
1376
 
1325
1377
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1378
  return null;
@@ -1332,7 +1384,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1384
  var immune = false;
1333
1385
  var mist = false;
1334
1386
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1387
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1388
+ if (!showdown) try options.chance.commit(player, .hit);
1389
+ return true;
1390
+ }
1336
1391
 
1337
1392
  assert(!immune);
1338
1393
  if (mist) {
@@ -1341,11 +1396,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1396
  try options.log.activate(.{ foe_ident, .Mist });
1342
1397
  try options.log.fail(.{ foe_ident, .None });
1343
1398
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1399
  try options.log.lastmiss(.{});
1346
1400
  try options.log.miss(.{battle.active(player)});
1347
1401
  }
1348
1402
 
1403
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1404
  return false;
1350
1405
  }
1351
1406
 
@@ -1356,7 +1411,7 @@ fn moveHit(
1356
1411
  immune: *bool,
1357
1412
  mist: *bool,
1358
1413
  options: anytype,
1359
- ) bool {
1414
+ ) !bool {
1360
1415
  var side = battle.side(player);
1361
1416
  const foe = battle.foe(player);
1362
1417
 
@@ -1407,7 +1462,7 @@ fn moveHit(
1407
1462
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1463
 
1409
1464
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1465
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1466
  };
1412
1467
 
1413
1468
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1473,17 @@ fn moveHit(
1418
1473
 
1419
1474
  if (!miss) return true;
1420
1475
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1476
+
1421
1477
  side.active.volatiles.Binding = false;
1478
+ options.chance.observe(.binding, player, .None);
1479
+
1422
1480
  return false;
1423
1481
  }
1424
1482
 
1425
1483
  fn checkFaint(
1426
1484
  battle: anytype,
1427
1485
  player: Player,
1486
+ cap: bool,
1428
1487
  options: anytype,
1429
1488
  ) @TypeOf(options.log).Error!?Result {
1430
1489
  const side = battle.side(player);
@@ -1437,8 +1496,8 @@ fn checkFaint(
1437
1496
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1497
  const more = player_out or foe_out;
1439
1498
 
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;
1499
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1500
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1501
 
1443
1502
  assert(!side.active.volatiles.MultiHit);
1444
1503
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1518,7 @@ fn checkFaint(
1459
1518
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1519
  }
1461
1520
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1521
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1522
  var side = battle.side(player);
1464
1523
  var foe = battle.foe(player);
1465
1524
  assert(side.stored().hp == 0);
@@ -1493,7 +1552,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1552
  }
1494
1553
 
1495
1554
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1555
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1556
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1557
+ if (cap) options.calc.capped(player);
1497
1558
  return null;
1498
1559
  }
1499
1560
 
@@ -1653,17 +1714,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1714
  }
1654
1715
  }
1655
1716
 
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
1717
  fn onBegin(
1668
1718
  battle: anytype,
1669
1719
  player: Player,
@@ -1750,8 +1800,10 @@ pub const Effects = struct {
1750
1800
 
1751
1801
  side.active.volatiles.Bide = true;
1752
1802
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1803
+
1753
1804
  side.active.volatiles.state = 0;
1754
1805
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1806
+ options.chance.observe(.attacking, player, .started);
1755
1807
 
1756
1808
  try options.log.start(.{ battle.active(player), .Bide });
1757
1809
  }
@@ -1813,12 +1865,13 @@ pub const Effects = struct {
1813
1865
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1866
  if (!try checkHit(battle, player, move, options)) return;
1815
1867
  }
1816
- try options.chance.commit(player, .hit);
1817
1868
  }
1818
1869
 
1819
1870
  if (foe.active.volatiles.Confusion) return;
1820
1871
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1872
+
1873
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1874
+ options.chance.observe(.confusion, player.foe(), .started);
1822
1875
 
1823
1876
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1877
  }
@@ -1846,10 +1899,7 @@ pub const Effects = struct {
1846
1899
  const foe_ident = battle.active(player.foe());
1847
1900
 
1848
1901
  // 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
- }
1902
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1903
 
1854
1904
  if (volatiles.disable_move != 0) {
1855
1905
  try options.log.fail(.{ foe_ident, .None });
@@ -1879,6 +1929,7 @@ pub const Effects = struct {
1879
1929
  volatiles.disable_move =
1880
1930
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1931
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1932
+ options.chance.observe(.disable, player.foe(), .started);
1882
1933
 
1883
1934
  const id = foe.active.move(volatiles.disable_move).id;
1884
1935
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1985,22 +2036,24 @@ pub const Effects = struct {
1985
2036
  if (p != player and Status.any(s.stored().status)) {
1986
2037
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2038
  s.stored().status = 0;
2039
+ options.chance.observe(.sleep, p, .None);
1988
2040
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2041
  s.stored().status = Status.init(.PSN);
1990
2042
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2043
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2044
+ try clearVolatiles(battle, p, options);
1993
2045
  }
1994
2046
  } else {
1995
2047
  if (Status.any(foe_stored.status)) {
1996
2048
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2049
  foe.last_selected_move = .SKIP_TURN;
2050
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2051
  }
1999
2052
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2053
  foe_stored.status = 0;
2001
2054
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2055
+ try clearVolatiles(battle, player, options);
2056
+ try clearVolatiles(battle, player.foe(), options);
2004
2057
  }
2005
2058
  }
2006
2059
 
@@ -2051,7 +2104,6 @@ pub const Effects = struct {
2051
2104
  }
2052
2105
  }
2053
2106
 
2054
- try options.chance.commit(player, .hit);
2055
2107
  foe.active.volatiles.LeechSeed = true;
2056
2108
 
2057
2109
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2144,10 @@ pub const Effects = struct {
2092
2144
  if (!has_mimic) {
2093
2145
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2146
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2147
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2148
  }
2098
2149
  }
2099
2150
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2151
 
2102
2152
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2153
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2201,6 @@ pub const Effects = struct {
2151
2201
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2202
  if (!try checkHit(battle, player, move, options)) return;
2153
2203
  }
2154
- try options.chance.commit(player, .hit);
2155
2204
 
2156
2205
  foe_stored.status = Status.init(.PAR);
2157
2206
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2268,6 @@ pub const Effects = struct {
2219
2268
 
2220
2269
  if (move.effect == .Poison) {
2221
2270
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2271
  } else {
2224
2272
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2273
  return;
@@ -2288,9 +2336,8 @@ pub const Effects = struct {
2288
2336
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2337
  });
2290
2338
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2339
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2340
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2341
  }
2295
2342
 
2296
2343
  // Sleep Clause Mod
@@ -2302,6 +2349,8 @@ pub const Effects = struct {
2302
2349
  foe.active.volatiles.Recharging = false;
2303
2350
 
2304
2351
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2352
+ options.chance.observe(.sleep, player.foe(), .started);
2353
+
2305
2354
  const last = battle.side(player).last_selected_move;
2306
2355
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2356
  }
@@ -2343,9 +2392,7 @@ pub const Effects = struct {
2343
2392
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2393
  if (showdown) {
2345
2394
  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
- }
2395
+ _ = try checkHit(battle, player, move, options);
2349
2396
  battle.last_damage = 0;
2350
2397
  } else {
2351
2398
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2407,8 @@ pub const Effects = struct {
2360
2407
 
2361
2408
  volatiles.Thrashing = true;
2362
2409
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2410
+
2411
+ options.chance.observe(.attacking, player, .started);
2363
2412
  }
2364
2413
 
2365
2414
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2405,6 +2454,7 @@ pub const Effects = struct {
2405
2454
 
2406
2455
  side.active.volatiles.attacks =
2407
2456
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2457
+ options.chance.observe(.binding, player, .started);
2408
2458
  }
2409
2459
 
2410
2460
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2571,6 @@ pub const Effects = struct {
2521
2571
  } else {
2522
2572
  // checkHit already checks for Invulnerable
2523
2573
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2574
  }
2526
2575
 
2527
2576
  var stats = &foe.active.stats;
@@ -2635,22 +2684,22 @@ fn isForced(active: anytype) bool {
2635
2684
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2685
  }
2637
2686
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2687
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2688
  var log = options.log;
2640
2689
  var side = battle.side(who);
2641
2690
  var volatiles = &side.active.volatiles;
2642
2691
  const ident = battle.active(who);
2643
2692
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2693
  if (volatiles.disable_move != 0) {
2647
2694
  volatiles.disable_move = 0;
2648
2695
  volatiles.disable_duration = 0;
2696
+ options.chance.observe(.disable, who, .None);
2649
2697
  try log.end(.{ ident, .DisableSilent });
2650
2698
  }
2651
2699
  if (volatiles.Confusion) {
2652
2700
  // volatiles.confusion is left unchanged
2653
2701
  volatiles.Confusion = false;
2702
+ options.chance.observe(.confusion, who, .None);
2654
2703
  try log.end(.{ ident, .ConfusionSilent });
2655
2704
  }
2656
2705
  if (volatiles.Mist) {
@@ -2685,10 +2734,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2734
  }
2686
2735
  }
2687
2736
 
2737
+ fn decrement(
2738
+ comptime field: chance.Action.Field,
2739
+ player: Player,
2740
+ options: anytype,
2741
+ n: anytype,
2742
+ ) @TypeOf(n) {
2743
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2744
+ .started, .ended => 0,
2745
+ .continuing => if (n > 1) n - 1 else n,
2746
+ else => unreachable,
2747
+ } else n - 1;
2748
+ }
2749
+
2688
2750
  pub const Rolls = struct {
2689
2751
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2752
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2691
2753
  player == .P1
2754
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2755
+ battle.rng.speedTie()
2692
2756
  else if (showdown)
2693
2757
  battle.rng.range(u8, 0, 2) == 0
2694
2758
  else
@@ -2698,21 +2762,31 @@ pub const Rolls = struct {
2698
2762
  return p1;
2699
2763
  }
2700
2764
 
2701
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2765
+ fn criticalHit(
2766
+ battle: anytype,
2767
+ player: Player,
2768
+ rate: u8,
2769
+ spurious: bool,
2770
+ options: anytype,
2771
+ ) !bool {
2702
2772
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2703
2773
  val == .true
2774
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2775
+ battle.rng.criticalHit(player, rate)
2704
2776
  else if (showdown)
2705
2777
  battle.rng.chance(u8, rate, 256)
2706
2778
  else
2707
2779
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2708
2780
 
2709
- try options.chance.criticalHit(player, crit, rate);
2781
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2710
2782
  return crit;
2711
2783
  }
2712
2784
 
2713
2785
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2714
2786
  const roll = if (options.calc.overridden(player, .damage)) |val|
2715
2787
  val
2788
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2789
+ battle.rng.damage(player)
2716
2790
  else roll: {
2717
2791
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2718
2792
  while (true) {
@@ -2726,21 +2800,27 @@ pub const Rolls = struct {
2726
2800
  return roll;
2727
2801
  }
2728
2802
 
2729
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2803
+ const uN = if (pkmn.options.miss) u8 else u9;
2804
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2805
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2730
2806
  const ok = if (options.calc.overridden(player, .hit)) |val|
2731
2807
  val == .true
2808
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2809
+ battle.rng.hit(player, acc)
2732
2810
  else if (showdown)
2733
- battle.rng.chance(u8, accuracy, 256)
2811
+ battle.rng.chance(uN, acc, 256)
2734
2812
  else
2735
- battle.rng.next() < accuracy;
2813
+ battle.rng.next() < acc;
2736
2814
 
2737
- options.chance.hit(ok, accuracy);
2815
+ try options.chance.hit(player, ok, acc);
2738
2816
  return ok;
2739
2817
  }
2740
2818
 
2741
2819
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2742
2820
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2743
2821
  val == .true
2822
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2823
+ battle.rng.confused(player)
2744
2824
  else if (showdown)
2745
2825
  !battle.rng.chance(u8, 128, 256)
2746
2826
  else
@@ -2753,6 +2833,8 @@ pub const Rolls = struct {
2753
2833
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2754
2834
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2755
2835
  val == .true
2836
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2837
+ battle.rng.paralyzed(player)
2756
2838
  else if (showdown)
2757
2839
  battle.rng.chance(u8, 63, 256)
2758
2840
  else
@@ -2765,6 +2847,8 @@ pub const Rolls = struct {
2765
2847
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2766
2848
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2767
2849
  val == .true
2850
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2851
+ battle.rng.secondaryChance(player, 25)
2768
2852
  else if (showdown)
2769
2853
  battle.rng.chance(u8, 25, 256)
2770
2854
  else
@@ -2779,6 +2863,8 @@ pub const Rolls = struct {
2779
2863
 
2780
2864
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2865
  val == .true
2866
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2867
+ battle.rng.secondaryChance(player, rate)
2782
2868
  else if (showdown)
2783
2869
  battle.rng.chance(u8, rate, 256)
2784
2870
  else
@@ -2793,6 +2879,8 @@ pub const Rolls = struct {
2793
2879
 
2794
2880
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2881
  val == .true
2882
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2883
+ battle.rng.secondaryChance(player, rate)
2796
2884
  else if (showdown)
2797
2885
  battle.rng.chance(u8, rate, 256)
2798
2886
  else
@@ -2805,6 +2893,8 @@ pub const Rolls = struct {
2805
2893
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2806
2894
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2807
2895
  val == .true
2896
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2897
+ battle.rng.secondaryChance(player, 85)
2808
2898
  else if (showdown)
2809
2899
  battle.rng.chance(u8, 85, 256)
2810
2900
  else
@@ -2817,6 +2907,8 @@ pub const Rolls = struct {
2817
2907
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2818
2908
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2819
2909
  val
2910
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2911
+ battle.rng.metronome(player)
2820
2912
  else if (showdown)
2821
2913
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2822
2914
  else move: {
@@ -2835,6 +2927,8 @@ pub const Rolls = struct {
2835
2927
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2836
2928
  const power = if (options.calc.overridden(player, .psywave)) |val|
2837
2929
  val - 1
2930
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2931
+ battle.rng.psywave(player)
2838
2932
  else if (showdown)
2839
2933
  battle.rng.range(u8, 0, max)
2840
2934
  else power: {
@@ -2852,6 +2946,8 @@ pub const Rolls = struct {
2852
2946
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2853
2947
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2854
2948
  @intCast(val)
2949
+ else 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,52 @@ 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
2966
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2871
2967
  val
2968
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2969
+ battle.rng.disableDuration(player)
2872
2970
  else if (showdown)
2873
2971
  battle.rng.range(u4, 1, 9)
2874
2972
  else
2875
2973
  @intCast((battle.rng.next() & 7) + 1);
2876
2974
 
2877
2975
  assert(duration >= 1 and duration <= 8);
2878
- options.chance.duration(.disable, player, player.foe(), duration);
2976
+ options.chance.duration(player, duration);
2879
2977
  return duration;
2880
2978
  }
2881
2979
 
2882
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2980
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2883
2981
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2884
2982
  @intCast(val)
2983
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2984
+ battle.rng.confusionDuration(player)
2885
2985
  else if (showdown)
2886
2986
  battle.rng.range(u3, 2, 6)
2887
2987
  else
2888
2988
  @intCast((battle.rng.next() & 3) + 2);
2889
2989
 
2890
2990
  assert(duration >= 2 and duration <= 5);
2891
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2991
+ options.chance.duration(player, duration);
2892
2992
  return duration;
2893
2993
  }
2894
2994
 
2895
2995
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2896
2996
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2897
2997
  @intCast(val)
2998
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2999
+ battle.rng.attackingDuration(player)
2898
3000
  else if (showdown)
2899
3001
  battle.rng.range(u3, 2, 4)
2900
3002
  else
2901
3003
  @intCast((battle.rng.next() & 1) + 2);
2902
3004
 
2903
3005
  assert(duration >= 2 and duration <= 3);
2904
- options.chance.duration(.attacking, player, player, duration);
3006
+ options.chance.duration(player, duration);
2905
3007
  return duration;
2906
3008
  }
2907
3009
 
@@ -2916,6 +3018,8 @@ pub const Rolls = struct {
2916
3018
  const roll = if (effect == .Binding) .duration else .multi_hit;
2917
3019
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2918
3020
  @intCast(val)
3021
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3022
+ battle.rng.distribution(player)
2919
3023
  else if (showdown)
2920
3024
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2921
3025
  else n: {
@@ -2925,7 +3029,7 @@ pub const Rolls = struct {
2925
3029
 
2926
3030
  assert(n >= 2 and n <= 5);
2927
3031
  if (effect == .Binding) {
2928
- options.chance.duration(.binding, player, player, n);
3032
+ options.chance.duration(player, n);
2929
3033
  } else {
2930
3034
  try options.chance.multiHit(player, n);
2931
3035
  }
@@ -2948,7 +3052,9 @@ pub const Rolls = struct {
2948
3052
  else
2949
3053
  null;
2950
3054
 
2951
- const slot: u4 = overridden orelse slot: {
3055
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3056
+ battle.rng.moveSlot(player, moves, check_pp)
3057
+ else slot: {
2952
3058
  if (showdown) {
2953
3059
  if (check_pp == 0) {
2954
3060
  var i: usize = moves.len;
@@ -3032,7 +3138,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3032
3138
  const stored = side.stored();
3033
3139
 
3034
3140
  // 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.
3141
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3036
3142
  //
3037
3143
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3038
3144
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3040,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3040
3146
  // passing due to being forced into using a move. Instead of introducing another option
3041
3147
  // we simply repurpose Move with no move slot, even though pedantically this is not
3042
3148
  // strictly correct as the player would not have been presented the option to move or
3043
- // switch at all.
3149
+ // switch at all
3044
3150
  if (isForced(active)) {
3045
3151
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3046
3152
  n += 1;
@@ -3059,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3059
3165
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3060
3166
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3061
3167
  // either disables all other moves in the case of limited or requires you to select a
3062
- // move normally if sleeping/frozen/bound.
3168
+ // move normally if sleeping/frozen/bound
3063
3169
  if (!showdown and (limited or foe.active.volatiles.Binding or
3064
3170
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3065
3171
  {
@@ -3072,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3072
3178
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3073
3179
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3074
3180
  // 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.
3181
+ // proc-ed move in their moveset) and disabling all other moves
3076
3182
  if (limited) {
3077
3183
  assert(showdown);
3078
3184
  assert(side.last_selected_move != .None);