@pkmn/engine 0.1.0-dev.047d6e71 → 0.1.0-dev.04a96f24

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 (73) hide show
  1. package/README.md +15 -11
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +44 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +97 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +21 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +22 -0
  12. package/build/pkg/addon.js +35 -58
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data.d.ts +25 -4
  15. package/build/pkg/data.js.map +1 -1
  16. package/build/pkg/gen1.js +1 -1
  17. package/build/pkg/gen1.js.map +1 -1
  18. package/build/pkg/index.d.ts +22 -9
  19. package/build/pkg/index.js +15 -10
  20. package/build/pkg/index.js.map +1 -1
  21. package/build/pkg/protocol.d.ts +20 -1
  22. package/build/pkg/protocol.js +109 -107
  23. package/build/pkg/protocol.js.map +1 -1
  24. package/build/tools/debug.js +36 -70
  25. package/build/tools/debug.js.map +1 -1
  26. package/build/tools/display/debug.jsx +141 -0
  27. package/build/tools/display/debug.jsx.map +1 -0
  28. package/build/tools/display/index.js +141 -0
  29. package/build/tools/display/index.js.map +1 -0
  30. package/build/tools/display/ui.jsx +349 -0
  31. package/build/tools/display/ui.jsx.map +1 -0
  32. package/build/tools/display/util.js +18 -0
  33. package/build/tools/display/util.js.map +1 -0
  34. package/build.zig +213 -75
  35. package/package.json +53 -33
  36. package/src/bin/install-pkmn-engine +17 -7
  37. package/src/bin/pkmn-debug +1 -1
  38. package/src/lib/common/array.zig +77 -0
  39. package/src/lib/common/data.zig +8 -8
  40. package/src/lib/common/debug.zig +39 -8
  41. package/src/lib/common/js.zig +4 -2
  42. package/src/lib/common/optional.zig +13 -9
  43. package/src/lib/common/options.zig +4 -0
  44. package/src/lib/common/protocol.zig +45 -44
  45. package/src/lib/common/rational.zig +121 -29
  46. package/src/lib/common/rng.zig +5 -7
  47. package/src/lib/common/util.zig +50 -1
  48. package/src/lib/gen1/calc.zig +683 -156
  49. package/src/lib/gen1/chance.zig +415 -290
  50. package/src/lib/gen1/data/moves.zig +2 -4
  51. package/src/lib/gen1/data/species.zig +1 -3
  52. package/src/lib/gen1/data/types.zig +1 -2
  53. package/src/lib/gen1/data.zig +38 -41
  54. package/src/lib/gen1/helpers.zig +92 -548
  55. package/src/lib/gen1/mechanics.zig +280 -216
  56. package/src/lib/gen2/calc.zig +37 -27
  57. package/src/lib/gen2/chance.zig +90 -502
  58. package/src/lib/gen2/data/items.zig +1 -3
  59. package/src/lib/gen2/data/moves.zig +2 -4
  60. package/src/lib/gen2/data/species.zig +1 -3
  61. package/src/lib/gen2/data/types.zig +1 -2
  62. package/src/lib/gen2/data.zig +26 -29
  63. package/src/lib/gen2/generated.zig +18 -23
  64. package/src/lib/gen2/helpers.zig +46 -25
  65. package/src/lib/gen2/mechanics.zig +66 -104
  66. package/src/lib/node.zig +2 -4
  67. package/src/lib/pkmn.zig +14 -16
  68. package/src/lib/wasm.zig +1 -2
  69. package/src/tools/display/dom.ts +96 -0
  70. package/src/tools/display/ui.css +354 -0
  71. package/build/test/display.js +0 -400
  72. package/build/test/display.js.map +0 -1
  73. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,37 +1,28 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
5
+ const data = @import("data.zig");
6
+ const pkmn = @import("../pkmn.zig");
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
-
10
- const data = @import("data.zig");
9
+ const std = @import("std");
11
10
 
12
11
  const assert = std.debug.assert;
13
-
14
- const expectEqual = std.testing.expectEqual;
15
-
16
- const showdown = pkmn.options.showdown;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
12
  const Boost = protocol.Boost;
13
+ const Choice = common.Choice;
24
14
  const Damage = protocol.Damage;
15
+ const Effectiveness = data.Effectiveness;
16
+ const expectEqual = std.testing.expectEqual;
25
17
  const Fail = protocol.Fail;
26
- const Heal = protocol.Heal;
27
-
28
18
  const Gen12 = rng.Gen12;
29
-
30
- const ActivePokemon = data.ActivePokemon;
31
- const Effectiveness = data.Effectiveness;
19
+ const Heal = protocol.Heal;
20
+ const ID = common.ID;
32
21
  const Move = data.Move;
