@pkmn/engine 0.1.0-dev.6c5fdce2 → 0.1.0-dev.6d566713

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 (45) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +41 -14
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -5
  6. package/build/pkg/gen1.js.map +1 -1
  7. package/build/pkg/index.d.ts +1 -1
  8. package/build/pkg/index.js.map +1 -1
  9. package/build/pkg/protocol.js +4 -4
  10. package/build/pkg/protocol.js.map +1 -1
  11. package/build/test/display.js +8 -3
  12. package/build/test/display.js.map +1 -1
  13. package/build/tools/debug.js +2 -3
  14. package/build/tools/debug.js.map +1 -1
  15. package/build.zig +203 -89
  16. package/package.json +32 -22
  17. package/src/bin/install-pkmn-engine +30 -20
  18. package/src/lib/common/array.zig +77 -0
  19. package/src/lib/common/data.zig +4 -4
  20. package/src/lib/common/debug.zig +2 -2
  21. package/src/lib/common/js.zig +4 -2
  22. package/src/lib/common/optional.zig +14 -10
  23. package/src/lib/common/options.zig +2 -0
  24. package/src/lib/common/protocol.zig +57 -57
  25. package/src/lib/common/rational.zig +114 -24
  26. package/src/lib/common/rng.zig +7 -7
  27. package/src/lib/common/util.zig +50 -1
  28. package/src/lib/gen1/calc.zig +506 -128
  29. package/src/lib/gen1/chance.zig +374 -267
  30. package/src/lib/gen1/data/species.zig +1 -1
  31. package/src/lib/gen1/data.zig +17 -16
  32. package/src/lib/gen1/helpers.zig +95 -538
  33. package/src/lib/gen1/mechanics.zig +268 -174
  34. package/src/lib/gen2/calc.zig +34 -16
  35. package/src/lib/gen2/chance.zig +84 -496
  36. package/src/lib/gen2/data.zig +7 -6
  37. package/src/lib/gen2/generated.zig +0 -2
  38. package/src/lib/gen2/helpers.zig +41 -13
  39. package/src/lib/gen2/mechanics.zig +158 -108
  40. package/src/lib/{binding/node.zig → node.zig} +3 -6
  41. package/src/lib/pkmn.zig +32 -15
  42. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  43. package/src/test/display/pkmn.html.tmpl +5 -5
  44. package/src/lib/gen1/README.md +0 -894
  45. 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,14 +652,8 @@ fn beforeMove(
635
652
  volatiles.state +%= battle.last_damage;
636
653
  }
637
654
 
638
- assert(volatiles.attacks > 0);
639
-
640
- if (options.calc.modify(player, .attacking)) |extend| {
641
- if (!extend) volatiles.attacks = 0;
642
- } else {
643
- volatiles.attacks -= 1;
644
- }
645
- 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);
646
657
 
647
658
  if (volatiles.attacks != 0) {
648
659
  try log.activate(.{ ident, .Bide });
@@ -676,39 +687,36 @@ fn beforeMove(
676
687
  }
677
688
 
678
689
  if (volatiles.Thrashing) {
679
- assert(volatiles.attacks > 0);
690
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
680
691
 
681
- if (options.calc.modify(player, .attacking)) |extend| {
682
- if (!extend) volatiles.attacks = 0;
683
- } else {
684
- volatiles.attacks -= 1;
685
- }
686
- try options.chance.attacking(player, volatiles.attacks);
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;
687
697
 
688
- 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)
689
706
  try log.start(.{ battle.active(player), .ConfusionSilent });
707
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
690
708
  }
691
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
692
- if (showdown) {
693
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
694
- // used in useMove and doesn't have the notion of skipping canMove semantics
695
- side.last_used_move = side.last_selected_move;
696
- if (handleThrashing(battle, active, player, options)) {
697
- try log.start(.{ battle.active(player), .ConfusionSilent });
698
- }
699
- }
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
+
700
714
  return .skip_can;
701
715
  }
702
716
 
703
717
  if (volatiles.Binding) {
704
- assert(volatiles.attacks > 0);
705
-
706
- if (options.calc.modify(player, .binding)) |extend| {
707
- if (!extend) volatiles.attacks = 0;
708
- } else {
709
- volatiles.attacks -= 1;
710
- }
711
- 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);
712
720
 
713
721
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
722
  if (showdown or battle.last_damage != 0) {
@@ -801,7 +809,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
801
809
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
802
810
  }
