@pkmn/engine 0.1.0-dev.5570b5c5 → 0.1.0-dev.565de616

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 (76) hide show
  1. package/README.md +12 -8
  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 +29 -51
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/data.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +10 -13
  19. package/build/pkg/gen1.js +1 -1
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +53 -12
  22. package/build/pkg/index.js +15 -10
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +35 -68
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +141 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +141 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +349 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +18 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +230 -74
  38. package/package.json +51 -31
  39. package/src/bin/install-pkmn-engine +8 -6
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/common/array.zig +77 -0
  42. package/src/lib/common/data.zig +8 -8
  43. package/src/lib/common/debug.zig +39 -8
  44. package/src/lib/common/js.zig +4 -2
  45. package/src/lib/common/optional.zig +13 -9
  46. package/src/lib/common/options.zig +6 -0
  47. package/src/lib/common/protocol.zig +45 -44
  48. package/src/lib/common/rational.zig +121 -29
  49. package/src/lib/common/rng.zig +5 -7
  50. package/src/lib/common/util.zig +50 -1
  51. package/src/lib/gen1/calc.zig +683 -156
  52. package/src/lib/gen1/chance.zig +415 -291
  53. package/src/lib/gen1/data/moves.zig +2 -4
  54. package/src/lib/gen1/data/species.zig +1 -3
  55. package/src/lib/gen1/data/types.zig +1 -2
  56. package/src/lib/gen1/data.zig +38 -41
  57. package/src/lib/gen1/helpers.zig +92 -548
  58. package/src/lib/gen1/mechanics.zig +325 -235
  59. package/src/lib/gen2/calc.zig +37 -27
  60. package/src/lib/gen2/chance.zig +90 -503
  61. package/src/lib/gen2/data/items.zig +1 -3
  62. package/src/lib/gen2/data/moves.zig +2 -4
  63. package/src/lib/gen2/data/species.zig +1 -3
  64. package/src/lib/gen2/data/types.zig +1 -2
  65. package/src/lib/gen2/data.zig +26 -29
  66. package/src/lib/gen2/generated.zig +18 -23
  67. package/src/lib/gen2/helpers.zig +46 -25
  68. package/src/lib/gen2/mechanics.zig +69 -107
  69. package/src/lib/node.zig +2 -4
  70. package/src/lib/pkmn.zig +14 -16
  71. package/src/lib/wasm.zig +1 -2
  72. package/src/tools/display/dom.ts +96 -0
  73. package/src/tools/display/ui.css +354 -0
  74. package/build/test/display.js +0 -400
  75. package/build/test/display.js.map +0 -1
  76. 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);
@@ -174,7 +165,10 @@ fn selectMove(
174
165
  if (foe_choice.type == .Switch) {
175
166
  const last = side.active.move(battle.lastMove(player).index);
176
167
  if (last.id == .Metronome) side.last_selected_move = last.id;
177
- if (last.id == .MirrorMove) return Result.Error;
168
+ if (last.id == .MirrorMove) {
169
+ if (!pkmn.options.mod) return Result.Error;
170
+ side.last_selected_move = last.id;
171
+ }
178
172
  }
179
173
  }
180
174
  return null;
@@ -253,11 +247,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
247
  active.moves = incoming.moves;
254
248
 
255
249
  statusModify(incoming.status, &active.stats);
250
+ options.chance.switched(player, slot);
256
251
 
257
252
  foe.active.volatiles.Binding = false;
258
253
 
259
254
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
255
 
262
256
  if (showdown and incoming.status == Status.TOX) {
263
257
  incoming.status = Status.init(.PSN);
@@ -347,13 +341,13 @@ fn doTurn(
347
341
  )) |r| return r;
348
342
  if (!replace) {
349
343
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
344
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
345
  }
352
346
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
347
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
348
  } else if (foe_choice.type == .Pass) return null;
355
349
 
356
- options.chance.clearPending();
350
+ if (!showdown) options.chance.clearPending();
357
351
 
358
352
  residual = true;
359
353
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +363,10 @@ fn doTurn(
369
363
  )) |r| return r;
370
364
  if (!replace) {
371
365
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
366
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
367
  }
374
368
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
369
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
370
  }
377
371
 