33
22
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
23
+ const Player = common.Player;
24
+ const Result = common.Result;
25
+ const showdown = pkmn.options.showdown;
35
26
  const Side = data.Side;
36
27
  const Species = data.Species;
37
28
  const Stats = data.Stats;
@@ -166,7 +157,7 @@ fn selectMove(
166
157
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
158
  // set the selected move will not actually be used, it will just be reported as having
168
159
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
160
+ // replicate the incorrect behavior with the correct mechanisms)
170
161
  saveMove(battle, player, choice);
171
162
  } else {
172
163
  assert(choice.data == 0);
@@ -253,11 +244,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
244
  active.moves = incoming.moves;
254
245
 
255
246
  statusModify(incoming.status, &active.stats);
247
+ options.chance.switched(player, slot);
256
248
 
257
249
  foe.active.volatiles.Binding = false;
258
250
 
259
251
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
252
 
262
253
  if (showdown and incoming.status == Status.TOX) {
263
254
  incoming.status = Status.init(.PSN);
@@ -347,13 +338,13 @@ fn doTurn(
347
338
  )) |r| return r;
348
339
  if (!replace) {
349
340
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
341
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
342
  }
352
343
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
344
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
345
  } else if (foe_choice.type == .Pass) return null;
355
346
 
356
- options.chance.clearPending();
347
+ if (!showdown) options.chance.clearPending();
357
348
 
358
349
  residual = true;
359
350
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +360,10 @@ fn doTurn(
369
360
  )) |r| return r;
370
361
  if (!replace) {
371
362
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
363
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
364
  }
374
365
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
366
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
367
  }
377
368
 
