@pkmn/engine 0.1.0-dev.b3f3e3a6 → 0.1.0-dev.b9015a94

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