803
811
 
804
- fn incrementPP(side: *Side, mslot: u4) void {
812
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
805
813
  var active = &side.active;
806
814
  const volatiles = &active.volatiles;
807
815
 
@@ -809,8 +817,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
809
817
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
810
818
  if (showdown and volatiles.Transform) return;
811
819
 
820
+ const n = options.calc.overridden(player, .pp) orelse 1;
821
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
822
+
812
823
  assert(mslot > 0 and mslot <= 4);
813
- 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;
814
825
  }
815
826
 
816
827
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -858,13 +869,13 @@ fn doMove(
858
869
  break :miss true;
859
870
  }
860
871
  break :miss (move.target != .Self and
861
- !moveHit(battle, player, move, &immune, &mist, options));
872
+ !try moveHit(battle, player, move, &immune, &mist, options));
862
873
  };
863
874
  assert(!immune or miss or (showdown and move.effect == .Binding));
864
875
 
865
876
  var late = showdown and move.effect != .Explode;
866
877
  const skip = status or immune;
867
- if ((!showdown or (!skip or counter)) and !miss) blk: {
878
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
868
879
  if (showdown and move.effect.isMulti()) {
869
880
  try Effects.multiHit(battle, player, move, options);
870
881
  hits = side.active.volatiles.attacks;
@@ -886,8 +897,9 @@ fn doMove(
886
897
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
887
898
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
888
899
  battle.last_damage = if (ohko) 65535 else 0;
889
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
890
- } 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);
891
903
  return @as(?Result, Result.Error);
892
904
  }
893
905
  if (battle.last_damage == 0) {
@@ -901,11 +913,12 @@ fn doMove(
901
913
  }
902
914
  }
903
915
 
916
+ var missed: bool = undefined;
904
917
  const zero = battle.last_damage == 0;
905
- miss = if (showdown or skip)
906
- miss
907
- else
908
- (!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
+ }
909
922
 
910
923
  assert(showdown or miss or battle.last_damage > 0 or skip);
911
924
  assert((!showdown and miss) or !(ohko and immune));
@@ -932,11 +945,32 @@ fn doMove(
932
945
  try log.immune(.{ foe_ident, .OHKO });
933
946
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
934
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
+ }
935
969
  } else if (mist) {
936
970
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
937
971
  try log.fail(.{ foe_ident, .None });
938
972
  } else {
939
- 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);
940
974
  try log.lastmiss(.{});
941
975
  try log.miss(.{battle.active(player)});
942
976
  }
@@ -951,17 +985,15 @@ fn doMove(
951
985
  try buildRage(battle, player.foe(), options);
952
986
  } else if (showdown and move.effect == .Disable) {
953
987
  try buildRage(battle, player.foe(), options);
954
- } else if (!showdown and immune and move.effect == .Binding) {
955
- try options.chance.commit(player, .binding);
956
988
  }
957
989
  return null;
958
- } else {
990
+ } else if (!showdown) {
959
991
  try options.chance.commit(player, .hit);
960
992
  }
961
993
 
