@pkmn/engine 0.1.0-dev.d4119c62 → 0.1.0-dev.d4bd80b1

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 (83) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -18
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -90
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +53 -78
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +330 -104
  39. package/package.json +60 -134
  40. package/src/bin/install-pkmn-engine +32 -21
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +255 -0
  43. package/src/lib/{binding → bindings}/node.zig +8 -21
  44. package/src/lib/bindings/wasm.zig +59 -0
  45. package/src/lib/common/array.zig +77 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +39 -8
  48. package/src/lib/common/js.zig +4 -2
  49. package/src/lib/common/optional.zig +14 -10
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +63 -67
  52. package/src/lib/common/rational.zig +128 -32
  53. package/src/lib/common/rng.zig +7 -9
  54. package/src/lib/common/util.zig +50 -1
  55. package/src/lib/gen1/calc.zig +686 -159
  56. package/src/lib/gen1/chance.zig +405 -280
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +2 -4
  59. package/src/lib/gen1/data/types.zig +1 -2
  60. package/src/lib/gen1/data.zig +24 -27
  61. package/src/lib/gen1/helpers.zig +92 -548
  62. package/src/lib/gen1/mechanics.zig +379 -258
  63. package/src/lib/gen2/calc.zig +37 -27
  64. package/src/lib/gen2/chance.zig +84 -496
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +1 -2
  69. package/src/lib/gen2/data.zig +19 -22
  70. package/src/lib/gen2/generated.zig +19 -26
  71. package/src/lib/gen2/helpers.zig +46 -25
  72. package/src/lib/gen2/mechanics.zig +217 -163
  73. package/src/lib/node.zig +18 -0
  74. package/src/lib/pkmn.zig +37 -15
  75. package/src/lib/wasm.zig +17 -0
  76. package/src/tools/display/dom.ts +95 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -400
  79. package/build/test/display.js.map +0 -1
  80. package/src/lib/binding/wasm.zig +0 -12
  81. package/src/lib/gen1/README.md +0 -894
  82. package/src/lib/gen2/README.md +0 -329
  83. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,37 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
-
10
- const data = @import("data.zig");
10
+ const std = @import("std");
11
11
 
12
12
  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
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
24
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
25
18
  const Fail = protocol.Fail;
26
- const Heal = protocol.Heal;
27
-
28
19
  const Gen12 = rng.Gen12;
29
-
30
- const ActivePokemon = data.ActivePokemon;
31
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
32
22
  const Move = data.Move;
33
23
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
35
27
  const Side = data.Side;
36
28
  const Species = data.Species;
37
29
  const Stats = data.Stats;
@@ -71,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
71
63
  var p1 = battle.side(.P1);
72
64
  var p2 = battle.side(.P2);
73
65
 
74
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
75
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
66
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
67
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
68
 
77
69
  if (try turnOrder(battle, c1, c2, options) == .P1) {
78
70
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -90,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
90
82
  const p1 = battle.side(.P1);
91
83
  const p2 = battle.side(.P2);
92
84
 
93
- var p1_slot = findFirstAlive(p1);
85
+ const p1_slot = findFirstAlive(p1);
94
86
  assert(!showdown or p1_slot == 1);
95
87
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
96
88
 
97
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
98
90
  assert(!showdown or p2_slot == 1);
99
91
  if (p2_slot == 0) return Result.Win;
100
92
 
@@ -166,7 +158,7 @@ fn selectMove(
166
158
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
159
  // set the selected move will not actually be used, it will just be reported as having
168
160
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
161
+ // replicate the incorrect behavior with the correct mechanisms)
170
162
  saveMove(battle, player, choice);
171
163
  } else {
172
164
  assert(choice.data == 0);
@@ -174,7 +166,10 @@ fn selectMove(
174
166
  if (foe_choice.type == .Switch) {
175
167
  const last = side.active.move(battle.lastMove(player).index);
176
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
177
- if (last.id == .MirrorMove) return Result.Error;
169
+ if (last.id == .MirrorMove) {
170
+ if (!pkmn.options.mod) return Result.Error;
171
+ side.last_selected_move = last.id;
172
+ }
178
173
  }
179
174
  }
180
175
  return null;
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
248
  active.moves = incoming.moves;
254
249
 
255
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
256
252
 
257
253
  foe.active.volatiles.Binding = false;
258
254
 
259
255
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
256
 
262
257
  if (showdown and incoming.status == Status.TOX) {
263
258
  incoming.status = Status.init(.PSN);
@@ -347,13 +342,13 @@ fn doTurn(
347
342
  )) |r| return r;
348
343
  if (!replace) {
349
344
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
345
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
346
  }
352
347
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
348
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
349
  } else if (foe_choice.type == .Pass) return null;
355
350
 
356
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
357
352
 
358
353
  residual = true;
359
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +364,10 @@ fn doTurn(
369
364
  )) |r| return r;
370
365
  if (!replace) {
371
366
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
367
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
368
  }
374
369
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
371
  }
377
372
 