378
372
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -425,13 +419,13 @@ fn executeMove(
425
419
  mslot = @intCast(battle.lastMove(player).index);
426
420
  const stored = side.stored();
427
421
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
428
- auto = isForced(&side.active) or
422
+ auto = pkmn.options.mod or isForced(&side.active) or
429
423
  side.active.volatiles.Binding or side.active.volatiles.Bide or
430
424
  side.last_selected_move == .HyperBeam or
431
425
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
426
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
427
  // 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.
428
+ // index 1 and the current Pokémon has the same move in its first slot
435
429
  if (!auto) {
436
430
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
431
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -458,6 +452,25 @@ fn executeMove(
458
452
  .err => return @as(?Result, Result.Error),
459
453
  }
460
454
 
455
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
456
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
457
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
458
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
459
+ side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
460
+ {
461
+ assert(mslot == 1);
462
+ const has_beam = has_beam: {
463
+ for (side.active.moves, 0..) |m, i| {
464
+ if (m.id == .HyperBeam) {
465
+ mslot = @intCast(i + 1);
466
+ break :has_beam true;
467
+ }
468
+ }
469
+ break :has_beam false;
470
+ };
471
+ if (!has_beam) skip_pp = true;
472
+ }
473
+
461
474
  const can = skip_can or
462
475
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
476
  if (!can) return null;
@@ -484,17 +497,22 @@ fn beforeMove(
484
497
 
485
498
  if (Status.is(stored.status, .SLP)) {
486
499
  const before = stored.status;
500
+ const slf = Status.is(stored.status, .EXT);
487
501
  // 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;
502
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
503
+ .started, .ended => stored.status = 0,
504
+ .continuing => if (Status.duration(stored.status) > 1) {
505
+ stored.status -= 1;
506
+ },
507
+ else => unreachable,
490
508
  } else {
491
509
  stored.status -= 1;
492
510
  }
493
511
 
494
512
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
513
+ try options.chance.sleep(player, if (slf)
514
+ .None
515
+ else if (duration == 0) .ended else .continuing);
498
516
 
499
517
  if (duration == 0) {
500
518
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +550,12 @@ fn beforeMove(
532
550
  }
533
551
 
534
552
  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);
553
+ volatiles.disable_duration =
554
+ decrement(.disable, player, options, volatiles.disable_duration);
555
+ try options.chance.disable(
556
+ player,
557
+ if (volatiles.disable_duration == 0) .ended else .continuing,
558
+ );
541
559
 
542
560
  if (volatiles.disable_duration == 0) {
543
561
  volatiles.disable_move = 0;
@@ -550,19 +568,24 @@ fn beforeMove(
550
568
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
551
569
  // before the side had a selected a move - we simply need to assume this leads to a desync
552
570
  if (side.last_selected_move == .None) {
553
- assert(!showdown);
571
+ assert(!pkmn.options.mod);
554
572
  return .err;
555
573
  }
556
574
 
557
575
  if (volatiles.Confusion) {
558
576
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
577
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
578
+ .started, .ended => volatiles.confusion = 0,
579
+ .continuing => if (volatiles.confusion > 1) {
580
+ volatiles.confusion -= 1;
581
+ },
582
+ .overwritten => {},
583
+ else => unreachable,
562
584
  } else {
563
585
  volatiles.confusion -= 1;
564
586
  }
565
- try options.chance.confusion(player, volatiles.confusion);
587
+
588
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
589
 
567
590
  if (volatiles.confusion == 0) {
568
591
  volatiles.Confusion = false;
@@ -580,7 +603,9 @@ fn beforeMove(
580
603
  volatiles.Charging = false;
581
604
  volatiles.Binding = false;
582
605
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
606
+
607
+ options.chance.observe(.attacking, player, .None);
608
+ options.chance.observe(.binding, player, .None);
584
609
  {
585
610
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
611
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +613,9 @@ fn beforeMove(
588
613
  const def = foe.active.stats.def;
589
614
  foe.active.stats.def = active.stats.def;
590
615
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
616
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
617
+ return .err;
618
+ }
592
619
  }
593
620
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
621
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +644,10 @@ fn beforeMove(
617
644
  volatiles.Thrashing = false;
618
645
  volatiles.Charging = false;
619
646
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
647
+
648
+ options.chance.observe(.attacking, player, .None);
649
+ options.chance.observe(.binding, player, .None);
650
+
621
651
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
652
  try log.cant(.{ ident, .Paralysis });
623
653
  return .done;
@@ -635,12 +665,8 @@ fn beforeMove(
635
665
  volatiles.state +%= battle.last_damage;
636
666
  }
637
667
 
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);
668
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
669
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
670
 
645
671
  if (volatiles.attacks != 0) {
646
672
  try log.activate(.{ ident, .Bide });
@@ -674,35 +700,40 @@ fn beforeMove(
674
700
  }
675
701
 
676
702
  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);
703
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
704
+
705
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
706
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
683
707
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
708
+ if (volatiles.attacks == 0) {
709
+ const overwritten = volatiles.Confusion;
710
+
711
+ volatiles.Thrashing = false;
712
+ volatiles.Confusion = true;
713
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
714
+
715
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
716
+ // ended (the engine's protocol is always from the omniscient perspective and clients
717
+ // can be expected to recognize this pattern and pass the information only to the
718
+ // correct player)
685
719
  try log.start(.{ battle.active(player), .ConfusionSilent });
720
+ options.chance.observe(.confusion, player, if (overwritten)
721
+ if (pkmn.options.overwrite) .overwritten else .continuing
722
+ else
723
+ .started);
724
+ if (!overwritten) options.calc.confusion(player);
686
725
  }
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
- }
726
+
727
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
728
+ // used in useMove and doesn't have the notion of skipping canMove semantics
729
+ if (showdown) side.last_used_move = side.last_selected_move;
730
+
696
731
  return .skip_can;
697
732
  }
698
733
 
699
734
  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);
735
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
736
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
737
 
707
738
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
739
  if (showdown or battle.last_damage != 0) {
@@ -756,9 +787,14 @@ fn canMove(
756
787
 
757
788
  if (move.effect == .Thrashing) {
758
789
  Effects.thrashing(battle, player, options);
759
- } else if (!showdown and move.effect == .Binding) {
760
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
761
- try Effects.binding(battle, player, false, options);
790
+ } else if (move.effect == .Binding) {
791
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
792
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
793
+ if (showdown) {
794
+ battle.foe(player).active.volatiles.Recharging = false;
795
+ } else {
796
+ try Effects.binding(battle, player, false, options);
797
+ }
762
798
  }
763
799
 
764
800
  return true;
@@ -795,7 +831,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
831
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
832
  }
797
833
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
834
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
835
  var active = &side.active;
800
836
  const volatiles = &active.volatiles;
801
837
 
@@ -803,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
839
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
840
  if (showdown and volatiles.Transform) return;
805
841
 
842
+ const n = options.calc.overridden(player, .pp) orelse 1;
843
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
844
+
806
845
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
846
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
847
  }
809
848
 
810
849
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,13 +891,13 @@ fn doMove(
852
891
  break :miss true;
853
892
  }
854
893
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
894
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
895
  };
857
896
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
897
 
859
898
  var late = showdown and move.effect != .Explode;
860
899
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
900
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
901
  if (showdown and move.effect.isMulti()) {
863
902
  try Effects.multiHit(battle, player, move, options);
864
903
  hits = side.active.volatiles.attacks;
@@ -880,8 +919,9 @@ fn doMove(
880
919
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
920
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
921
  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)) {
922
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
923
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
924
+ if (!showdown) try options.chance.commit(player, .err);
885
925
  return @as(?Result, Result.Error);
886
926
  }
887
927
  if (battle.last_damage == 0) {
@@ -895,11 +935,12 @@ fn doMove(
895
935
  }
896
936
  }
897
937
 
938
+ var missed: bool = undefined;
898
939
  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);
940
+ if (!showdown and !skip) {
941
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
942
+ miss = missed or zero;
943
+ }
903
944
 
904
945
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
946
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +967,32 @@ fn doMove(
926
967
  try log.immune(.{ foe_ident, .OHKO });
927
968
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
969
  try log.immune(.{ foe_ident, .None });
970
+ if (!showdown) {
971
+ if (move.effect == .Binding) {
972
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
973
+ // then simply clears it later on if the move happens to miss in moveHit.
974
+ // However, moveHit can return true and we'll still end up in the outer miss
975
+ // handling block because attacking into an immune Pokemon does zero damage
976
+ // (considered missing)
977
+ assert(zero);
978
+ // Thus we still need to actually save the hit result (but not the crit roll).
979
+ // Using .miss for this commit would accomplish what we want, but is somewhat
980
+ // misleading as its entirely possible the move actually did hit (and the target
981
+ // is now trapped) so we use the .binding value instead which is effectively
982
+ // identical, just doesn't claim to know about the actual results of the hit
983
+ // roll)
984
+ try options.chance.commit(player, .binding);
985
+ } else {
986
+ // The .glitch option allows for conditionally committing if we are in a
987
+ // division-by-zero freeze alternative timeline scenario
988
+ try options.chance.commit(player, .glitch);
989
+ }
990
+ }
929
991
  } else if (mist) {
930
992
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
993
  try log.fail(.{ foe_ident, .None });
932
994
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
995
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
996
  try log.lastmiss(.{});
935
997
  try log.miss(.{battle.active(player)});
936
998
  }
@@ -945,17 +1007,15 @@ fn doMove(
945
1007
  try buildRage(battle, player.foe(), options);
946
1008
  } else if (showdown and move.effect == .Disable) {
947
1009
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
1010
  }
951
1011
  return null;
952
- } else {
1012
+ } else if (!showdown) {
953
1013
  try options.chance.commit(player, .hit);
954
1014
  }
955
1015
 
956
1016
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
1017
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
1018
+ // it up above before damage calculation)
959
1019
  if (!showdown and move.effect.isMulti()) {
960
1020
  try Effects.multiHit(battle, player, move, options);
961
1021
  hits = side.active.volatiles.attacks;
@@ -994,32 +1054,30 @@ fn doMove(
994
1054
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1055
  }
996
1056
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1057
+ } else if (showdown and move.effect == .Binding) {
998
1058
  // 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;
1059
+ try Effects.binding(battle, player, rewrap, options);
1060
+ if (immune) {
1061
+ battle.last_damage = 0;
1062
+ assert(foe.stored().hp > 0);
1063
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1064
+ if (sub) {
1065
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1066
+ } else {
1067
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1068
+ try buildRage(battle, player.foe(), options);
1012
1069
  }
1070
+ return null;
1013
1071
  }
1014
1072
  }
1015
1073
 
1016
1074
  // 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.
1075
+ // if an effect was intended to "always happen" it will still get skipped
1018
1076
  if (nullified) return null;
1019
1077
 
1020
1078
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1079
  // 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.
1080
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1081
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1082
 
1025
1083
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1092,7 @@ fn doMove(
1034
1092
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1093
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1094
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1095
+ // puts the |-status| message before |-hitcount| instead of after
1038
1096
  if (move.effect == .Twineedle) {
1039
1097
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1098
  } else if (move.effect == .Disable) {
@@ -1058,7 +1116,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1116
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1117
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1118
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1119
+ const spurious =
1120
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1121
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1122
  }
1063
1123
 
1064
1124
  fn calcDamage(
@@ -1067,6 +1127,7 @@ fn calcDamage(
1067
1127
  target_player: Player,
1068
1128
  m: ?Move.Data,
1069
1129
  crit: bool,
1130
+ options: anytype,
1070
1131
  ) bool {
1071
1132
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1133
  const cfz = m == null;
@@ -1078,28 +1139,8 @@ fn calcDamage(
1078
1139
 
1079
1140
  const special = move.type.special();
1080
1141
 
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
1142
+ var atk = attack(side, crit, special);
1143
+ var def = defense(target, crit, special, cfz);
1103
1144
 
1104
1145
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1146
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1107,7 +1148,7 @@ fn calcDamage(
1107
1148
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1108
1149
  atk = @max((atk / 4) & 255, 1);
1109
1150
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1110
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1151
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1111
1152
  }
1112
1153
 
1113
1154
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1124,11 +1165,41 @@ fn calcDamage(
1124
1165
  d = @min(997, d);
1125
1166
  d += 2;
1126
1167
 
1168
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1169
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1170
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1171
+ // that results in counter intuitive probabilities in the common case
1172
+ if (pkmn.options.chance and !pkmn.options.mod) {
1173
+ atk = attack(side, !crit, special);
1174
+ def = defense(target, !crit, special, false);
1175
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1176
+ }
1177
+
1127
1178
  battle.last_damage = @intCast(d);
1128
1179
 
1129
1180
  return true;
1130
1181
  }
1131
1182
 
1183
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1186
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1187
+ }
1188
+
1189
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1190
+ return if (crit)
1191
+ if (special) target.stored().stats.spc else target.stored().stats.def
1192
+ else
1193
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1194
+ if (special)
1195
+ target.active.stats.spc *
1196
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1197
+ else
1198
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1199
+ target.active.stats.def *
1200
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1201
+ }
1202
+
1132
1203
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1204
  const side = battle.side(player);
1134
1205
  const foe = battle.foe(player);
@@ -1173,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1244
  const foe = battle.foe(player);
1174
1245
 
1175
1246
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1247
 
1178
1248
  battle.last_damage = switch (side.last_selected_move) {
1179
1249
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1184,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1184
1254
  .Psywave => power: {
1185
1255
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1186
1256
  // GLITCH: Psywave infinite glitch loop
1187
- if (!showdown and max <= 1) return Result.Error;
1257
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1188
1258
  break :power try Rolls.psywave(battle, player, max, options);
1189
1259
  },
1190
1260
  else => unreachable,
1191
1261
  };
1192
1262
 
1193
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1263
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1194
1264
 
1195
1265
  const sub = showdown and foe.active.volatiles.Substitute;
1196
1266
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1227,16 +1297,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1297
 
1228
1298
  if (!used or !selected) {
1229
1299
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1230
- if (!showdown) return Result.Error;
1300
+ if (!pkmn.options.mod) return Result.Error;
1231
1301
  try options.log.fail(.{ battle.active(player), .None });
1232
1302
  return null;
1233
1303
  }
1234
1304
 
1235
1305
  battle.last_damage *|= 2;
1236
1306
 
1237
- // Pokémon Showdown calls checkHit before Counter
1307
+ // Pokémon Showdown calls moveHit before Counter
1238
1308
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1309
 
1241
1310
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1311
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1334,7 @@ fn applyDamage(
1265
1334
  subbed.active.volatiles.Substitute = false;
1266
1335
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1336
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1337
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1338
  return true;
1270
1339
  } else {
1271
1340
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1368,7 @@ fn mirrorMove(
1299
1368
  try options.log.fail(.{ battle.active(player), .None });
1300
1369
  return null;
1301
1370
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1371
+ incrementPP(side, player, mslot, options);
1303
1372
  }
1304
1373
 
1305
1374
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1389,7 @@ fn metronome(
1320
1389
  var side = battle.side(player);
1321
1390
 
1322
1391
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1392
+ incrementPP(side, player, mslot, options);
1324
1393
 
1325
1394
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1395
  return null;
@@ -1332,7 +1401,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1401
  var immune = false;
1333
1402
  var mist = false;
1334
1403
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1404
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1405
+ if (!showdown) try options.chance.commit(player, .hit);
1406
+ return true;
1407
+ }
1336
1408
 
1337
1409
  assert(!immune);
1338
1410
  if (mist) {
@@ -1341,11 +1413,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1413
  try options.log.activate(.{ foe_ident, .Mist });
1342
1414
  try options.log.fail(.{ foe_ident, .None });
1343
1415
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1416
  try options.log.lastmiss(.{});
1346
1417
  try options.log.miss(.{battle.active(player)});
1347
1418
  }
1348
1419
 
1420
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1421
  return false;
1350
1422
  }
1351
1423
 
@@ -1356,7 +1428,7 @@ fn moveHit(
1356
1428
  immune: *bool,
1357
1429
  mist: *bool,
1358
1430
  options: anytype,
1359
- ) bool {
1431
+ ) !bool {
1360
1432
  var side = battle.side(player);
1361
1433
  const foe = battle.foe(player);
1362
1434
 
@@ -1407,7 +1479,7 @@ fn moveHit(
1407
1479
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1480
 
1409
1481
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1482
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1483
  };
1412
1484
 
1413
1485
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1490,17 @@ fn moveHit(
1418
1490
 
1419
1491
  if (!miss) return true;
1420
1492
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1493
+
1421
1494
  side.active.volatiles.Binding = false;
1495
+ options.chance.observe(.binding, player, .None);
1496
+
1422
1497
  return false;
1423
1498
  }
1424
1499
 
1425
1500
  fn checkFaint(
1426
1501
  battle: anytype,
1427
1502
  player: Player,
1503
+ cap: bool,
1428
1504
  options: anytype,
1429
1505
  ) @TypeOf(options.log).Error!?Result {
1430
1506
  const side = battle.side(player);
@@ -1437,8 +1513,8 @@ fn checkFaint(
1437
1513
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1514
  const more = player_out or foe_out;
1439
1515
 
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;
1516
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1517
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1518
 
1443
1519
  assert(!side.active.volatiles.MultiHit);
1444
1520
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1535,7 @@ fn checkFaint(
1459
1535
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1536
  }
1461
1537
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1538
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1539
  var side = battle.side(player);
1464
1540
  var foe = battle.foe(player);
1465
1541
  assert(side.stored().hp == 0);
@@ -1468,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1468
1544
  assert(!foe_volatiles.MultiHit);
1469
1545
  if (foe_volatiles.Bide) {
1470
1546
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1471
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1547
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1472
1548
  if (foe_volatiles.state != 0) return Result.Error;
1473
1549
  }
1474
1550
 
@@ -1493,7 +1569,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1569
  }
1494
1570
 
1495
1571
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1572
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1573
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1574
+ if (cap) options.calc.capped(player);
1497
1575
  return null;
1498
1576
  }
1499
1577
 
@@ -1557,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1557
1635
 
1558
1636
  battle.turn += 1;
1559
1637
 
1560
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1638
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1561
1639
  try options.log.tie(.{});
1562
1640
  return Result.Tie;
1563
1641
  }
1564
1642
 
1565
- if (showdown and battle.turn >= 1000) {
1643
+ if (pkmn.options.mod and battle.turn >= 1000) {
1566
1644
  try options.log.tie(.{});
1567
1645
  return Result.Tie;
1568
1646
  } else if (battle.turn >= 65535) {
@@ -1653,17 +1731,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1731
  }
1654
1732
  }
1655
1733
 
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
1734
  fn onBegin(
1668
1735
  battle: anytype,
1669
1736
  player: Player,
@@ -1750,8 +1817,10 @@ pub const Effects = struct {
1750
1817
 
1751
1818
  side.active.volatiles.Bide = true;
1752
1819
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1820
+
1753
1821
  side.active.volatiles.state = 0;
1754
1822
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1823
+ options.chance.observe(.attacking, player, .started);
1755
1824
 
1756
1825
  try options.log.start(.{ battle.active(player), .Bide });
1757
1826
  }
@@ -1813,12 +1882,14 @@ pub const Effects = struct {
1813
1882
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1883
  if (!try checkHit(battle, player, move, options)) return;
1815
1884
  }
1816
- try options.chance.commit(player, .hit);
1817
1885
  }
1818
1886
 
1819
1887
  if (foe.active.volatiles.Confusion) return;
1820
1888
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1889
+
1890
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1891
+ options.chance.observe(.confusion, player.foe(), .started);
1892
+ options.calc.confusion(player.foe());
1822
1893
 
1823
1894
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1895
  }
@@ -1846,10 +1917,7 @@ pub const Effects = struct {
1846
1917
  const foe_ident = battle.active(player.foe());
1847
1918
 
1848
1919
  // 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
- }
1920
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1921
 
1854
1922
  if (volatiles.disable_move != 0) {
1855
1923
  try options.log.fail(.{ foe_ident, .None });
@@ -1867,18 +1935,18 @@ pub const Effects = struct {
1867
1935
 
1868
1936
  // Technically this is still considered simply a "miss" on the cartridge,
1869
1937
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1870
- if (n == 0) {
1938
+ if (n == 0 or (pkmn.options.mod and err)) {
1871
1939
  try options.log.fail(.{ foe_ident, .None });
1872
1940
  return null;
1873
1941
  } else if (err) {
1874
1942
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1875
- assert(!showdown);
1876
1943
  return Result.Error;
1877
1944
  }
1878
1945
 
1879
1946
  volatiles.disable_move =
1880
1947
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1948
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1949
+ options.chance.observe(.disable, player.foe(), .started);
1882
1950
 
1883
1951
  const id = foe.active.move(volatiles.disable_move).id;
1884
1952
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1949,7 +2017,7 @@ pub const Effects = struct {
1949
2017
  if (foe.active.types.includes(move.type)) return;
1950
2018
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1951
2019
  // Freeze Clause Mod
1952
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2020
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1953
2021
 
1954
2022
  foe_stored.status = Status.init(.FRZ);
1955
2023
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1985,22 +2053,24 @@ pub const Effects = struct {
1985
2053
  if (p != player and Status.any(s.stored().status)) {
1986
2054
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2055
  s.stored().status = 0;
2056
+ options.chance.observe(.sleep, p, .None);
1988
2057
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2058
  s.stored().status = Status.init(.PSN);
1990
2059
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2060
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2061
+ try clearVolatiles(battle, p, options);
1993
2062
  }
1994
2063
  } else {
1995
2064
  if (Status.any(foe_stored.status)) {
1996
2065
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2066
  foe.last_selected_move = .SKIP_TURN;
2067
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2068
  }
1999
2069
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2070
  foe_stored.status = 0;
2001
2071
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2072
+ try clearVolatiles(battle, player, options);
2073
+ try clearVolatiles(battle, player.foe(), options);
2004
2074
  }
2005
2075
  }
2006
2076
 
@@ -2051,7 +2121,6 @@ pub const Effects = struct {
2051
2121
  }
2052
2122
  }
2053
2123
 
2054
- try options.chance.commit(player, .hit);
2055
2124
  foe.active.volatiles.LeechSeed = true;
2056
2125
 
2057
2126
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2161,10 @@ pub const Effects = struct {
2092
2161
  if (!has_mimic) {
2093
2162
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2163
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2164
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2165
  }
2098
2166
  }
2099
2167
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2168
 
2102
2169
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2170
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2218,6 @@ pub const Effects = struct {
2151
2218
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2219
  if (!try checkHit(battle, player, move, options)) return;
2153
2220
  }
2154
- try options.chance.commit(player, .hit);
2155
2221
 
2156
2222
  foe_stored.status = Status.init(.PAR);
2157
2223
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2285,6 @@ pub const Effects = struct {
2219
2285
 
2220
2286
  if (move.effect == .Poison) {
2221
2287
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2288
  } else {
2224
2289
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2290
  return;
@@ -2288,13 +2353,12 @@ pub const Effects = struct {
2288
2353
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2354
  });
2290
2355
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2356
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2357
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2358
  }
2295
2359
 
2296
2360
  // Sleep Clause Mod
2297
- if (showdown) {
2361
+ if (pkmn.options.mod) {
2298
2362
  for (foe.pokemon) |p| {
2299
2363
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2300
2364
  }
@@ -2302,6 +2366,8 @@ pub const Effects = struct {
2302
2366
  foe.active.volatiles.Recharging = false;
2303
2367
 
2304
2368
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2369
+ options.chance.observe(.sleep, player.foe(), .started);
2370
+
2305
2371
  const last = battle.side(player).last_selected_move;
2306
2372
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2373
  }
@@ -2343,9 +2409,7 @@ pub const Effects = struct {
2343
2409
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2410
  if (showdown) {
2345
2411
  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
- }
2412
+ _ = try checkHit(battle, player, move, options);
2349
2413
  battle.last_damage = 0;
2350
2414
  } else {
2351
2415
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2424,8 @@ pub const Effects = struct {
2360
2424
 
2361
2425
  volatiles.Thrashing = true;
2362
2426
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2427
+
2428
+ options.chance.observe(.attacking, player, .started);
2363
2429
  }
2364
2430
 
2365
2431
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2398,13 +2464,14 @@ pub const Effects = struct {
2398
2464
  if (side.active.volatiles.Binding) return;
2399
2465
  side.active.volatiles.Binding = true;
2400
2466
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2401
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2467
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2402
2468
  if (!showdown) {
2403
2469
  foe.active.volatiles.Recharging = false;
2404
2470
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2405
2471
 
2406
2472
  side.active.volatiles.attacks =
2407
2473
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2474
+ options.chance.observe(.binding, player, .started);
2408
2475
  }
2409
2476
 
2410
2477
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2588,6 @@ pub const Effects = struct {
2521
2588
  } else {
2522
2589
  // checkHit already checks for Invulnerable
2523
2590
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2591
  }
2526
2592
 
2527
2593
  var stats = &foe.active.stats;
@@ -2635,22 +2701,22 @@ fn isForced(active: anytype) bool {
2635
2701
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2702
  }
2637
2703
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2704
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2705
  var log = options.log;
2640
2706
  var side = battle.side(who);
2641
2707
  var volatiles = &side.active.volatiles;
2642
2708
  const ident = battle.active(who);
2643
2709
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2710
  if (volatiles.disable_move != 0) {
2647
2711
  volatiles.disable_move = 0;
2648
2712
  volatiles.disable_duration = 0;
2713
+ options.chance.observe(.disable, who, .None);
2649
2714
  try log.end(.{ ident, .DisableSilent });
2650
2715
  }
2651
2716
  if (volatiles.Confusion) {
2652
2717
  // volatiles.confusion is left unchanged
2653
2718
  volatiles.Confusion = false;
2719
+ options.chance.observe(.confusion, who, .None);
2654
2720
  try log.end(.{ ident, .ConfusionSilent });
2655
2721
  }
2656
2722
  if (volatiles.Mist) {
@@ -2685,6 +2751,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2751
  }
2686
2752
  }
2687
2753
 
2754
+ fn decrement(
2755
+ comptime field: chance.Action.Field,
2756
+ player: Player,
2757
+ options: anytype,
2758
+ n: anytype,
2759
+ ) @TypeOf(n) {
2760
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2761
+ .started, .ended => 0,
2762
+ .continuing => if (n > 1) n - 1 else n,
2763
+ else => unreachable,
2764
+ } else n - 1;
2765
+ }
2766
+
2688
2767
  pub const Rolls = struct {
2689
2768
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2769
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2700,7 +2779,13 @@ pub const Rolls = struct {
2700
2779
  return p1;
2701
2780
  }
2702
2781
 
2703
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2782
+ fn criticalHit(
2783
+ battle: anytype,
2784
+ player: Player,
2785
+ rate: u8,
2786
+ spurious: bool,
2787
+ options: anytype,
2788
+ ) !bool {
2704
2789
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
2790
  val == .true
2706
2791
  else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
@@ -2710,7 +2795,7 @@ pub const Rolls = struct {
2710
2795
  else
2711
2796
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
2797
 
2713
- try options.chance.criticalHit(player, crit, rate);
2798
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2714
2799
  return crit;
2715
2800
  }
2716
2801
 
@@ -2732,17 +2817,19 @@ pub const Rolls = struct {
2732
2817
  return roll;
2733
2818
  }
2734
2819
 
2735
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2820
+ const uN = if (pkmn.options.miss) u8 else u9;
2821
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2822
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2736
2823
  const ok = if (options.calc.overridden(player, .hit)) |val|
2737
2824
  val == .true
2738
2825
  else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
- battle.rng.hit(player, accuracy)
2826
+ battle.rng.hit(player, acc)
2740
2827
  else if (showdown)
2741
- battle.rng.chance(u8, accuracy, 256)
2828
+ battle.rng.chance(uN, acc, 256)
2742
2829
  else
2743
- battle.rng.next() < accuracy;
2830
+ battle.rng.next() < acc;
2744
2831
 
2745
- options.chance.hit(ok, accuracy);
2832
+ try options.chance.hit(player, ok, acc);
2746
2833
  return ok;
2747
2834
  }
2748
2835
 
@@ -2874,9 +2961,10 @@ pub const Rolls = struct {
2874
2961
  }
2875
2962
 
2876
2963
  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"))
2964
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2965
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2966
+ else
2967
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
2968
  battle.rng.sleepDuration(player)
2881
2969
  else if (showdown)
2882
2970
  battle.rng.range(u3, 1, 8)
@@ -2888,14 +2976,15 @@ pub const Rolls = struct {
2888
2976
  };
2889
2977
 
2890
2978
  assert(duration >= 1 and duration <= 7);
2891
- options.chance.duration(.sleep, player, player.foe(), duration);
2979
+ options.chance.duration(player, duration);
2892
2980
  return duration;
2893
2981
  }
2894
2982
 
2895
2983
  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"))
2984
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2985
+ if (val >= 1 and val <= 8) val else null
2986
+ else
2987
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
2988
  battle.rng.disableDuration(player)
2900
2989
  else if (showdown)
2901
2990
  battle.rng.range(u4, 1, 9)
@@ -2903,29 +2992,31 @@ pub const Rolls = struct {
2903
2992
  @intCast((battle.rng.next() & 7) + 1);
2904
2993
 
2905
2994
  assert(duration >= 1 and duration <= 8);
2906
- options.chance.duration(.disable, player, player.foe(), duration);
2995
+ options.chance.duration(player, duration);
2907
2996
  return duration;
2908
2997
  }
2909
2998
 
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)
2999
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3000
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3001
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3002
+ else
3003
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3004
+ battle.rng.confusionDuration(player)
2915
3005
  else if (showdown)
2916
3006
  battle.rng.range(u3, 2, 6)
2917
3007
  else
2918
3008
  @intCast((battle.rng.next() & 3) + 2);
2919
3009
 
2920
3010
  assert(duration >= 2 and duration <= 5);
2921
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3011
+ options.chance.duration(player, duration);
2922
3012
  return duration;
2923
3013
  }
2924
3014
 
2925
3015
  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"))
3016
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3017
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3018
+ else
3019
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
3020
  battle.rng.attackingDuration(player)
2930
3021
  else if (showdown)
2931
3022
  battle.rng.range(u3, 2, 4)
@@ -2933,7 +3024,7 @@ pub const Rolls = struct {
2933
3024
  @intCast((battle.rng.next() & 1) + 2);
2934
3025
 
2935
3026
  assert(duration >= 2 and duration <= 3);
2936
- options.chance.duration(.attacking, player, player, duration);
3027
+ options.chance.duration(player, duration);
2937
3028
  return duration;
2938
3029
  }
2939
3030
 
@@ -2946,9 +3037,10 @@ pub const Rolls = struct {
2946
3037
  options: anytype,
2947
3038
  ) !u3 {
2948
3039
  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"))
3040
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3041
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3042
+ else
3043
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
3044
  battle.rng.distribution(player)
2953
3045
  else if (showdown)
2954
3046
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2959,7 +3051,7 @@ pub const Rolls = struct {
2959
3051
 
2960
3052
  assert(n >= 2 and n <= 5);
2961
3053
  if (effect == .Binding) {
2962
- options.chance.duration(.binding, player, player, n);
3054
+ options.chance.duration(player, n);
2963
3055
  } else {
2964
3056
  try options.chance.multiHit(player, n);
2965
3057
  }
@@ -2974,15 +3066,13 @@ pub const Rolls = struct {
2974
3066
  options: anytype,
2975
3067
  ) !u4 {
2976
3068
  // TODO: consider throwing error instead of rerolling?
2977
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3069
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2978
3070
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2979
3071
  val
2980
3072
  else
2981
3073
  null
2982
3074
  else
2983
- null;
2984
-
2985
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3075
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
3076
  battle.rng.moveSlot(player, moves, check_pp)
2987
3077
  else slot: {
2988
3078
  if (showdown) {
@@ -3068,7 +3158,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3158
  const stored = side.stored();
3069
3159
 
3070
3160
  // 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.
3161
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3072
3162
  //
3073
3163
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3074
3164
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3076,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3076
3166
  // passing due to being forced into using a move. Instead of introducing another option
3077
3167
  // we simply repurpose Move with no move slot, even though pedantically this is not
3078
3168
  // strictly correct as the player would not have been presented the option to move or
3079
- // switch at all.
3169
+ // switch at all
3080
3170
  if (isForced(active)) {
3081
3171
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3082
3172
  n += 1;
@@ -3095,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3095
3185
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3096
3186
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3097
3187
  // either disables all other moves in the case of limited or requires you to select a
3098
- // move normally if sleeping/frozen/bound.
3188
+ // move normally if sleeping/frozen/bound
3099
3189
  if (!showdown and (limited or foe.active.volatiles.Binding or
3100
3190
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3101
3191
  {
@@ -3108,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3198
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3109
3199
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3110
3200
  // 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.
3201
+ // proc-ed move in their moveset) and disabling all other moves
3112
3202
  if (limited) {
3113
3203
  assert(showdown);
3114
3204
  assert(side.last_selected_move != .None);
@@ -3140,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3140
3230
  // Struggle (Pokémon Showdown would use 'move 1' here)
3141
3231
  if (n == before) {
3142
3232
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3143
- if (!showdown) {
3233
+ if (!pkmn.options.mod) {
3144
3234
  while (slot <= 4) : (slot += 1) {
3145
3235
  if (active.moves[slot - 1].pp != 0) return n;
3146
3236
  }