962
994
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
963
995
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
964
- // it up above before damage calculation).
996
+ // it up above before damage calculation)
965
997
  if (!showdown and move.effect.isMulti()) {
966
998
  try Effects.multiHit(battle, player, move, options);
967
999
  hits = side.active.volatiles.attacks;
@@ -1020,12 +1052,12 @@ fn doMove(
1020
1052
  }
1021
1053
 
1022
1054
  // Substitute being broken nullifies the move's effect completely so even
1023
- // 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
1024
1056
  if (nullified) return null;
1025
1057
 
1026
1058
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1027
1059
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1028
- // 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
1029
1061
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1030
1062
 
1031
1063
  if (foe.stored().hp == 0) return null;
@@ -1040,7 +1072,7 @@ fn doMove(
1040
1072
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1041
1073
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1042
1074
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1043
- // puts the |-status| message before |-hitcount| instead of after.
1075
+ // puts the |-status| message before |-hitcount| instead of after
1044
1076
  if (move.effect == .Twineedle) {
1045
1077
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1046
1078
  } else if (move.effect == .Disable) {
@@ -1059,12 +1091,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1059
1091
  const side = battle.side(player);
1060
1092
 
1061
1093
  // Base speed is used for the critical hit calculation, even when Transform-ed
1062
- var rate = @as(u16, Species.chance(side.stored().species));
1094
+ var rate: u16 = Species.chance(side.stored().species);
1063
1095
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1064
1096
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1065
1097
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1066
1098
 
1067
- 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);
1068
1102
  }
1069
1103
 
1070
1104
  fn calcDamage(
@@ -1073,6 +1107,7 @@ fn calcDamage(
1073
1107
  target_player: Player,
1074
1108
  m: ?Move.Data,
1075
1109
  crit: bool,
1110
+ options: anytype,
1076
1111
  ) bool {
1077
1112
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1078
1113
  const cfz = m == null;
@@ -1084,28 +1119,8 @@ fn calcDamage(
1084
1119
 
1085
1120
  const special = move.type.special();
1086
1121
 
1087
- // zig fmt: off
1088
- var atk: u32 =
1089
- if (crit)
1090
- if (special) side.stored().stats.spc
1091
- else side.stored().stats.atk
1092
- else
1093
- if (special) side.active.stats.spc
1094
- else side.active.stats.atk;
1095
- var def: u32 =
1096
- if (crit)
1097
- if (special) target.stored().stats.spc
1098
- else target.stored().stats.def
1099
- else
1100
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1101
- if (special)
1102
- target.active.stats.spc *
1103
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1104
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1105
- else
1106
- target.active.stats.def *
1107
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1108
- // zig fmt: on
1122
+ var atk = attack(side, crit, special);
1123
+ var def = defense(target, crit, special, cfz);
1109
1124
 
1110
1125
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1111
1126
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1116,9 +1131,9 @@ fn calcDamage(
1116
1131
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1117
1132
  }
1118
1133
 
1119
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1134
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1120
1135
 
1121
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1136
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1122
1137
 
1123
1138
  if (def == 0) return false;
1124
1139
 
@@ -1130,11 +1145,41 @@ fn calcDamage(
1130
1145
  d = @min(997, d);
1131
1146
  d += 2;
1132
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
+
1133
1158
  battle.last_damage = @intCast(d);
1134
1159
 
1135
1160
  return true;
1136
1161
  }
1137
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
+
1138
1183
  fn adjustDamage(battle: anytype, player: Player) u16 {
1139
1184
  const side = battle.side(player);
1140
1185
  const foe = battle.foe(player);
@@ -1179,7 +1224,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1179
1224
  const foe = battle.foe(player);
1180
1225
 
1181
1226
  if (!try checkHit(battle, player, move, options)) return null;
1182
- try options.chance.commit(player, .hit);
1183
1227
 
1184
1228
  battle.last_damage = switch (side.last_selected_move) {
1185
1229
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1240,9 +1284,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1240
1284
 
1241
1285
  battle.last_damage *|= 2;
1242
1286
 
1243
- // Pokémon Showdown calls checkHit before Counter
1287
+ // Pokémon Showdown calls moveHit before Counter
1244
1288
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1245
- try options.chance.commit(player, .hit);
1246
1289
 
1247
1290
  const sub = showdown and foe.active.volatiles.Substitute;
1248
1291
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1271,7 +1314,7 @@ fn applyDamage(
1271
1314
  subbed.active.volatiles.Substitute = false;
1272
1315
  // battle.last_damage is not updated with the amount of HP the Substitute had
1273
1316
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1274
- options.calc.capped(target_player);
1317
+ options.calc.capped(sub_player);
1275
1318
  return true;
1276
1319
  } else {
1277
1320
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1305,7 +1348,7 @@ fn mirrorMove(
1305
1348
  try options.log.fail(.{ battle.active(player), .None });
1306
1349
  return null;
1307
1350
  } else if (!showdown or foe.last_used_move != .Struggle) {
1308
- incrementPP(side, mslot);
1351
+ incrementPP(side, player, mslot, options);
1309
1352
  }
1310
1353
 
1311
1354
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1326,7 +1369,7 @@ fn metronome(
1326
1369
  var side = battle.side(player);
1327
1370
 
1328
1371
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1329
- incrementPP(side, mslot);
1372
+ incrementPP(side, player, mslot, options);
1330
1373
 
1331
1374
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1332
1375
  return null;
@@ -1338,7 +1381,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1338
1381
  var immune = false;
1339
1382
  var mist = false;
1340
1383
 
1341
- 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
+ }
1342
1388
 
1343
1389
  assert(!immune);
1344
1390
  if (mist) {
@@ -1347,11 +1393,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1347
1393
  try options.log.activate(.{ foe_ident, .Mist });
1348
1394
  try options.log.fail(.{ foe_ident, .None });
1349
1395
  } else {
1350
- try options.chance.commit(player, .miss);
1351
1396
  try options.log.lastmiss(.{});
1352
1397
  try options.log.miss(.{battle.active(player)});
1353
1398
  }
1354
1399
 
1400
+ if (!showdown) try options.chance.commit(player, .miss);
1355
1401
  return false;
1356
1402
  }
1357
1403
 
@@ -1362,7 +1408,7 @@ fn moveHit(
1362
1408
  immune: *bool,
1363
1409
  mist: *bool,
1364
1410
  options: anytype,
1365
- ) bool {
1411
+ ) !bool {
1366
1412
  var side = battle.side(player);
1367
1413
  const foe = battle.foe(player);
1368
1414
 
@@ -1401,7 +1447,7 @@ fn moveHit(
1401
1447
  const overwritten = overwrite and state > 0;
1402
1448
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1449
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1450
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1451
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1452
  accuracy = accuracy * boost[0] / boost[1];
1407
1453
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1413,7 +1459,7 @@ fn moveHit(
1413
1459
  if (overwrite) side.active.volatiles.state = accuracy;
1414
1460
 
1415
1461
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1416
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1462
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1417
1463
  };
1418
1464
 
1419
1465
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1424,13 +1470,17 @@ fn moveHit(
1424
1470
 
1425
1471
  if (!miss) return true;
1426
1472
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1473
+
1427
1474
  side.active.volatiles.Binding = false;
1475
+ options.chance.observe(.binding, player, .None);
1476
+
1428
1477
  return false;
1429
1478
  }
1430
1479
 
1431
1480
  fn checkFaint(
1432
1481
  battle: anytype,
1433
1482
  player: Player,
1483
+ cap: bool,
1434
1484
  options: anytype,
1435
1485
  ) @TypeOf(options.log).Error!?Result {
1436
1486
  const side = battle.side(player);
@@ -1443,8 +1493,8 @@ fn checkFaint(
1443
1493
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
1494
  const more = player_out or foe_out;
1445
1495
 
1446
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1447
- 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;
1448
1498
 
1449
1499
  assert(!side.active.volatiles.MultiHit);
1450
1500
  assert(!foe.active.volatiles.MultiHit);
@@ -1465,7 +1515,7 @@ fn checkFaint(
1465
1515
  return .{ .p1 = foe_choice, .p2 = .Switch };
1466
1516
  }
1467
1517
 
1468
- 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 {
1469
1519
  var side = battle.side(player);
1470
1520
  var foe = battle.foe(player);
1471
1521
  assert(side.stored().hp == 0);
@@ -1499,7 +1549,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1499
1549
  }
1500
1550
 
1501
1551
  try options.log.faint(.{ battle.active(player), done });
1502
- 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);
1503
1555
  return null;
1504
1556
  }
1505
1557
 
@@ -1659,17 +1711,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1711
  }
1660
1712
  }
1661
1713
 
1662
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1663
- var volatiles = &active.volatiles;
1664
- assert(volatiles.Thrashing);
1665
- if (volatiles.attacks > 0) return false;
1666
-
1667
- volatiles.Thrashing = false;
1668
- volatiles.Confusion = true;
1669
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1670
- return true;
1671
- }
1672
-
1673
1714
  fn onBegin(
1674
1715
  battle: anytype,
1675
1716
  player: Player,
@@ -1678,7 +1719,6 @@ fn onBegin(
1678
1719
  residual: *bool,
1679
1720
  options: anytype,
1680
1721
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1722
  return switch (move.effect) {
1683
1723
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1724
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,7 +1741,6 @@ fn onBegin(
1701
1741
  }
1702
1742
 
1703
1743
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1705
1744
  return switch (move.effect) {
1706
1745
  .Bide => Effects.bide(battle, player, options),
1707
1746
  // zig fmt: off
@@ -1723,7 +1762,6 @@ fn alwaysHappens(
1723
1762
  residual: *bool,
1724
1763
  options: anytype,
1725
1764
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1765
  return switch (move.effect) {
1728
1766
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1767
  .Explode => Effects.explode(battle, player),
@@ -1759,8 +1797,10 @@ pub const Effects = struct {
1759
1797
 
1760
1798
  side.active.volatiles.Bide = true;
1761
1799
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1800
+
1762
1801
  side.active.volatiles.state = 0;
1763
1802
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1803
+ options.chance.observe(.attacking, player, .started);
1764
1804
 
1765
1805
  try options.log.start(.{ battle.active(player), .Bide });
1766
1806
  }
@@ -1822,12 +1862,13 @@ pub const Effects = struct {
1822
1862
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1823
1863
  if (!try checkHit(battle, player, move, options)) return;
1824
1864
  }
1825
- try options.chance.commit(player, .hit);
1826
1865
  }
1827
1866
 
1828
1867
  if (foe.active.volatiles.Confusion) return;
1829
1868
  foe.active.volatiles.Confusion = true;
1830
- 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);
1831
1872
 
1832
1873
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1833
1874
  }
@@ -1855,10 +1896,7 @@ pub const Effects = struct {
1855
1896
  const foe_ident = battle.active(player.foe());
1856
1897
 
1857
1898
  // Pokémon Showdown handles hit/miss earlier in doMove
1858
- if (!showdown) {
1859
- if (!try checkHit(battle, player, move, options)) return null;
1860
- try options.chance.commit(player, .hit);
1861
- }
1899
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1862
1900
 
1863
1901
  if (volatiles.disable_move != 0) {
1864
1902
  try options.log.fail(.{ foe_ident, .None });
@@ -1888,6 +1926,7 @@ pub const Effects = struct {
1888
1926
  volatiles.disable_move =
1889
1927
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1890
1928
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1929
+ options.chance.observe(.disable, player.foe(), .started);
1891
1930
 
1892
1931
  const id = foe.active.move(volatiles.disable_move).id;
1893
1932
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1994,22 +2033,24 @@ pub const Effects = struct {
1994
2033
  if (p != player and Status.any(s.stored().status)) {
1995
2034
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1996
2035
  s.stored().status = 0;
2036
+ options.chance.observe(.sleep, p, .None);
1997
2037
  } else if (showdown and s.stored().status == Status.TOX) {
1998
2038
  s.stored().status = Status.init(.PSN);
1999
2039
  try log.status(.{ battle.active(p), s.stored().status, .None });
2000
2040
  }
2001
- try clearVolatiles(battle, p, p != player, options);
2041
+ try clearVolatiles(battle, p, options);
2002
2042
  }
2003
2043
  } else {
2004
2044
  if (Status.any(foe_stored.status)) {
2005
2045
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2006
2046
  foe.last_selected_move = .SKIP_TURN;
2047
+ options.chance.observe(.sleep, player.foe(), .None);
2007
2048
  }
2008
2049
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2009
2050
  foe_stored.status = 0;
2010
2051
  }
2011
- try clearVolatiles(battle, player, false, options);
2012
- try clearVolatiles(battle, player.foe(), true, options);
2052
+ try clearVolatiles(battle, player, options);
2053
+ try clearVolatiles(battle, player.foe(), options);
2013
2054
  }
2014
2055
  }
2015
2056
 
@@ -2060,7 +2101,6 @@ pub const Effects = struct {
2060
2101
  }
2061
2102
  }
2062
2103
 
2063
- try options.chance.commit(player, .hit);
2064
2104
  foe.active.volatiles.LeechSeed = true;
2065
2105
 
2066
2106
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2101,12 +2141,10 @@ pub const Effects = struct {
2101
2141
  if (!has_mimic) {
2102
2142
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2103
2143
  if (!try checkHit(battle, player, move, options)) return;
2104
- try options.chance.commit(player, .hit);
2105
2144
  return options.log.fail(.{ battle.active(player.foe()), .None });
2106
2145
  }
2107
2146
  }
2108
2147
  if (!try checkHit(battle, player, move, options)) return;
2109
- try options.chance.commit(player, .hit);
2110
2148
 
2111
2149
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2112
2150
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2160,7 +2198,6 @@ pub const Effects = struct {
2160
2198
  if (immune) return log.immune(.{ foe_ident, .None });
2161
2199
  if (!try checkHit(battle, player, move, options)) return;
2162
2200
  }
2163
- try options.chance.commit(player, .hit);
2164
2201
 
2165
2202
  foe_stored.status = Status.init(.PAR);
2166
2203
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2228,7 +2265,6 @@ pub const Effects = struct {
2228
2265
 
2229
2266
  if (move.effect == .Poison) {
2230
2267
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2231
- try options.chance.commit(player, .hit);
2232
2268
  } else {
2233
2269
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2234
2270
  return;
@@ -2297,9 +2333,8 @@ pub const Effects = struct {
2297
2333
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2298
2334
  });
2299
2335
  }
2300
- // 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
2301
2337
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2302
- try options.chance.commit(player, .hit);
2303
2338
  }
2304
2339
 
2305
2340
  // Sleep Clause Mod
@@ -2311,6 +2346,8 @@ pub const Effects = struct {
2311
2346
  foe.active.volatiles.Recharging = false;
2312
2347
 
2313
2348
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2349
+ options.chance.observe(.sleep, player.foe(), .started);
2350
+
2314
2351
  const last = battle.side(player).last_selected_move;
2315
2352
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2316
2353
  }
@@ -2352,9 +2389,7 @@ pub const Effects = struct {
2352
2389
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2353
2390
  if (showdown) {
2354
2391
  if (battle.side(player).last_selected_move == .Teleport) return;
2355
- if (try checkHit(battle, player, move, options)) {
2356
- try options.chance.commit(player, .hit);
2357
- }
2392
+ _ = try checkHit(battle, player, move, options);
2358
2393
  battle.last_damage = 0;
2359
2394
  } else {
2360
2395
  try options.log.fail(.{ battle.active(player), .None });
@@ -2369,6 +2404,8 @@ pub const Effects = struct {
2369
2404
 
2370
2405
  volatiles.Thrashing = true;
2371
2406
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2407
+
2408
+ options.chance.observe(.attacking, player, .started);
2372
2409
  }
2373
2410
 
2374
2411
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2414,6 +2451,7 @@ pub const Effects = struct {
2414
2451
 
2415
2452
  side.active.volatiles.attacks =
2416
2453
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2454
+ options.chance.observe(.binding, player, .started);
2417
2455
  }
2418
2456
 
2419
2457
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2530,7 +2568,6 @@ pub const Effects = struct {
2530
2568
  } else {
2531
2569
  // checkHit already checks for Invulnerable
2532
2570
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2533
- try options.chance.commit(player, .hit);
2534
2571
  }
2535
2572
 
2536
2573
  var stats = &foe.active.stats;
@@ -2644,22 +2681,22 @@ fn isForced(active: anytype) bool {
2644
2681
  active.volatiles.Thrashing or active.volatiles.Charging;
2645
2682
  }
2646
2683
 
2647
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2684
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2648
2685
  var log = options.log;
2649
2686
  var side = battle.side(who);
2650
2687
  var volatiles = &side.active.volatiles;
2651
2688
  const ident = battle.active(who);
2652
2689
 
2653
- options.chance.clearDurations(who, clear);
2654
-
2655
2690
  if (volatiles.disable_move != 0) {
2656
2691
  volatiles.disable_move = 0;
2657
2692
  volatiles.disable_duration = 0;
2693
+ options.chance.observe(.disable, who, .None);
2658
2694
  try log.end(.{ ident, .DisableSilent });
2659
2695
  }
2660
2696
  if (volatiles.Confusion) {
2661
2697
  // volatiles.confusion is left unchanged
2662
2698
  volatiles.Confusion = false;
2699
+ options.chance.observe(.confusion, who, .None);
2663
2700
  try log.end(.{ ident, .ConfusionSilent });
2664
2701
  }
2665
2702
  if (volatiles.Mist) {
@@ -2694,10 +2731,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2694
2731
  }
2695
2732
  }
2696
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
+
2697
2747
  pub const Rolls = struct {
2698
2748
  fn speedTie(battle: anytype, options: anytype) !bool {
2699
2749
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2700
2750
  player == .P1
2751
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2752
+ battle.rng.speedTie()
2701
2753
  else if (showdown)
2702
2754
  battle.rng.range(u8, 0, 2) == 0
2703
2755
  else
@@ -2707,21 +2759,31 @@ pub const Rolls = struct {
2707
2759
  return p1;
2708
2760
  }
2709
2761
 
2710
- 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 {
2711
2769
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2712
2770
  val == .true
2771
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2772
+ battle.rng.criticalHit(player, rate)
2713
2773
  else if (showdown)
2714
2774
  battle.rng.chance(u8, rate, 256)
2715
2775
  else
2716
2776
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2717
2777
 
2718
- try options.chance.criticalHit(player, crit, rate);
2778
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2719
2779
  return crit;
2720
2780
  }
2721
2781
 
2722
2782
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2723
2783
  const roll = if (options.calc.overridden(player, .damage)) |val|
2724
2784
  val
2785
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2786
+ battle.rng.damage(player)
2725
2787
  else roll: {
2726
2788
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2727
2789
  while (true) {
@@ -2735,21 +2797,27 @@ pub const Rolls = struct {
2735
2797
  return roll;
2736
2798
  }
2737
2799
 
2738
- 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;
2739
2803
  const ok = if (options.calc.overridden(player, .hit)) |val|
2740
2804
  val == .true
2805
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2806
+ battle.rng.hit(player, acc)
2741
2807
  else if (showdown)
2742
- battle.rng.chance(u8, accuracy, 256)
2808
+ battle.rng.chance(uN, acc, 256)
2743
2809
  else
2744
- battle.rng.next() < accuracy;
2810
+ battle.rng.next() < acc;
2745
2811
 
2746
- options.chance.hit(ok, accuracy);
2812
+ try options.chance.hit(player, ok, acc);
2747
2813
  return ok;
2748
2814
  }
2749
2815
 
2750
2816
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2751
2817
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2752
2818
  val == .true
2819
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2820
+ battle.rng.confused(player)
2753
2821
  else if (showdown)
2754
2822
  !battle.rng.chance(u8, 128, 256)
2755
2823
  else
@@ -2762,6 +2830,8 @@ pub const Rolls = struct {
2762
2830
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
2831
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2764
2832
  val == .true
2833
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2834
+ battle.rng.paralyzed(player)
2765
2835
  else if (showdown)
2766
2836
  battle.rng.chance(u8, 63, 256)
2767
2837
  else
@@ -2774,6 +2844,8 @@ pub const Rolls = struct {
2774
2844
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2775
2845
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2776
2846
  val == .true
2847
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2848
+ battle.rng.secondaryChance(player, 25)
2777
2849
  else if (showdown)
2778
2850
  battle.rng.chance(u8, 25, 256)
2779
2851
  else
@@ -2788,6 +2860,8 @@ pub const Rolls = struct {
2788
2860
 
2789
2861
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2790
2862
  val == .true
2863
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2864
+ battle.rng.secondaryChance(player, rate)
2791
2865
  else if (showdown)
2792
2866
  battle.rng.chance(u8, rate, 256)
2793
2867
  else
@@ -2802,6 +2876,8 @@ pub const Rolls = struct {
2802
2876
 
2803
2877
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2804
2878
  val == .true
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2880
+ battle.rng.secondaryChance(player, rate)
2805
2881
  else if (showdown)
2806
2882
  battle.rng.chance(u8, rate, 256)
2807
2883
  else
@@ -2814,6 +2890,8 @@ pub const Rolls = struct {
2814
2890
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2815
2891
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2816
2892
  val == .true
2893
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2894
+ battle.rng.secondaryChance(player, 85)
2817
2895
  else if (showdown)
2818
2896
  battle.rng.chance(u8, 85, 256)
2819
2897
  else
@@ -2826,6 +2904,8 @@ pub const Rolls = struct {
2826
2904
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2827
2905
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2828
2906
  val
2907
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2908
+ battle.rng.metronome(player)
2829
2909
  else if (showdown)
2830
2910
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2831
2911
  else move: {
@@ -2844,6 +2924,8 @@ pub const Rolls = struct {
2844
2924
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2845
2925
  const power = if (options.calc.overridden(player, .psywave)) |val|
2846
2926
  val - 1
2927
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2928
+ battle.rng.psywave(player)
2847
2929
  else if (showdown)
2848
2930
  battle.rng.range(u8, 0, max)
2849
2931
  else power: {
@@ -2861,6 +2943,8 @@ pub const Rolls = struct {
2861
2943
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2862
2944
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2863
2945
  @intCast(val)
2946
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2947
+ battle.rng.sleepDuration(player)
2864
2948
  else if (showdown)
2865
2949
  battle.rng.range(u3, 1, 8)
2866
2950
  else duration: {
@@ -2871,46 +2955,52 @@ pub const Rolls = struct {
2871
2955
  };
2872
2956
 
2873
2957
  assert(duration >= 1 and duration <= 7);
2874
- options.chance.duration(.sleep, player, player.foe(), duration);
2958
+ options.chance.duration(player, duration);
2875
2959
  return duration;
2876
2960
  }
2877
2961
 
2878
2962
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2879
2963
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2880
2964
  val
2965
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2966
+ battle.rng.disableDuration(player)
2881
2967
  else if (showdown)
2882
2968
  battle.rng.range(u4, 1, 9)
2883
2969
  else
2884
2970
  @intCast((battle.rng.next() & 7) + 1);
2885
2971
 
2886
2972
  assert(duration >= 1 and duration <= 8);
2887
- options.chance.duration(.disable, player, player.foe(), duration);
2973
+ options.chance.duration(player, duration);
2888
2974
  return duration;
2889
2975
  }
2890
2976
 
2891
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2977
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2892
2978
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2893
2979
  @intCast(val)
2980
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2981
+ battle.rng.confusionDuration(player)
2894
2982
  else if (showdown)
2895
2983
  battle.rng.range(u3, 2, 6)
2896
2984
  else
2897
2985
  @intCast((battle.rng.next() & 3) + 2);
2898
2986
 
2899
2987
  assert(duration >= 2 and duration <= 5);
2900
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2988
+ options.chance.duration(player, duration);
2901
2989
  return duration;
2902
2990
  }
2903
2991
 
2904
2992
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2993
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2994
  @intCast(val)
2995
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2996
+ battle.rng.attackingDuration(player)
2907
2997
  else if (showdown)
2908
2998
  battle.rng.range(u3, 2, 4)
2909
2999
  else
2910
3000
  @intCast((battle.rng.next() & 1) + 2);
2911
3001
 
2912
3002
  assert(duration >= 2 and duration <= 3);
2913
- options.chance.duration(.attacking, player, player, duration);
3003
+ options.chance.duration(player, duration);
2914
3004
  return duration;
2915
3005
  }
2916
3006
 
@@ -2925,6 +3015,8 @@ pub const Rolls = struct {
2925
3015
  const roll = if (effect == .Binding) .duration else .multi_hit;
2926
3016
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2927
3017
  @intCast(val)
3018
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3019
+ battle.rng.distribution(player)
2928
3020
  else if (showdown)
2929
3021
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2930
3022
  else n: {
@@ -2934,7 +3026,7 @@ pub const Rolls = struct {
2934
3026
 
2935
3027
  assert(n >= 2 and n <= 5);
2936
3028
  if (effect == .Binding) {
2937
- options.chance.duration(.binding, player, player, n);
3029
+ options.chance.duration(player, n);
2938
3030
  } else {
2939
3031
  try options.chance.multiHit(player, n);
2940
3032
  }
@@ -2957,7 +3049,9 @@ pub const Rolls = struct {
2957
3049
  else
2958
3050
  null;
2959
3051
 
2960
- 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: {
2961
3055
  if (showdown) {
2962
3056
  if (check_pp == 0) {
2963
3057
  var i: usize = moves.len;
@@ -3041,7 +3135,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3041
3135
  const stored = side.stored();
3042
3136
 
3043
3137
  // While players are not given any input options on the cartridge in these cases,
3044
- // 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
3045
3139
  //
3046
3140
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3047
3141
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3049,7 +3143,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3049
3143
  // passing due to being forced into using a move. Instead of introducing another option
3050
3144
  // we simply repurpose Move with no move slot, even though pedantically this is not
3051
3145
  // strictly correct as the player would not have been presented the option to move or
3052
- // switch at all.
3146
+ // switch at all
3053
3147
  if (isForced(active)) {
3054
3148
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3055
3149
  n += 1;
@@ -3068,7 +3162,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3162
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3069
3163
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3070
3164
  // either disables all other moves in the case of limited or requires you to select a
3071
- // move normally if sleeping/frozen/bound.
3165
+ // move normally if sleeping/frozen/bound
3072
3166
  if (!showdown and (limited or foe.active.volatiles.Binding or
3073
3167
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3074
3168
  {
@@ -3081,7 +3175,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3081
3175
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3082
3176
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3083
3177
  // Move will not result in forcing the subsequent use unless the user also had the
3084
- // proc-ed move in their moveset) and disabling all other moves.
3178
+ // proc-ed move in their moveset) and disabling all other moves
3085
3179
  if (limited) {
3086
3180
  assert(showdown);
3087
3181
  assert(side.last_selected_move != .None);