378
369
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +422,7 @@ fn executeMove(
431
422
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
423
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
424
  // 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.
425
+ // index 1 and the current Pokémon has the same move in its first slot
435
426
  if (!auto) {
436
427
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
428
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -484,17 +475,22 @@ fn beforeMove(
484
475
 
485
476
  if (Status.is(stored.status, .SLP)) {
486
477
  const before = stored.status;
478
+ const slf = Status.is(stored.status, .EXT);
487
479
  // 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;
480
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
481
+ .started, .ended => stored.status = 0,
482
+ .continuing => if (Status.duration(stored.status) > 1) {
483
+ stored.status -= 1;
484
+ },
485
+ else => unreachable,
490
486
  } else {
491
487
  stored.status -= 1;
492
488
  }
493
489
 
494
490
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
491
+ try options.chance.sleep(player, if (slf)
492
+ .None
493
+ else if (duration == 0) .ended else .continuing);
498
494
 
499
495
  if (duration == 0) {
500
496
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +528,12 @@ fn beforeMove(
532
528
  }
533
529
 
534
530
  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);
531
+ volatiles.disable_duration =
532
+ decrement(.disable, player, options, volatiles.disable_duration);
533
+ try options.chance.disable(
534
+ player,
535
+ if (volatiles.disable_duration == 0) .ended else .continuing,
536
+ );
541
537
 
542
538
  if (volatiles.disable_duration == 0) {
543
539
  volatiles.disable_move = 0;
@@ -556,13 +552,18 @@ fn beforeMove(
556
552
 
557
553
  if (volatiles.Confusion) {
558
554
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
555
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
556
+ .started, .ended => volatiles.confusion = 0,
557
+ .continuing => if (volatiles.confusion > 1) {
558
+ volatiles.confusion -= 1;
559
+ },
560
+ .overwritten => {},
561
+ else => unreachable,
562
562
  } else {
563
563
  volatiles.confusion -= 1;
564
564
  }
565
- try options.chance.confusion(player, volatiles.confusion);
565
+
566
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
567
 
567
568
  if (volatiles.confusion == 0) {
568
569
  volatiles.Confusion = false;
@@ -580,7 +581,9 @@ fn beforeMove(
580
581
  volatiles.Charging = false;
581
582
  volatiles.Binding = false;
582
583
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
584
+
585
+ options.chance.observe(.attacking, player, .None);
586
+ options.chance.observe(.binding, player, .None);
584
587
  {
585
588
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
589
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +591,9 @@ fn beforeMove(
588
591
  const def = foe.active.stats.def;
589
592
  foe.active.stats.def = active.stats.def;
590
593
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
594
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
595
+ return .err;
596
+ }
592
597
  }
593
598
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
599
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +622,10 @@ fn beforeMove(
617
622
  volatiles.Thrashing = false;
618
623
  volatiles.Charging = false;
619
624
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
625
+
626
+ options.chance.observe(.attacking, player, .None);
627
+ options.chance.observe(.binding, player, .None);
628
+
621
629
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
630
  try log.cant(.{ ident, .Paralysis });
623
631
  return .done;
@@ -635,12 +643,8 @@ fn beforeMove(
635
643
  volatiles.state +%= battle.last_damage;
636
644
  }
637
645
 
638
- if (options.calc.modify(player, .attacking)) |extend| {
639
- if (!extend) volatiles.attacks = 0;
640
- } else {
641
- volatiles.attacks -= 1;
642
- }
643
- try options.chance.attacking(player, volatiles.attacks);
646
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
647
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
648
 
645
649
  if (volatiles.attacks != 0) {
646
650
  try log.activate(.{ ident, .Bide });
@@ -674,35 +678,40 @@ fn beforeMove(
674
678
  }
675
679
 
676
680
  if (volatiles.Thrashing) {
677
- if (options.calc.modify(player, .attacking)) |extend| {
678
- if (!extend) volatiles.attacks = 0;
679
- } else {
680
- volatiles.attacks -= 1;
681
- }
682
- try options.chance.attacking(player, volatiles.attacks);
681
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
682
+
683
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
684
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
683
685
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
686
+ if (volatiles.attacks == 0) {
687
+ const overwritten = volatiles.Confusion;
688
+
689
+ volatiles.Thrashing = false;
690
+ volatiles.Confusion = true;
691
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
692
+
693
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
694
+ // ended (the engine's protocol is always from the omniscient perspective and clients
695
+ // can be expected to recognize this pattern and pass the information only to the
696
+ // correct player)
685
697
  try log.start(.{ battle.active(player), .ConfusionSilent });
698
+ options.chance.observe(.confusion, player, if (overwritten)
699
+ if (pkmn.options.overwrite) .overwritten else .continuing
700
+ else
701
+ .started);
702
+ if (!overwritten) options.calc.confusion(player);
686
703
  }
687
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active, player, options)) {
693
- try log.start(.{ battle.active(player), .ConfusionSilent });
694
- }
695
- }
704
+
705
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
706
+ // used in useMove and doesn't have the notion of skipping canMove semantics
707
+ if (showdown) side.last_used_move = side.last_selected_move;
708
+
696
709
  return .skip_can;
697
710
  }
698
711
 
699
712
  if (volatiles.Binding) {
700
- if (options.calc.modify(player, .binding)) |extend| {
701
- if (!extend) volatiles.attacks = 0;
702
- } else {
703
- volatiles.attacks -= 1;
704
- }
705
- try options.chance.binding(player, volatiles.attacks);
713
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
714
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
715
 
707
716
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
717
  if (showdown or battle.last_damage != 0) {
@@ -795,7 +804,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
804
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
805
  }
797
806
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
807
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
808
  var active = &side.active;
800
809
  const volatiles = &active.volatiles;
801
810
 
@@ -803,8 +812,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
812
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
813
  if (showdown and volatiles.Transform) return;
805
814
 
815
+ const n = options.calc.overridden(player, .pp) orelse 1;
816
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
817
+
806
818
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
819
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
820
  }
809
821
 
810
822
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,13 +864,13 @@ fn doMove(
852
864
  break :miss true;
853
865
  }
854
866
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
867
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
868
  };
857
869
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
870
 
859
871
  var late = showdown and move.effect != .Explode;
860
872
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
873
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
874
  if (showdown and move.effect.isMulti()) {
863
875
  try Effects.multiHit(battle, player, move, options);
864
876
  hits = side.active.volatiles.attacks;
@@ -880,8 +892,9 @@ fn doMove(
880
892
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
893
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
894
  battle.last_damage = if (ohko) 65535 else 0;
883
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
884
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
895
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
896
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
897
+ if (!showdown) try options.chance.commit(player, .err);
885
898
  return @as(?Result, Result.Error);
886
899
  }
887
900
  if (battle.last_damage == 0) {
@@ -895,11 +908,12 @@ fn doMove(
895
908
  }
896
909
  }
897
910
 
911
+ var missed: bool = undefined;
898
912
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
913
+ if (!showdown and !skip) {
914
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
915
+ miss = missed or zero;
916
+ }
903
917
 
904
918
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
919
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +940,32 @@ fn doMove(
926
940
  try log.immune(.{ foe_ident, .OHKO });
927
941
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
942
  try log.immune(.{ foe_ident, .None });
943
+ if (!showdown) {
944
+ if (move.effect == .Binding) {
945
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
946
+ // then simply clears it later on if the move happens to miss in moveHit.
947
+ // However, moveHit can return true and we'll still end up in the outer miss
948
+ // handling block because attacking into an immune Pokemon does zero damage
949
+ // (considered missing)
950
+ assert(zero);
951
+ // Thus we still need to actually save the hit result (but not the crit roll).
952
+ // Using .miss for this commit would accomplish what we want, but is somewhat
953
+ // misleading as its entirely possible the move actually did hit (and the target
954
+ // is now trapped) so we use the .binding value instead which is effectively
955
+ // identical, just doesn't claim to know about the actual results of the hit
956
+ // roll)
957
+ try options.chance.commit(player, .binding);
958
+ } else {
959
+ // The .glitch option allows for conditionally committing if we are in a
960
+ // division-by-zero freeze alternative timeline scenario
961
+ try options.chance.commit(player, .glitch);
962
+ }
963
+ }
929
964
  } else if (mist) {
930
965
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
966
  try log.fail(.{ foe_ident, .None });
932
967
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
968
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
969
  try log.lastmiss(.{});
935
970
  try log.miss(.{battle.active(player)});
936
971
  }
@@ -945,17 +980,15 @@ fn doMove(
945
980
  try buildRage(battle, player.foe(), options);
946
981
  } else if (showdown and move.effect == .Disable) {
947
982
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
983
  }
951
984
  return null;
952
- } else {
985
+ } else if (!showdown) {
953
986
  try options.chance.commit(player, .hit);
954
987
  }
955
988
 
956
989
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
990
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
991
+ // it up above before damage calculation)
959
992
  if (!showdown and move.effect.isMulti()) {
960
993
  try Effects.multiHit(battle, player, move, options);
961
994
  hits = side.active.volatiles.attacks;
@@ -994,32 +1027,30 @@ fn doMove(
994
1027
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1028
  }
996
1029
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1030
+ } else if (showdown and move.effect == .Binding) {
998
1031
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
999
- if (move.effect == .Binding) {
1000
- try Effects.binding(battle, player, rewrap, options);
1001
- if (immune) {
1002
- battle.last_damage = 0;
1003
- assert(foe.stored().hp > 0);
1004
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1005
- if (sub) {
1006
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
- } else {
1008
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
- try buildRage(battle, player.foe(), options);
1010
- }
1011
- return null;
1032
+ try Effects.binding(battle, player, rewrap, options);
1033
+ if (immune) {
1034
+ battle.last_damage = 0;
1035
+ assert(foe.stored().hp > 0);
1036
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1037
+ if (sub) {
1038
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1039
+ } else {
1040
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1041
+ try buildRage(battle, player.foe(), options);
1012
1042
  }
1043
+ return null;
1013
1044
  }
1014
1045
  }
1015
1046
 
1016
1047
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1048
+ // if an effect was intended to "always happen" it will still get skipped
1018
1049
  if (nullified) return null;
1019
1050
 
1020
1051
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1052
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1053
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1054
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1055
 
1025
1056
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1065,7 @@ fn doMove(
1034
1065
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1066
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1067
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1068
+ // puts the |-status| message before |-hitcount| instead of after
1038
1069
  if (move.effect == .Twineedle) {
1039
1070
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1071
  } else if (move.effect == .Disable) {
@@ -1058,7 +1089,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1089
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1090
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1091
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1092
+ const spurious =
1093
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1094
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1095
  }
1063
1096
 
1064
1097
  fn calcDamage(
@@ -1067,6 +1100,7 @@ fn calcDamage(
1067
1100
  target_player: Player,
1068
1101
  m: ?Move.Data,
1069
1102
  crit: bool,
1103
+ options: anytype,
1070
1104
  ) bool {
1071
1105
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1106
  const cfz = m == null;
@@ -1078,28 +1112,8 @@ fn calcDamage(
1078
1112
 
1079
1113
  const special = move.type.special();
1080
1114
 
1081
- // zig fmt: off
1082
- var atk: u32 =
1083
- if (crit)
1084
- if (special) side.stored().stats.spc
1085
- else side.stored().stats.atk
1086
- else
1087
- if (special) side.active.stats.spc
1088
- else side.active.stats.atk;
1089
- var def: u32 =
1090
- if (crit)
1091
- if (special) target.stored().stats.spc
1092
- else target.stored().stats.def
1093
- else
1094
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1095
- if (special)
1096
- target.active.stats.spc *
1097
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1098
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1099
- else
1100
- target.active.stats.def *
1101
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1102
- // zig fmt: on
1115
+ var atk = attack(side, crit, special);
1116
+ var def = defense(target, crit, special, cfz);
1103
1117
 
1104
1118
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1119
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1124,11 +1138,41 @@ fn calcDamage(
1124
1138
  d = @min(997, d);
1125
1139
  d += 2;
1126
1140
 
1141
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1142
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1143
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1144
+ // that results in counter intuitive probabilities in the common case
1145
+ if (pkmn.options.chance and !showdown) {
1146
+ atk = attack(side, !crit, special);
1147
+ def = defense(target, !crit, special, false);
1148
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1149
+ }
1150
+
1127
1151
  battle.last_damage = @intCast(d);
1128
1152
 
1129
1153
  return true;
1130
1154
  }
1131
1155
 
1156
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1157
+ return if (crit)
1158
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1159
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1160
+ }
1161
+
1162
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1163
+ return if (crit)
1164
+ if (special) target.stored().stats.spc else target.stored().stats.def
1165
+ else
1166
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1167
+ if (special)
1168
+ target.active.stats.spc *
1169
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1170
+ else
1171
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1172
+ target.active.stats.def *
1173
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1174
+ }
1175
+
1132
1176
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1177
  const side = battle.side(player);
1134
1178
  const foe = battle.foe(player);
@@ -1173,7 +1217,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1217
  const foe = battle.foe(player);
1174
1218
 
1175
1219
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1220
 
1178
1221
  battle.last_damage = switch (side.last_selected_move) {
1179
1222
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1277,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1277
 
1235
1278
  battle.last_damage *|= 2;
1236
1279
 
1237
- // Pokémon Showdown calls checkHit before Counter
1280
+ // Pokémon Showdown calls moveHit before Counter
1238
1281
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1282
 
1241
1283
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1284
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1307,7 @@ fn applyDamage(
1265
1307
  subbed.active.volatiles.Substitute = false;
1266
1308
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1309
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1310
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1311
  return true;
1270
1312
  } else {
1271
1313
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1341,7 @@ fn mirrorMove(
1299
1341
  try options.log.fail(.{ battle.active(player), .None });
1300
1342
  return null;
1301
1343
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1344
+ incrementPP(side, player, mslot, options);
1303
1345
  }
1304
1346
 
1305
1347
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1362,7 @@ fn metronome(
1320
1362
  var side = battle.side(player);
1321
1363
 
1322
1364
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1365
+ incrementPP(side, player, mslot, options);
1324
1366
 
1325
1367
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1368
  return null;
@@ -1332,7 +1374,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1374
  var immune = false;
1333
1375
  var mist = false;
1334
1376
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1377
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1378
+ if (!showdown) try options.chance.commit(player, .hit);
1379
+ return true;
1380
+ }
1336
1381
 
1337
1382
  assert(!immune);
1338
1383
  if (mist) {
@@ -1341,11 +1386,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1386
  try options.log.activate(.{ foe_ident, .Mist });
1342
1387
  try options.log.fail(.{ foe_ident, .None });
1343
1388
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1389
  try options.log.lastmiss(.{});
1346
1390
  try options.log.miss(.{battle.active(player)});
1347
1391
  }
1348
1392
 
1393
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1394
  return false;
1350
1395
  }
1351
1396
 
@@ -1356,7 +1401,7 @@ fn moveHit(
1356
1401
  immune: *bool,
1357
1402
  mist: *bool,
1358
1403
  options: anytype,
1359
- ) bool {
1404
+ ) !bool {
1360
1405
  var side = battle.side(player);
1361
1406
  const foe = battle.foe(player);
1362
1407
 
@@ -1407,7 +1452,7 @@ fn moveHit(
1407
1452
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1453
 
1409
1454
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1455
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1456
  };
1412
1457
 
1413
1458
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1463,17 @@ fn moveHit(
1418
1463
 
1419
1464
  if (!miss) return true;
1420
1465
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1466
+
1421
1467
  side.active.volatiles.Binding = false;
1468
+ options.chance.observe(.binding, player, .None);
1469
+
1422
1470
  return false;
1423
1471
  }
1424
1472
 
1425
1473
  fn checkFaint(
1426
1474
  battle: anytype,
1427
1475
  player: Player,
1476
+ cap: bool,
1428
1477
  options: anytype,
1429
1478
  ) @TypeOf(options.log).Error!?Result {
1430
1479
  const side = battle.side(player);
@@ -1437,8 +1486,8 @@ fn checkFaint(
1437
1486
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1487
  const more = player_out or foe_out;
1439
1488
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1489
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1490
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1491
 
1443
1492
  assert(!side.active.volatiles.MultiHit);
1444
1493
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1508,7 @@ fn checkFaint(
1459
1508
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1509
  }
1461
1510
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1511
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1512
  var side = battle.side(player);
1464
1513
  var foe = battle.foe(player);
1465
1514
  assert(side.stored().hp == 0);
@@ -1493,7 +1542,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1542
  }
1494
1543
 
1495
1544
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1545
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1546
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1547
+ if (cap) options.calc.capped(player);
1497
1548
  return null;
1498
1549
  }
1499
1550
 
@@ -1653,17 +1704,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1704
  }
1654
1705
  }
1655
1706
 
1656
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1657
- var volatiles = &active.volatiles;
1658
- assert(volatiles.Thrashing);
1659
- if (volatiles.attacks > 0) return false;
1660
-
1661
- volatiles.Thrashing = false;
1662
- volatiles.Confusion = true;
1663
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1664
- return true;
1665
- }
1666
-
1667
1707
  fn onBegin(
1668
1708
  battle: anytype,
1669
1709
  player: Player,
@@ -1750,8 +1790,10 @@ pub const Effects = struct {
1750
1790
 
1751
1791
  side.active.volatiles.Bide = true;
1752
1792
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1793
+
1753
1794
  side.active.volatiles.state = 0;
1754
1795
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1796
+ options.chance.observe(.attacking, player, .started);
1755
1797
 
1756
1798
  try options.log.start(.{ battle.active(player), .Bide });
1757
1799
  }
@@ -1813,12 +1855,14 @@ pub const Effects = struct {
1813
1855
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1856
  if (!try checkHit(battle, player, move, options)) return;
1815
1857
  }
1816
- try options.chance.commit(player, .hit);
1817
1858
  }
1818
1859
 
1819
1860
  if (foe.active.volatiles.Confusion) return;
1820
1861
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1862
+
1863
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1864
+ options.chance.observe(.confusion, player.foe(), .started);
1865
+ options.calc.confusion(player.foe());
1822
1866
 
1823
1867
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1868
  }
@@ -1846,10 +1890,7 @@ pub const Effects = struct {
1846
1890
  const foe_ident = battle.active(player.foe());
1847
1891
 
1848
1892
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1893
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1894
 
1854
1895
  if (volatiles.disable_move != 0) {
1855
1896
  try options.log.fail(.{ foe_ident, .None });
@@ -1879,6 +1920,7 @@ pub const Effects = struct {
1879
1920
  volatiles.disable_move =
1880
1921
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1922
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1923
+ options.chance.observe(.disable, player.foe(), .started);
1882
1924
 
1883
1925
  const id = foe.active.move(volatiles.disable_move).id;
1884
1926
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1985,22 +2027,24 @@ pub const Effects = struct {
1985
2027
  if (p != player and Status.any(s.stored().status)) {
1986
2028
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2029
  s.stored().status = 0;
2030
+ options.chance.observe(.sleep, p, .None);
1988
2031
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2032
  s.stored().status = Status.init(.PSN);
1990
2033
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2034
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2035
+ try clearVolatiles(battle, p, options);
1993
2036
  }
1994
2037
  } else {
1995
2038
  if (Status.any(foe_stored.status)) {
1996
2039
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2040
  foe.last_selected_move = .SKIP_TURN;
2041
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2042
  }
1999
2043
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2044
  foe_stored.status = 0;
2001
2045
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2046
+ try clearVolatiles(battle, player, options);
2047
+ try clearVolatiles(battle, player.foe(), options);
2004
2048
  }
2005
2049
  }
2006
2050
 
@@ -2051,7 +2095,6 @@ pub const Effects = struct {
2051
2095
  }
2052
2096
  }
2053
2097
 
2054
- try options.chance.commit(player, .hit);
2055
2098
  foe.active.volatiles.LeechSeed = true;
2056
2099
 
2057
2100
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2135,10 @@ pub const Effects = struct {
2092
2135
  if (!has_mimic) {
2093
2136
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2137
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2138
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2139
  }
2098
2140
  }
2099
2141
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2142
 
2102
2143
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2144
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2192,6 @@ pub const Effects = struct {
2151
2192
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2193
  if (!try checkHit(battle, player, move, options)) return;
2153
2194
  }
2154
- try options.chance.commit(player, .hit);
2155
2195
 
2156
2196
  foe_stored.status = Status.init(.PAR);
2157
2197
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2259,6 @@ pub const Effects = struct {
2219
2259
 
2220
2260
  if (move.effect == .Poison) {
2221
2261
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2262
  } else {
2224
2263
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2264
  return;
@@ -2288,9 +2327,8 @@ pub const Effects = struct {
2288
2327
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2328
  });
2290
2329
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2330
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2331
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2332
  }
2295
2333
 
2296
2334
  // Sleep Clause Mod
@@ -2302,6 +2340,8 @@ pub const Effects = struct {
2302
2340
  foe.active.volatiles.Recharging = false;
2303
2341
 
2304
2342
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2343
+ options.chance.observe(.sleep, player.foe(), .started);
2344
+
2305
2345
  const last = battle.side(player).last_selected_move;
2306
2346
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2347
  }
@@ -2343,9 +2383,7 @@ pub const Effects = struct {
2343
2383
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2384
  if (showdown) {
2345
2385
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2386
+ _ = try checkHit(battle, player, move, options);
2349
2387
  battle.last_damage = 0;
2350
2388
  } else {
2351
2389
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2398,8 @@ pub const Effects = struct {
2360
2398
 
2361
2399
  volatiles.Thrashing = true;
2362
2400
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2401
+
2402
+ options.chance.observe(.attacking, player, .started);
2363
2403
  }
2364
2404
 
2365
2405
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2405,6 +2445,7 @@ pub const Effects = struct {
2405
2445
 
2406
2446
  side.active.volatiles.attacks =
2407
2447
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2448
+ options.chance.observe(.binding, player, .started);
2408
2449
  }
2409
2450
 
2410
2451
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2562,6 @@ pub const Effects = struct {
2521
2562
  } else {
2522
2563
  // checkHit already checks for Invulnerable
2523
2564
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2565
  }
2526
2566
 
2527
2567
  var stats = &foe.active.stats;
@@ -2635,22 +2675,22 @@ fn isForced(active: anytype) bool {
2635
2675
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2676
  }
2637
2677
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2678
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2679
  var log = options.log;
2640
2680
  var side = battle.side(who);
2641
2681
  var volatiles = &side.active.volatiles;
2642
2682
  const ident = battle.active(who);
2643
2683
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2684
  if (volatiles.disable_move != 0) {
2647
2685
  volatiles.disable_move = 0;
2648
2686
  volatiles.disable_duration = 0;
2687
+ options.chance.observe(.disable, who, .None);
2649
2688
  try log.end(.{ ident, .DisableSilent });
2650
2689
  }
2651
2690
  if (volatiles.Confusion) {
2652
2691
  // volatiles.confusion is left unchanged
2653
2692
  volatiles.Confusion = false;
2693
+ options.chance.observe(.confusion, who, .None);
2654
2694
  try log.end(.{ ident, .ConfusionSilent });
2655
2695
  }
2656
2696
  if (volatiles.Mist) {
@@ -2685,6 +2725,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2725
  }
2686
2726
  }
2687
2727
 
2728
+ fn decrement(
2729
+ comptime field: chance.Action.Field,
2730
+ player: Player,
2731
+ options: anytype,
2732
+ n: anytype,
2733
+ ) @TypeOf(n) {
2734
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2735
+ .started, .ended => 0,
2736
+ .continuing => if (n > 1) n - 1 else n,
2737
+ else => unreachable,
2738
+ } else n - 1;
2739
+ }
2740
+
2688
2741
  pub const Rolls = struct {
2689
2742
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2743
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2700,7 +2753,13 @@ pub const Rolls = struct {
2700
2753
  return p1;
2701
2754
  }
2702
2755
 
2703
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2756
+ fn criticalHit(
2757
+ battle: anytype,
2758
+ player: Player,
2759
+ rate: u8,
2760
+ spurious: bool,
2761
+ options: anytype,
2762
+ ) !bool {
2704
2763
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
2764
  val == .true
2706
2765
  else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
@@ -2710,7 +2769,7 @@ pub const Rolls = struct {
2710
2769
  else
2711
2770
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
2771
 
2713
- try options.chance.criticalHit(player, crit, rate);
2772
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2714
2773
  return crit;
2715
2774
  }
2716
2775
 
@@ -2732,17 +2791,19 @@ pub const Rolls = struct {
2732
2791
  return roll;
2733
2792
  }
2734
2793
 
2735
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2794
+ const uN = if (pkmn.options.miss) u8 else u9;
2795
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2796
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2736
2797
  const ok = if (options.calc.overridden(player, .hit)) |val|
2737
2798
  val == .true
2738
2799
  else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
- battle.rng.hit(player, accuracy)
2800
+ battle.rng.hit(player, acc)
2740
2801
  else if (showdown)
2741
- battle.rng.chance(u8, accuracy, 256)
2802
+ battle.rng.chance(uN, acc, 256)
2742
2803
  else
2743
- battle.rng.next() < accuracy;
2804
+ battle.rng.next() < acc;
2744
2805
 
2745
- options.chance.hit(ok, accuracy);
2806
+ try options.chance.hit(player, ok, acc);
2746
2807
  return ok;
2747
2808
  }
2748
2809
 
@@ -2874,9 +2935,10 @@ pub const Rolls = struct {
2874
2935
  }
2875
2936
 
2876
2937
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2877
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2878
- @intCast(val)
2879
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2938
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2939
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2940
+ else
2941
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
2942
  battle.rng.sleepDuration(player)
2881
2943
  else if (showdown)
2882
2944
  battle.rng.range(u3, 1, 8)
@@ -2888,14 +2950,15 @@ pub const Rolls = struct {
2888
2950
  };
2889
2951
 
2890
2952
  assert(duration >= 1 and duration <= 7);
2891
- options.chance.duration(.sleep, player, player.foe(), duration);
2953
+ options.chance.duration(player, duration);
2892
2954
  return duration;
2893
2955
  }
2894
2956
 
2895
2957
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2896
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2897
- val
2898
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2958
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2959
+ if (val >= 1 and val <= 8) val else null
2960
+ else
2961
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
2962
  battle.rng.disableDuration(player)
2900
2963
  else if (showdown)
2901
2964
  battle.rng.range(u4, 1, 9)
@@ -2903,29 +2966,31 @@ pub const Rolls = struct {
2903
2966
  @intCast((battle.rng.next() & 7) + 1);
2904
2967
 
2905
2968
  assert(duration >= 1 and duration <= 8);
2906
- options.chance.duration(.disable, player, player.foe(), duration);
2969
+ options.chance.duration(player, duration);
2907
2970
  return duration;
2908
2971
  }
2909
2972
 
2910
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2911
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2912
- @intCast(val)
2913
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
- battle.rng.confusionDuration(player, self)
2973
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2974
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2975
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2976
+ else
2977
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2978
+ battle.rng.confusionDuration(player)
2915
2979
  else if (showdown)
2916
2980
  battle.rng.range(u3, 2, 6)
2917
2981
  else
2918
2982
  @intCast((battle.rng.next() & 3) + 2);
2919
2983
 
2920
2984
  assert(duration >= 2 and duration <= 5);
2921
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2985
+ options.chance.duration(player, duration);
2922
2986
  return duration;
2923
2987
  }
2924
2988
 
2925
2989
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2926
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2927
- @intCast(val)
2928
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2990
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2991
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
2992
+ else
2993
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
2994
  battle.rng.attackingDuration(player)
2930
2995
  else if (showdown)
2931
2996
  battle.rng.range(u3, 2, 4)
@@ -2933,7 +2998,7 @@ pub const Rolls = struct {
2933
2998
  @intCast((battle.rng.next() & 1) + 2);
2934
2999
 
2935
3000
  assert(duration >= 2 and duration <= 3);
2936
- options.chance.duration(.attacking, player, player, duration);
3001
+ options.chance.duration(player, duration);
2937
3002
  return duration;
2938
3003
  }
2939
3004
 
@@ -2946,9 +3011,10 @@ pub const Rolls = struct {
2946
3011
  options: anytype,
2947
3012
  ) !u3 {
2948
3013
  const roll = if (effect == .Binding) .duration else .multi_hit;
2949
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2950
- @intCast(val)
2951
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3014
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3015
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
3018
  battle.rng.distribution(player)
2953
3019
  else if (showdown)
2954
3020
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2959,7 +3025,7 @@ pub const Rolls = struct {
2959
3025
 
2960
3026
  assert(n >= 2 and n <= 5);
2961
3027
  if (effect == .Binding) {
2962
- options.chance.duration(.binding, player, player, n);
3028
+ options.chance.duration(player, n);
2963
3029
  } else {
2964
3030
  try options.chance.multiHit(player, n);
2965
3031
  }
@@ -2974,15 +3040,13 @@ pub const Rolls = struct {
2974
3040
  options: anytype,
2975
3041
  ) !u4 {
2976
3042
  // TODO: consider throwing error instead of rerolling?
2977
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3043
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2978
3044
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2979
3045
  val
2980
3046
  else
2981
3047
  null
2982
3048
  else
2983
- null;
2984
-
2985
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3049
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
3050
  battle.rng.moveSlot(player, moves, check_pp)
2987
3051
  else slot: {
2988
3052
  if (showdown) {
@@ -3068,7 +3132,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3132
  const stored = side.stored();
3069
3133
 
3070
3134
  // While players are not given any input options on the cartridge in these cases,
3071
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3135
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3072
3136
  //
3073
3137
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3074
3138
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3076,7 +3140,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3076
3140
  // passing due to being forced into using a move. Instead of introducing another option
3077
3141
  // we simply repurpose Move with no move slot, even though pedantically this is not
3078
3142
  // strictly correct as the player would not have been presented the option to move or
3079
- // switch at all.
3143
+ // switch at all
3080
3144
  if (isForced(active)) {
3081
3145
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3082
3146
  n += 1;
@@ -3095,7 +3159,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3095
3159
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3096
3160
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3097
3161
  // either disables all other moves in the case of limited or requires you to select a
3098
- // move normally if sleeping/frozen/bound.
3162
+ // move normally if sleeping/frozen/bound
3099
3163
  if (!showdown and (limited or foe.active.volatiles.Binding or
3100
3164
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3101
3165
  {
@@ -3108,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3172
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3109
3173
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3110
3174
  // Move will not result in forcing the subsequent use unless the user also had the
3111
- // proc-ed move in their moveset) and disabling all other moves.
3175
+ // proc-ed move in their moveset) and disabling all other moves
3112
3176
  if (limited) {
3113
3177
  assert(showdown);
3114
3178
  assert(side.last_selected_move != .None);