378
373
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -425,13 +420,13 @@ fn executeMove(
425
420
  mslot = @intCast(battle.lastMove(player).index);
426
421
  const stored = side.stored();
427
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
428
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
429
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
430
425
  side.last_selected_move == .HyperBeam or
431
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
427
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
428
  // 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.
429
+ // index 1 and the current Pokémon has the same move in its first slot
435
430
  if (!auto) {
436
431
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -458,6 +453,25 @@ fn executeMove(
458
453
  .err => return @as(?Result, Result.Error),
459
454
  }
460
455
 
456
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
457
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
458
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
459
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
460
+ side.last_selected_move != side.active.move(mslot).id)
461
+ {
462
+ assert(mslot == 1);
463
+ const has_beam = has_beam: {
464
+ for (side.active.moves, 0..) |m, i| {
465
+ if (m.id == .HyperBeam) {
466
+ mslot = @intCast(i + 1);
467
+ break :has_beam true;
468
+ }
469
+ }
470
+ break :has_beam false;
471
+ };
472
+ if (!has_beam) skip_pp = true;
473
+ }
474
+
461
475
  const can = skip_can or
462
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
477
  if (!can) return null;
@@ -484,17 +498,22 @@ fn beforeMove(
484
498
 
485
499
  if (Status.is(stored.status, .SLP)) {
486
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
487
502
  // 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;
503
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
504
+ .started, .ended => stored.status = 0,
505
+ .continuing => if (Status.duration(stored.status) > 1) {
506
+ stored.status -= 1;
507
+ },
508
+ else => unreachable,
490
509
  } else {
491
510
  stored.status -= 1;
492
511
  }
493
512
 
494
513
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
498
517
 
499
518
  if (duration == 0) {
500
519
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +551,12 @@ fn beforeMove(
532
551
  }
533
552
 
534
553
  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);
554
+ volatiles.disable_duration =
555
+ decrement(.disable, player, options, volatiles.disable_duration);
556
+ try options.chance.disable(
557
+ player,
558
+ if (volatiles.disable_duration == 0) .ended else .continuing,
559
+ );
541
560
 
542
561
  if (volatiles.disable_duration == 0) {
543
562
  volatiles.disable_move = 0;
@@ -550,19 +569,24 @@ fn beforeMove(
550
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
551
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
552
571
  if (side.last_selected_move == .None) {
553
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
554
573
  return .err;
555
574
  }
556
575
 
557
576
  if (volatiles.Confusion) {
558
577
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
578
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
579
+ .started, .ended => volatiles.confusion = 0,
580
+ .continuing => if (volatiles.confusion > 1) {
581
+ volatiles.confusion -= 1;
582
+ },
583
+ .overwritten => {},
584
+ else => unreachable,
562
585
  } else {
563
586
  volatiles.confusion -= 1;
564
587
  }
565
- try options.chance.confusion(player, volatiles.confusion);
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
590
 
567
591
  if (volatiles.confusion == 0) {
568
592
  volatiles.Confusion = false;
@@ -580,7 +604,9 @@ fn beforeMove(
580
604
  volatiles.Charging = false;
581
605
  volatiles.Binding = false;
582
606
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
607
+
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
584
610
  {
585
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +614,9 @@ fn beforeMove(
588
614
  const def = foe.active.stats.def;
589
615
  foe.active.stats.def = active.stats.def;
590
616
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
592
620
  }
593
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +645,10 @@ fn beforeMove(
617
645
  volatiles.Thrashing = false;
618
646
  volatiles.Charging = false;
619
647
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
648
+
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
651
+
621
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
653
  try log.cant(.{ ident, .Paralysis });
623
654
  return .done;
@@ -635,14 +666,8 @@ fn beforeMove(
635
666
  volatiles.state +%= battle.last_damage;
636
667
  }
637
668
 
638
- assert(volatiles.attacks > 0);
639
-
640
- if (options.calc.modify(player, .attacking)) |extend| {
641
- if (!extend) volatiles.attacks = 0;
642
- } else {
643
- volatiles.attacks -= 1;
644
- }
645
- try options.chance.attacking(player, volatiles.attacks);
669
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
670
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
646
671
 
647
672
  if (volatiles.attacks != 0) {
648
673
  try log.activate(.{ ident, .Bide });
@@ -676,39 +701,40 @@ fn beforeMove(
676
701
  }
677
702
 
678
703
  if (volatiles.Thrashing) {
679
- assert(volatiles.attacks > 0);
704
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
680
705
 
681
- if (options.calc.modify(player, .attacking)) |extend| {
682
- if (!extend) volatiles.attacks = 0;
683
- } else {
684
- volatiles.attacks -= 1;
685
- }
686
- try options.chance.attacking(player, volatiles.attacks);
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
708
+
709
+ if (volatiles.attacks == 0) {
710
+ const overwritten = volatiles.Confusion;
687
711
 
688
- if (!showdown and handleThrashing(battle, active, player, options)) {
712
+ volatiles.Thrashing = false;
713
+ volatiles.Confusion = true;
714
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
715
+
716
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
717
+ // ended (the engine's protocol is always from the omniscient perspective and clients
718
+ // can be expected to recognize this pattern and pass the information only to the
719
+ // correct player)
689
720
  try log.start(.{ battle.active(player), .ConfusionSilent });
721
+ options.chance.observe(.confusion, player, if (overwritten)
722
+ if (pkmn.options.overwrite) .overwritten else .continuing
723
+ else
724
+ .started);
725
+ if (!overwritten) options.calc.confusion(player);
690
726
  }
691
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
692
- if (showdown) {
693
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
694
- // used in useMove and doesn't have the notion of skipping canMove semantics
695
- side.last_used_move = side.last_selected_move;
696
- if (handleThrashing(battle, active, player, options)) {
697
- try log.start(.{ battle.active(player), .ConfusionSilent });
698
- }
699
- }
727
+
728
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
729
+ // used in useMove and doesn't have the notion of skipping canMove semantics
730
+ if (showdown) side.last_used_move = side.last_selected_move;
731
+
700
732
  return .skip_can;
701
733
  }
702
734
 
703
735
  if (volatiles.Binding) {
704
- assert(volatiles.attacks > 0);
705
-
706
- if (options.calc.modify(player, .binding)) |extend| {
707
- if (!extend) volatiles.attacks = 0;
708
- } else {
709
- volatiles.attacks -= 1;
710
- }
711
- try options.chance.binding(player, volatiles.attacks);
736
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
737
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
712
738
 
713
739
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
740
  if (showdown or battle.last_damage != 0) {
@@ -738,7 +764,6 @@ fn canMove(
738
764
  const player_ident = battle.active(player);
739
765
  const move = Move.get(side.last_selected_move);
740
766
 
741
- var skip = skip_pp;
742
767
  if (side.active.volatiles.Charging) {
743
768
  side.active.volatiles.Charging = false;
744
769
  side.active.volatiles.Invulnerable = false;
@@ -750,7 +775,7 @@ fn canMove(
750
775
  }
751
776
 
752
777
  side.last_used_move = side.last_selected_move;
753
- if (!skip) decrementPP(side, mslot, auto);
778
+ if (!skip_pp) decrementPP(side, mslot, auto);
754
779
 
755
780
  const target = if (move.target == .Self) player else player.foe();
756
781
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -763,9 +788,14 @@ fn canMove(
763
788
 
764
789
  if (move.effect == .Thrashing) {
765
790
  Effects.thrashing(battle, player, options);
766
- } else if (!showdown and move.effect == .Binding) {
767
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- try Effects.binding(battle, player, false, options);
791
+ } else if (move.effect == .Binding) {
792
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
793
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
794
+ if (showdown) {
795
+ battle.foe(player).active.volatiles.Recharging = false;
796
+ } else {
797
+ try Effects.binding(battle, player, false, options);
798
+ }
769
799
  }
770
800
 
771
801
  return true;
@@ -802,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
802
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
803
833
  }
804
834
 
805
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
806
836
  var active = &side.active;
807
837
  const volatiles = &active.volatiles;
808
838
 
@@ -810,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
810
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
811
841
  if (showdown and volatiles.Transform) return;
812
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
813
846
  assert(mslot > 0 and mslot <= 4);
814
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
847
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
815
848
  }
816
849
 
817
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -859,13 +892,13 @@ fn doMove(
859
892
  break :miss true;
860
893
  }
861
894
  break :miss (move.target != .Self and
862
- !moveHit(battle, player, move, &immune, &mist, options));
895
+ !try moveHit(battle, player, move, &immune, &mist, options));
863
896
  };
864
897
  assert(!immune or miss or (showdown and move.effect == .Binding));
865
898
 
866
899
  var late = showdown and move.effect != .Explode;
867
900
  const skip = status or immune;
868
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
869
902
  if (showdown and move.effect.isMulti()) {
870
903
  try Effects.multiHit(battle, player, move, options);
871
904
  hits = side.active.volatiles.attacks;
@@ -887,8 +920,9 @@ fn doMove(
887
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
888
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
889
922
  battle.last_damage = if (ohko) 65535 else 0;
890
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
891
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
923
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
924
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
925
+ if (!showdown) try options.chance.commit(player, .err);
892
926
  return @as(?Result, Result.Error);
893
927
  }
894
928
  if (battle.last_damage == 0) {
@@ -902,11 +936,12 @@ fn doMove(
902
936
  }
903
937
  }
904
938
 
905
- var zero = battle.last_damage == 0;
906
- miss = if (showdown or skip)
907
- miss
908
- else
909
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
939
+ var missed: bool = undefined;
940
+ const zero = battle.last_damage == 0;
941
+ if (!showdown and !skip) {
942
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
943
+ miss = missed or zero;
944
+ }
910
945
 
911
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
912
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -933,11 +968,32 @@ fn doMove(
933
968
  try log.immune(.{ foe_ident, .OHKO });
934
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
935
970
  try log.immune(.{ foe_ident, .None });
971
+ if (!showdown) {
972
+ if (move.effect == .Binding) {
973
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
974
+ // then simply clears it later on if the move happens to miss in moveHit.
975
+ // However, moveHit can return true and we'll still end up in the outer miss
976
+ // handling block because attacking into an immune Pokemon does zero damage
977
+ // (considered missing)
978
+ assert(zero);
979
+ // Thus we still need to actually save the hit result (but not the crit roll).
980
+ // Using .miss for this commit would accomplish what we want, but is somewhat
981
+ // misleading as its entirely possible the move actually did hit (and the target
982
+ // is now trapped) so we use the .binding value instead which is effectively
983
+ // identical, just doesn't claim to know about the actual results of the hit
984
+ // roll)
985
+ try options.chance.commit(player, .binding);
986
+ } else {
987
+ // The .glitch option allows for conditionally committing if we are in a
988
+ // division-by-zero freeze alternative timeline scenario
989
+ try options.chance.commit(player, .glitch);
990
+ }
991
+ }
936
992
  } else if (mist) {
937
993
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
938
994
  try log.fail(.{ foe_ident, .None });
939
995
  } else {
940
- if (showdown or !zero) try options.chance.commit(player, .miss);
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
941
997
  try log.lastmiss(.{});
942
998
  try log.miss(.{battle.active(player)});
943
999
  }
@@ -952,17 +1008,15 @@ fn doMove(
952
1008
  try buildRage(battle, player.foe(), options);
953
1009
  } else if (showdown and move.effect == .Disable) {
954
1010
  try buildRage(battle, player.foe(), options);
955
- } else if (!showdown and immune and move.effect == .Binding) {
956
- try options.chance.commit(player, .binding);
957
1011
  }
958
1012
  return null;
959
- } else {
1013
+ } else if (!showdown) {
960
1014
  try options.chance.commit(player, .hit);
961
1015
  }
962
1016
 
963
1017
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
964
1018
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
965
- // it up above before damage calculation).
1019
+ // it up above before damage calculation)
966
1020
  if (!showdown and move.effect.isMulti()) {
967
1021
  try Effects.multiHit(battle, player, move, options);
968
1022
  hits = side.active.volatiles.attacks;
@@ -1001,32 +1055,30 @@ fn doMove(
1001
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1002
1056
  }
1003
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
1004
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
1005
1059
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1006
- if (move.effect == .Binding) {
1007
- try Effects.binding(battle, player, rewrap, options);
1008
- if (immune) {
1009
- battle.last_damage = 0;
1010
- assert(foe.stored().hp > 0);
1011
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1012
- if (sub) {
1013
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1014
- } else {
1015
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1016
- try buildRage(battle, player.foe(), options);
1017
- }
1018
- return null;
1060
+ try Effects.binding(battle, player, rewrap, options);
1061
+ if (immune) {
1062
+ battle.last_damage = 0;
1063
+ assert(foe.stored().hp > 0);
1064
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1065
+ if (sub) {
1066
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1067
+ } else {
1068
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1069
+ try buildRage(battle, player.foe(), options);
1019
1070
  }
1071
+ return null;
1020
1072
  }
1021
1073
  }
1022
1074
 
1023
1075
  // Substitute being broken nullifies the move's effect completely so even
1024
- // if an effect was intended to "always happen" it will still get skipped.
1076
+ // if an effect was intended to "always happen" it will still get skipped
1025
1077
  if (nullified) return null;
1026
1078
 
1027
1079
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1028
1080
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1029
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1081
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1030
1082
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1031
1083
 
1032
1084
  if (foe.stored().hp == 0) return null;
@@ -1041,7 +1093,7 @@ fn doMove(
1041
1093
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1042
1094
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1043
1095
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1044
- // puts the |-status| message before |-hitcount| instead of after.
1096
+ // puts the |-status| message before |-hitcount| instead of after
1045
1097
  if (move.effect == .Twineedle) {
1046
1098
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1047
1099
  } else if (move.effect == .Disable) {
@@ -1060,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1060
1112
  const side = battle.side(player);
1061
1113
 
1062
1114
  // Base speed is used for the critical hit calculation, even when Transform-ed
1063
- var rate = @as(u16, Species.chance(side.stored().species));
1115
+ var rate: u16 = Species.chance(side.stored().species);
1064
1116
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1065
1117
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1066
1118
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1067
1119
 
1068
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1120
+ const spurious =
1121
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1122
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1069
1123
  }
1070
1124
 
1071
1125
  fn calcDamage(
@@ -1074,6 +1128,7 @@ fn calcDamage(
1074
1128
  target_player: Player,
1075
1129
  m: ?Move.Data,
1076
1130
  crit: bool,
1131
+ options: anytype,
1077
1132
  ) bool {
1078
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1079
1134
  const cfz = m == null;
@@ -1085,28 +1140,8 @@ fn calcDamage(
1085
1140
 
1086
1141
  const special = move.type.special();
1087
1142
 
1088
- // zig fmt: off
1089
- var atk: u32 =
1090
- if (crit)
1091
- if (special) side.stored().stats.spc
1092
- else side.stored().stats.atk
1093
- else
1094
- if (special) side.active.stats.spc
1095
- else side.active.stats.atk;
1096
- var def: u32 =
1097
- if (crit)
1098
- if (special) target.stored().stats.spc
1099
- else target.stored().stats.def
1100
- else
1101
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1102
- if (special)
1103
- target.active.stats.spc *
1104
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1105
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1106
- else
1107
- target.active.stats.def *
1108
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1109
- // zig fmt: on
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1110
1145
 
1111
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1112
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1114,12 +1149,12 @@ fn calcDamage(
1114
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1115
1150
  atk = @max((atk / 4) & 255, 1);
1116
1151
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1117
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1118
1153
  }
1119
1154
 
1120
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1155
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1121
1156
 
1122
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1157
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1123
1158
 
1124
1159
  if (def == 0) return false;
1125
1160
 
@@ -1131,11 +1166,41 @@ fn calcDamage(
1131
1166
  d = @min(997, d);
1132
1167
  d += 2;
1133
1168
 
1169
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1170
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1171
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1172
+ // that results in counter intuitive probabilities in the common case
1173
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1174
+ atk = attack(side, !crit, special);
1175
+ def = defense(target, !crit, special, false);
1176
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1177
+ }
1178
+
1134
1179
  battle.last_damage = @intCast(d);
1135
1180
 
1136
1181
  return true;
1137
1182
  }
1138
1183
 
1184
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1185
+ return if (crit)
1186
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1187
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1188
+ }
1189
+
1190
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1191
+ return if (crit)
1192
+ if (special) target.stored().stats.spc else target.stored().stats.def
1193
+ else
1194
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1195
+ if (special)
1196
+ target.active.stats.spc *
1197
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1198
+ else
1199
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1200
+ target.active.stats.def *
1201
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1202
+ }
1203
+
1139
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1140
1205
  const side = battle.side(player);
1141
1206
  const foe = battle.foe(player);
@@ -1180,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1180
1245
  const foe = battle.foe(player);
1181
1246
 
1182
1247
  if (!try checkHit(battle, player, move, options)) return null;
1183
- try options.chance.commit(player, .hit);
1184
1248
 
1185
1249
  battle.last_damage = switch (side.last_selected_move) {
1186
1250
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1191,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1191
1255
  .Psywave => power: {
1192
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1193
1257
  // GLITCH: Psywave infinite glitch loop
1194
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1195
1259
  break :power try Rolls.psywave(battle, player, max, options);
1196
1260
  },
1197
1261
  else => unreachable,
1198
1262
  };
1199
1263
 
1200
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1264
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1201
1265
 
1202
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1203
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1234,16 +1298,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1298
 
1235
1299
  if (!used or !selected) {
1236
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1237
- if (!showdown) return Result.Error;
1301
+ if (!pkmn.options.mod) return Result.Error;
1238
1302
  try options.log.fail(.{ battle.active(player), .None });
1239
1303
  return null;
1240
1304
  }
1241
1305
 
1242
- battle.last_damage *|= 2;
1306
+ // NOTE: llvm/llvm-project#58557
1307
+ battle.last_damage = if (comptime builtin.target.isWasm())
1308
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1309
+ else
1310
+ battle.last_damage *| 2;
1243
1311
 
1244
- // Pokémon Showdown calls checkHit before Counter
1312
+ // Pokémon Showdown calls moveHit before Counter
1245
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1246
- try options.chance.commit(player, .hit);
1247
1314
 
1248
1315
  const sub = showdown and foe.active.volatiles.Substitute;
1249
1316
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1272,7 +1339,7 @@ fn applyDamage(
1272
1339
  subbed.active.volatiles.Substitute = false;
1273
1340
  // battle.last_damage is not updated with the amount of HP the Substitute had
1274
1341
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1275
- options.calc.capped(target_player);
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1276
1343
  return true;
1277
1344
  } else {
1278
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1306,7 +1373,7 @@ fn mirrorMove(
1306
1373
  try options.log.fail(.{ battle.active(player), .None });
1307
1374
  return null;
1308
1375
  } else if (!showdown or foe.last_used_move != .Struggle) {
1309
- incrementPP(side, mslot);
1376
+ incrementPP(side, player, mslot, options);
1310
1377
  }
1311
1378
 
1312
1379
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1327,7 +1394,7 @@ fn metronome(
1327
1394
  var side = battle.side(player);
1328
1395
 
1329
1396
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1330
- incrementPP(side, mslot);
1397
+ incrementPP(side, player, mslot, options);
1331
1398
 
1332
1399
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1333
1400
  return null;
@@ -1339,7 +1406,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1339
1406
  var immune = false;
1340
1407
  var mist = false;
1341
1408
 
1342
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1409
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1410
+ if (!showdown) try options.chance.commit(player, .hit);
1411
+ return true;
1412
+ }
1343
1413
 
1344
1414
  assert(!immune);
1345
1415
  if (mist) {
@@ -1348,11 +1418,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1348
1418
  try options.log.activate(.{ foe_ident, .Mist });
1349
1419
  try options.log.fail(.{ foe_ident, .None });
1350
1420
  } else {
1351
- try options.chance.commit(player, .miss);
1352
1421
  try options.log.lastmiss(.{});
1353
1422
  try options.log.miss(.{battle.active(player)});
1354
1423
  }
1355
1424
 
1425
+ if (!showdown) try options.chance.commit(player, .miss);
1356
1426
  return false;
1357
1427
  }
1358
1428
 
@@ -1363,7 +1433,7 @@ fn moveHit(
1363
1433
  immune: *bool,
1364
1434
  mist: *bool,
1365
1435
  options: anytype,
1366
- ) bool {
1436
+ ) !bool {
1367
1437
  var side = battle.side(player);
1368
1438
  const foe = battle.foe(player);
1369
1439
 
@@ -1402,7 +1472,7 @@ fn moveHit(
1402
1472
  const overwritten = overwrite and state > 0;
1403
1473
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1404
1474
 
1405
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1475
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1406
1476
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1407
1477
  accuracy = accuracy * boost[0] / boost[1];
1408
1478
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1414,7 +1484,7 @@ fn moveHit(
1414
1484
  if (overwrite) side.active.volatiles.state = accuracy;
1415
1485
 
1416
1486
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1417
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1487
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1418
1488
  };
1419
1489
 
1420
1490
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,13 +1495,17 @@ fn moveHit(
1425
1495
 
1426
1496
  if (!miss) return true;
1427
1497
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1498
+
1428
1499
  side.active.volatiles.Binding = false;
1500
+ options.chance.observe(.binding, player, .None);
1501
+
1429
1502
  return false;
1430
1503
  }
1431
1504
 
1432
1505
  fn checkFaint(
1433
1506
  battle: anytype,
1434
1507
  player: Player,
1508
+ cap: bool,
1435
1509
  options: anytype,
1436
1510
  ) @TypeOf(options.log).Error!?Result {
1437
1511
  const side = battle.side(player);
@@ -1444,8 +1518,8 @@ fn checkFaint(
1444
1518
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1445
1519
  const more = player_out or foe_out;
1446
1520
 
1447
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1448
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1521
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1522
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1449
1523
 
1450
1524
  assert(!side.active.volatiles.MultiHit);
1451
1525
  assert(!foe.active.volatiles.MultiHit);
@@ -1466,7 +1540,7 @@ fn checkFaint(
1466
1540
  return .{ .p1 = foe_choice, .p2 = .Switch };
1467
1541
  }
1468
1542
 
1469
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1543
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1470
1544
  var side = battle.side(player);
1471
1545
  var foe = battle.foe(player);
1472
1546
  assert(side.stored().hp == 0);
@@ -1475,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1475
1549
  assert(!foe_volatiles.MultiHit);
1476
1550
  if (foe_volatiles.Bide) {
1477
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1478
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1552
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1479
1553
  if (foe_volatiles.state != 0) return Result.Error;
1480
1554
  }
1481
1555
 
@@ -1500,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1500
1574
  }
1501
1575
 
1502
1576
  try options.log.faint(.{ battle.active(player), done });
1503
- options.calc.capped(player);
1577
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1578
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1579
+ if (cap) options.calc.capped(player);
1504
1580
  return null;
1505
1581
  }
1506
1582
 
@@ -1564,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1640
 
1565
1641
  battle.turn += 1;
1566
1642
 
1567
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1568
1644
  try options.log.tie(.{});
1569
1645
  return Result.Tie;
1570
1646
  }
1571
1647
 
1572
- if (showdown and battle.turn >= 1000) {
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1573
1649
  try options.log.tie(.{});
1574
1650
  return Result.Tie;
1575
1651
  } else if (battle.turn >= 65535) {
@@ -1660,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1660
1736
  }
1661
1737
  }
1662
1738
 
1663
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1664
- var volatiles = &active.volatiles;
1665
- assert(volatiles.Thrashing);
1666
- if (volatiles.attacks > 0) return false;
1667
-
1668
- volatiles.Thrashing = false;
1669
- volatiles.Confusion = true;
1670
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1671
- return true;
1672
- }
1673
-
1674
1739
  fn onBegin(
1675
1740
  battle: anytype,
1676
1741
  player: Player,
@@ -1679,7 +1744,6 @@ fn onBegin(
1679
1744
  residual: *bool,
1680
1745
  options: anytype,
1681
1746
  ) !void {
1682
- assert(move.effect.onBegin());
1683
1747
  return switch (move.effect) {
1684
1748
  .Confusion => Effects.confusion(battle, player, move, options),
1685
1749
  .Conversion => Effects.conversion(battle, player, options),
@@ -1702,7 +1766,6 @@ fn onBegin(
1702
1766
  }
1703
1767
 
1704
1768
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
- assert(move.effect.onEnd());
1706
1769
  return switch (move.effect) {
1707
1770
  .Bide => Effects.bide(battle, player, options),
1708
1771
  // zig fmt: off
@@ -1724,7 +1787,6 @@ fn alwaysHappens(
1724
1787
  residual: *bool,
1725
1788
  options: anytype,
1726
1789
  ) !void {
1727
- assert(move.effect.alwaysHappens());
1728
1790
  return switch (move.effect) {
1729
1791
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1730
1792
  .Explode => Effects.explode(battle, player),
@@ -1760,8 +1822,10 @@ pub const Effects = struct {
1760
1822
 
1761
1823
  side.active.volatiles.Bide = true;
1762
1824
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1825
+
1763
1826
  side.active.volatiles.state = 0;
1764
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1828
+ options.chance.observe(.attacking, player, .started);
1765
1829
 
1766
1830
  try options.log.start(.{ battle.active(player), .Bide });
1767
1831
  }
@@ -1823,12 +1887,14 @@ pub const Effects = struct {
1823
1887
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1888
  if (!try checkHit(battle, player, move, options)) return;
1825
1889
  }
1826
- try options.chance.commit(player, .hit);
1827
1890
  }
1828
1891
 
1829
1892
  if (foe.active.volatiles.Confusion) return;
1830
1893
  foe.active.volatiles.Confusion = true;
1831
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1894
+
1895
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1896
+ options.chance.observe(.confusion, player.foe(), .started);
1897
+ options.calc.confusion(player.foe());
1832
1898
 
1833
1899
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1834
1900
  }
@@ -1856,10 +1922,7 @@ pub const Effects = struct {
1856
1922
  const foe_ident = battle.active(player.foe());
1857
1923
 
1858
1924
  // Pokémon Showdown handles hit/miss earlier in doMove
1859
- if (!showdown) {
1860
- if (!try checkHit(battle, player, move, options)) return null;
1861
- try options.chance.commit(player, .hit);
1862
- }
1925
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1863
1926
 
1864
1927
  if (volatiles.disable_move != 0) {
1865
1928
  try options.log.fail(.{ foe_ident, .None });
@@ -1877,18 +1940,18 @@ pub const Effects = struct {
1877
1940
 
1878
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1879
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1880
- if (n == 0) {
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1881
1944
  try options.log.fail(.{ foe_ident, .None });
1882
1945
  return null;
1883
1946
  } else if (err) {
1884
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1885
- assert(!showdown);
1886
1948
  return Result.Error;
1887
1949
  }
1888
1950
 
1889
1951
  volatiles.disable_move =
1890
1952
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1891
1953
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1954
+ options.chance.observe(.disable, player.foe(), .started);
1892
1955
 
1893
1956
  const id = foe.active.move(volatiles.disable_move).id;
1894
1957
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1959,7 +2022,7 @@ pub const Effects = struct {
1959
2022
  if (foe.active.types.includes(move.type)) return;
1960
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1961
2024
  // Freeze Clause Mod
1962
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2025
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1963
2026
 
1964
2027
  foe_stored.status = Status.init(.FRZ);
1965
2028
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1972,7 +2035,7 @@ pub const Effects = struct {
1972
2035
  var side = battle.side(player);
1973
2036
  var foe = battle.foe(player);
1974
2037
 
1975
- var side_stored = side.stored();
2038
+ const side_stored = side.stored();
1976
2039
  var foe_stored = foe.stored();
1977
2040
 
1978
2041
  const player_ident = battle.active(player);
@@ -1995,22 +2058,24 @@ pub const Effects = struct {
1995
2058
  if (p != player and Status.any(s.stored().status)) {
1996
2059
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1997
2060
  s.stored().status = 0;
2061
+ options.chance.observe(.sleep, p, .None);
1998
2062
  } else if (showdown and s.stored().status == Status.TOX) {
1999
2063
  s.stored().status = Status.init(.PSN);
2000
2064
  try log.status(.{ battle.active(p), s.stored().status, .None });
2001
2065
  }
2002
- try clearVolatiles(battle, p, p != player, options);
2066
+ try clearVolatiles(battle, p, options);
2003
2067
  }
2004
2068
  } else {
2005
2069
  if (Status.any(foe_stored.status)) {
2006
2070
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2007
2071
  foe.last_selected_move = .SKIP_TURN;
2072
+ options.chance.observe(.sleep, player.foe(), .None);
2008
2073
  }
2009
2074
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2010
2075
  foe_stored.status = 0;
2011
2076
  }
2012
- try clearVolatiles(battle, player, false, options);
2013
- try clearVolatiles(battle, player.foe(), true, options);
2077
+ try clearVolatiles(battle, player, options);
2078
+ try clearVolatiles(battle, player.foe(), options);
2014
2079
  }
2015
2080
  }
2016
2081
 
@@ -2061,7 +2126,6 @@ pub const Effects = struct {
2061
2126
  }
2062
2127
  }
2063
2128
 
2064
- try options.chance.commit(player, .hit);
2065
2129
  foe.active.volatiles.LeechSeed = true;
2066
2130
 
2067
2131
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2102,12 +2166,10 @@ pub const Effects = struct {
2102
2166
  if (!has_mimic) {
2103
2167
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2104
2168
  if (!try checkHit(battle, player, move, options)) return;
2105
- try options.chance.commit(player, .hit);
2106
2169
  return options.log.fail(.{ battle.active(player.foe()), .None });
2107
2170
  }
2108
2171
  }
2109
2172
  if (!try checkHit(battle, player, move, options)) return;
2110
- try options.chance.commit(player, .hit);
2111
2173
 
2112
2174
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2113
2175
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2161,7 +2223,6 @@ pub const Effects = struct {
2161
2223
  if (immune) return log.immune(.{ foe_ident, .None });
2162
2224
  if (!try checkHit(battle, player, move, options)) return;
2163
2225
  }
2164
- try options.chance.commit(player, .hit);
2165
2226
 
2166
2227
  foe_stored.status = Status.init(.PAR);
2167
2228
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2229,7 +2290,6 @@ pub const Effects = struct {
2229
2290
 
2230
2291
  if (move.effect == .Poison) {
2231
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2232
- try options.chance.commit(player, .hit);
2233
2293
  } else {
2234
2294
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2235
2295
  return;
@@ -2298,13 +2358,12 @@ pub const Effects = struct {
2298
2358
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
2359
  });
2300
2360
  }
2301
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2361
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2302
2362
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2303
- try options.chance.commit(player, .hit);
2304
2363
  }
2305
2364
 
2306
2365
  // Sleep Clause Mod
2307
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2308
2367
  for (foe.pokemon) |p| {
2309
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2310
2369
  }
@@ -2312,6 +2371,8 @@ pub const Effects = struct {
2312
2371
  foe.active.volatiles.Recharging = false;
2313
2372
 
2314
2373
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2374
+ options.chance.observe(.sleep, player.foe(), .started);
2375
+
2315
2376
  const last = battle.side(player).last_selected_move;
2316
2377
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2317
2378
  }
@@ -2353,9 +2414,7 @@ pub const Effects = struct {
2353
2414
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2415
  if (showdown) {
2355
2416
  if (battle.side(player).last_selected_move == .Teleport) return;
2356
- if (try checkHit(battle, player, move, options)) {
2357
- try options.chance.commit(player, .hit);
2358
- }
2417
+ _ = try checkHit(battle, player, move, options);
2359
2418
  battle.last_damage = 0;
2360
2419
  } else {
2361
2420
  try options.log.fail(.{ battle.active(player), .None });
@@ -2370,6 +2429,8 @@ pub const Effects = struct {
2370
2429
 
2371
2430
  volatiles.Thrashing = true;
2372
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2432
+
2433
+ options.chance.observe(.attacking, player, .started);
2373
2434
  }
2374
2435
 
2375
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2408,13 +2469,14 @@ pub const Effects = struct {
2408
2469
  if (side.active.volatiles.Binding) return;
2409
2470
  side.active.volatiles.Binding = true;
2410
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2411
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2412
2473
  if (!showdown) {
2413
2474
  foe.active.volatiles.Recharging = false;
2414
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2415
2476
 
2416
2477
  side.active.volatiles.attacks =
2417
2478
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2479
+ options.chance.observe(.binding, player, .started);
2418
2480
  }
2419
2481
 
2420
2482
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,7 +2503,7 @@ pub const Effects = struct {
2441
2503
  }
2442
2504
  return log.fail(.{ ident, .None });
2443
2505
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2506
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2507
  const stat = unmodifiedStats(battle, side).atk;
2446
2508
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
2509
  try log.boost(.{ ident, reason, n });
@@ -2461,7 +2523,7 @@ pub const Effects = struct {
2461
2523
  }
2462
2524
  return log.fail(.{ ident, .None });
2463
2525
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2526
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2527
  const stat = unmodifiedStats(battle, side).def;
2466
2528
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
2529
  try log.boost(.{ ident, .Defense, n });
@@ -2478,7 +2540,7 @@ pub const Effects = struct {
2478
2540
  }
2479
2541
  return log.fail(.{ ident, .None });
2480
2542
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2543
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2544
  const stat = unmodifiedStats(battle, side).spe;
2483
2545
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
2546
  try log.boost(.{ ident, .Speed, 2 });
@@ -2498,7 +2560,7 @@ pub const Effects = struct {
2498
2560
  }
2499
2561
  return log.fail(.{ ident, .None });
2500
2562
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2563
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2564
  const stat = unmodifiedStats(battle, side).spc;
2503
2565
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
2566
  try log.boost(.{ ident, .SpecialAttack, n });
@@ -2531,7 +2593,6 @@ pub const Effects = struct {
2531
2593
  } else {
2532
2594
  // checkHit already checks for Invulnerable
2533
2595
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2596
  }
2536
2597
 
2537
2598
  var stats = &foe.active.stats;
@@ -2551,7 +2612,7 @@ pub const Effects = struct {
2551
2612
  }
2552
2613
  return log.fail(.{ foe_ident, .None });
2553
2614
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2615
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2616
  const stat = unmodifiedStats(battle, foe).atk;
2556
2617
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
2618
  try log.boost(.{ foe_ident, .Attack, -1 });
@@ -2569,7 +2630,7 @@ pub const Effects = struct {
2569
2630
  }
2570
2631
  return log.fail(.{ foe_ident, .None });
2571
2632
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2633
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2634
  const stat = unmodifiedStats(battle, foe).def;
2574
2635
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
2636
  try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
@@ -2586,7 +2647,7 @@ pub const Effects = struct {
2586
2647
  }
2587
2648
  return log.fail(.{ foe_ident, .None });
2588
2649
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2650
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2651
  const stat = unmodifiedStats(battle, foe).spe;
2591
2652
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
2653
  try log.boost(.{ foe_ident, .Speed, -1 });
@@ -2606,7 +2667,7 @@ pub const Effects = struct {
2606
2667
  }
2607
2668
  return log.fail(.{ foe_ident, .None });
2608
2669
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2670
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2671
  const stat = unmodifiedStats(battle, foe).spc;
2611
2672
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
2673
  try log.boost(.{ foe_ident, .SpecialAttack, -1 });
@@ -2645,22 +2706,22 @@ fn isForced(active: anytype) bool {
2645
2706
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2707
  }
2647
2708
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2709
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2710
  var log = options.log;
2650
2711
  var side = battle.side(who);
2651
2712
  var volatiles = &side.active.volatiles;
2652
2713
  const ident = battle.active(who);
2653
2714
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2715
  if (volatiles.disable_move != 0) {
2657
2716
  volatiles.disable_move = 0;
2658
2717
  volatiles.disable_duration = 0;
2718
+ options.chance.observe(.disable, who, .None);
2659
2719
  try log.end(.{ ident, .DisableSilent });
2660
2720
  }
2661
2721
  if (volatiles.Confusion) {
2662
2722
  // volatiles.confusion is left unchanged
2663
2723
  volatiles.Confusion = false;
2724
+ options.chance.observe(.confusion, who, .None);
2664
2725
  try log.end(.{ ident, .ConfusionSilent });
2665
2726
  }
2666
2727
  if (volatiles.Mist) {
@@ -2695,10 +2756,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2695
2756
  }
2696
2757
  }
2697
2758
 
2759
+ fn decrement(
2760
+ comptime field: chance.Action.Field,
2761
+ player: Player,
2762
+ options: anytype,
2763
+ n: anytype,
2764
+ ) @TypeOf(n) {
2765
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2766
+ .started, .ended => 0,
2767
+ .continuing => if (n > 1) n - 1 else n,
2768
+ else => unreachable,
2769
+ } else n - 1;
2770
+ }
2771
+
2698
2772
  pub const Rolls = struct {
2699
2773
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2774
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2775
  player == .P1
2776
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2777
+ battle.rng.speedTie()
2702
2778
  else if (showdown)
2703
2779
  battle.rng.range(u8, 0, 2) == 0
2704
2780
  else
@@ -2708,21 +2784,31 @@ pub const Rolls = struct {
2708
2784
  return p1;
2709
2785
  }
2710
2786
 
2711
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2787
+ fn criticalHit(
2788
+ battle: anytype,
2789
+ player: Player,
2790
+ rate: u8,
2791
+ spurious: bool,
2792
+ options: anytype,
2793
+ ) !bool {
2712
2794
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2797
+ battle.rng.criticalHit(player, rate)
2714
2798
  else if (showdown)
2715
2799
  battle.rng.chance(u8, rate, 256)
2716
2800
  else
2717
2801
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2802
 
2719
- try options.chance.criticalHit(player, crit, rate);
2803
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2804
  return crit;
2721
2805
  }
2722
2806
 
2723
2807
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2808
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2809
  val
2810
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2811
+ battle.rng.damage(player)
2726
2812
  else roll: {
2727
2813
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2814
  while (true) {
@@ -2736,21 +2822,27 @@ pub const Rolls = struct {
2736
2822
  return roll;
2737
2823
  }
2738
2824
 
2739
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2825
+ const uN = if (pkmn.options.miss) u8 else u9;
2826
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2827
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2740
2828
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2829
  val == .true
2830
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2831
+ battle.rng.hit(player, acc)
2742
2832
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2833
+ battle.rng.chance(uN, acc, 256)
2744
2834
  else
2745
- battle.rng.next() < accuracy;
2835
+ battle.rng.next() < acc;
2746
2836
 
2747
- options.chance.hit(ok, accuracy);
2837
+ try options.chance.hit(player, ok, acc);
2748
2838
  return ok;
2749
2839
  }
2750
2840
 
2751
2841
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2842
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2843
  val == .true
2844
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2845
+ battle.rng.confused(player)
2754
2846
  else if (showdown)
2755
2847
  !battle.rng.chance(u8, 128, 256)
2756
2848
  else
@@ -2763,6 +2855,8 @@ pub const Rolls = struct {
2763
2855
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2856
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2857
  val == .true
2858
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2859
+ battle.rng.paralyzed(player)
2766
2860
  else if (showdown)
2767
2861
  battle.rng.chance(u8, 63, 256)
2768
2862
  else
@@ -2775,6 +2869,8 @@ pub const Rolls = struct {
2775
2869
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2870
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2871
  val == .true
2872
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2873
+ battle.rng.secondaryChance(player, 25)
2778
2874
  else if (showdown)
2779
2875
  battle.rng.chance(u8, 25, 256)
2780
2876
  else
@@ -2789,6 +2885,8 @@ pub const Rolls = struct {
2789
2885
 
2790
2886
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2887
  val == .true
2888
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2889
+ battle.rng.secondaryChance(player, rate)
2792
2890
  else if (showdown)
2793
2891
  battle.rng.chance(u8, rate, 256)
2794
2892
  else
@@ -2803,6 +2901,8 @@ pub const Rolls = struct {
2803
2901
 
2804
2902
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2903
  val == .true
2904
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2905
+ battle.rng.secondaryChance(player, rate)
2806
2906
  else if (showdown)
2807
2907
  battle.rng.chance(u8, rate, 256)
2808
2908
  else
@@ -2815,6 +2915,8 @@ pub const Rolls = struct {
2815
2915
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2916
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2917
  val == .true
2918
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2919
+ battle.rng.secondaryChance(player, 85)
2818
2920
  else if (showdown)
2819
2921
  battle.rng.chance(u8, 85, 256)
2820
2922
  else
@@ -2827,6 +2929,8 @@ pub const Rolls = struct {
2827
2929
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2930
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2931
  val
2932
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2933
+ battle.rng.metronome(player)
2830
2934
  else if (showdown)
2831
2935
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2936
  else move: {
@@ -2845,6 +2949,8 @@ pub const Rolls = struct {
2845
2949
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2950
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2951
  val - 1
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2953
+ battle.rng.psywave(player)
2848
2954
  else if (showdown)
2849
2955
  battle.rng.range(u8, 0, max)
2850
2956
  else power: {
@@ -2860,8 +2966,11 @@ pub const Rolls = struct {
2860
2966
  }
2861
2967
 
2862
2968
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
- @intCast(val)
2969
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2970
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2971
+ else
2972
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2973
+ battle.rng.sleepDuration(player)
2865
2974
  else if (showdown)
2866
2975
  battle.rng.range(u3, 1, 8)
2867
2976
  else duration: {
@@ -2872,46 +2981,55 @@ pub const Rolls = struct {
2872
2981
  };
2873
2982
 
2874
2983
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2984
+ options.chance.duration(player, duration);
2876
2985
  return duration;
2877
2986
  }
2878
2987
 
2879
2988
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
- val
2989
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2990
+ if (val >= 1 and val <= 8) val else null
2991
+ else
2992
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2993
+ battle.rng.disableDuration(player)
2882
2994
  else if (showdown)
2883
2995
  battle.rng.range(u4, 1, 9)
2884
2996
  else
2885
2997
  @intCast((battle.rng.next() & 7) + 1);
2886
2998
 
2887
2999
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
3000
+ options.chance.duration(player, duration);
2889
3001
  return duration;
2890
3002
  }
2891
3003
 
2892
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2893
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2894
- @intCast(val)
3004
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3005
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3006
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3007
+ else
3008
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3009
+ battle.rng.confusionDuration(player)
2895
3010
  else if (showdown)
2896
3011
  battle.rng.range(u3, 2, 6)
2897
3012
  else
2898
3013
  @intCast((battle.rng.next() & 3) + 2);
2899
3014
 
2900
3015
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3016
+ options.chance.duration(player, duration);
2902
3017
  return duration;
2903
3018
  }
2904
3019
 
2905
3020
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
3021
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3022
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3023
+ else
3024
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3025
+ battle.rng.attackingDuration(player)
2908
3026
  else if (showdown)
2909
3027
  battle.rng.range(u3, 2, 4)
2910
3028
  else
2911
3029
  @intCast((battle.rng.next() & 1) + 2);
2912
3030
 
2913
3031
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3032
+ options.chance.duration(player, duration);
2915
3033
  return duration;
2916
3034
  }
2917
3035
 
@@ -2924,8 +3042,11 @@ pub const Rolls = struct {
2924
3042
  options: anytype,
2925
3043
  ) !u3 {
2926
3044
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
- @intCast(val)
3045
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3046
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3047
+ else
3048
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3049
+ battle.rng.distribution(player)
2929
3050
  else if (showdown)
2930
3051
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3052
  else n: {
@@ -2935,7 +3056,7 @@ pub const Rolls = struct {
2935
3056
 
2936
3057
  assert(n >= 2 and n <= 5);
2937
3058
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3059
+ options.chance.duration(player, n);
2939
3060
  } else {
2940
3061
  try options.chance.multiHit(player, n);
2941
3062
  }
@@ -2950,15 +3071,15 @@ pub const Rolls = struct {
2950
3071
  options: anytype,
2951
3072
  ) !u4 {
2952
3073
  // TODO: consider throwing error instead of rerolling?
2953
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2954
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2955
3076
  val
2956
3077
  else
2957
3078
  null
2958
3079
  else
2959
- null;
2960
-
2961
- const slot: u4 = overridden orelse slot: {
3080
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3081
+ battle.rng.moveSlot(player, moves, check_pp)
3082
+ else slot: {
2962
3083
  if (showdown) {
2963
3084
  if (check_pp == 0) {
2964
3085
  var i: usize = moves.len;
@@ -3038,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3159
  const side = battle.side(player);
3039
3160
  const foe = battle.foe(player);
3040
3161
 
3041
- var active = &side.active;
3162
+ const active = &side.active;
3042
3163
  const stored = side.stored();
3043
3164
 
3044
3165
  // While players are not given any input options on the cartridge in these cases,
3045
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3166
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3167
  //
3047
3168
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3169
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3171
  // passing due to being forced into using a move. Instead of introducing another option
3051
3172
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3173
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3174
+ // switch at all
3054
3175
  if (isForced(active)) {
3055
3176
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3177
  n += 1;
@@ -3069,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3190
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3191
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3192
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3193
+ // move normally if sleeping/frozen/bound
3073
3194
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3195
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3196
  {
@@ -3082,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3203
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3204
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3205
  // Move will not result in forcing the subsequent use unless the user also had the
3085
- // proc-ed move in their moveset) and disabling all other moves.
3206
+ // proc-ed move in their moveset) and disabling all other moves
3086
3207
  if (limited) {
3087
3208
  assert(showdown);
3088
3209
  assert(side.last_selected_move != .None);
@@ -3114,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3114
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3115
3236
  if (n == before) {
3116
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3117
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3118
3239
  while (slot <= 4) : (slot += 1) {
3119
3240
  if (active.moves[slot - 1].pp != 0) return n;
3120
3241
  }