@pkmn/engine 0.1.0-dev.c544e10e → 0.1.0-dev.cb3978dd

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 (79) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +41 -14
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +44 -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 +97 -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 +21 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +22 -0
  13. package/build/pkg/addon.js +35 -58
  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 +10 -13
  20. package/build/pkg/gen1.js +1 -1
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +55 -13
  23. package/build/pkg/index.js +15 -10
  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 +36 -70
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +141 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +141 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +349 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +18 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +253 -89
  39. package/package.json +58 -30
  40. package/src/bin/install-pkmn-engine +29 -19
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/common/array.zig +77 -0
  43. package/src/lib/common/data.zig +4 -4
  44. package/src/lib/common/debug.zig +39 -8
  45. package/src/lib/common/js.zig +4 -2
  46. package/src/lib/common/optional.zig +14 -10
  47. package/src/lib/common/options.zig +6 -0
  48. package/src/lib/common/protocol.zig +75 -79
  49. package/src/lib/common/rational.zig +128 -32
  50. package/src/lib/common/rng.zig +7 -9
  51. package/src/lib/common/util.zig +50 -1
  52. package/src/lib/gen1/calc.zig +686 -159
  53. package/src/lib/gen1/chance.zig +405 -280
  54. package/src/lib/gen1/data/moves.zig +2 -4
  55. package/src/lib/gen1/data/species.zig +2 -4
  56. package/src/lib/gen1/data/types.zig +1 -2
  57. package/src/lib/gen1/data.zig +24 -27
  58. package/src/lib/gen1/helpers.zig +92 -548
  59. package/src/lib/gen1/mechanics.zig +373 -257
  60. package/src/lib/gen2/calc.zig +37 -27
  61. package/src/lib/gen2/chance.zig +84 -496
  62. package/src/lib/gen2/data/items.zig +1 -3
  63. package/src/lib/gen2/data/moves.zig +2 -4
  64. package/src/lib/gen2/data/species.zig +1 -3
  65. package/src/lib/gen2/data/types.zig +1 -2
  66. package/src/lib/gen2/data.zig +19 -22
  67. package/src/lib/gen2/generated.zig +19 -26
  68. package/src/lib/gen2/helpers.zig +46 -25
  69. package/src/lib/gen2/mechanics.zig +200 -157
  70. package/src/lib/{binding/node.zig → node.zig} +8 -13
  71. package/src/lib/pkmn.zig +36 -15
  72. package/src/lib/{binding/wasm.zig → wasm.zig} +1 -1
  73. package/src/tools/display/dom.ts +96 -0
  74. package/src/tools/display/ui.css +354 -0
  75. package/build/test/display.js +0 -400
  76. package/build/test/display.js.map +0 -1
  77. package/src/lib/gen1/README.md +0 -894
  78. package/src/lib/gen2/README.md +0 -329
  79. 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;
@@ -71,8 +62,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
71
62
  var p1 = battle.side(.P1);
72
63
  var p2 = battle.side(.P2);
73
64
 
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;
65
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
66
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
67
 
77
68
  if (try turnOrder(battle, c1, c2, options) == .P1) {
78
69
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -90,11 +81,11 @@ fn start(battle: anytype, options: anytype) !Result {
90
81
  const p1 = battle.side(.P1);
91
82
  const p2 = battle.side(.P2);
92
83
 
93
- var p1_slot = findFirstAlive(p1);
84
+ const p1_slot = findFirstAlive(p1);
94
85
  assert(!showdown or p1_slot == 1);
95
86
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
96
87
 
97
- var p2_slot = findFirstAlive(p2);
88
+ const p2_slot = findFirstAlive(p2);
98
89
  assert(!showdown or p2_slot == 1);
99
90
  if (p2_slot == 0) return Result.Win;
100
91
 
@@ -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,14 +665,8 @@ fn beforeMove(
635
665
  volatiles.state +%= battle.last_damage;
636
666
  }
637
667
 
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);
668
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
669
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
646
670
 
647
671
  if (volatiles.attacks != 0) {
648
672
  try log.activate(.{ ident, .Bide });
@@ -676,39 +700,40 @@ fn beforeMove(
676
700
  }
677
701
 
678
702
  if (volatiles.Thrashing) {
679
- assert(volatiles.attacks > 0);
703
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
680
704
 
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);
705
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
706
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
707
+
708
+ if (volatiles.attacks == 0) {
709
+ const overwritten = volatiles.Confusion;
687
710
 
688
- if (!showdown and handleThrashing(battle, active, player, options)) {
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)
689
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);
690
725
  }
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
- }
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
+
700
731
  return .skip_can;
701
732
  }
702
733
 
703
734
  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);
735
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
736
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
712
737
 
713
738
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
739
  if (showdown or battle.last_damage != 0) {
@@ -738,7 +763,6 @@ fn canMove(
738
763
  const player_ident = battle.active(player);
739
764
  const move = Move.get(side.last_selected_move);
740
765
 
741
- var skip = skip_pp;
742
766
  if (side.active.volatiles.Charging) {
743
767
  side.active.volatiles.Charging = false;
744
768
  side.active.volatiles.Invulnerable = false;
@@ -750,7 +774,7 @@ fn canMove(
750
774
  }
751
775
 
752
776
  side.last_used_move = side.last_selected_move;
753
- if (!skip) decrementPP(side, mslot, auto);
777
+ if (!skip_pp) decrementPP(side, mslot, auto);
754
778
 
755
779
  const target = if (move.target == .Self) player else player.foe();
756
780
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -763,9 +787,14 @@ fn canMove(
763
787
 
764
788
  if (move.effect == .Thrashing) {
765
789
  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);
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
+ }
769
798
  }
770
799
 
771
800
  return true;
@@ -802,7 +831,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
802
831
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
803
832
  }
804
833
 
805
- fn incrementPP(side: *Side, mslot: u4) void {
834
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
806
835
  var active = &side.active;
807
836
  const volatiles = &active.volatiles;
808
837
 
@@ -810,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
810
839
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
811
840
  if (showdown and volatiles.Transform) return;
812
841
 
842
+ const n = options.calc.overridden(player, .pp) orelse 1;
843
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
844
+
813
845
  assert(mslot > 0 and mslot <= 4);
814
- 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;
815
847
  }
816
848
 
817
849
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -859,13 +891,13 @@ fn doMove(
859
891
  break :miss true;
860
892
  }
861
893
  break :miss (move.target != .Self and
862
- !moveHit(battle, player, move, &immune, &mist, options));
894
+ !try moveHit(battle, player, move, &immune, &mist, options));
863
895
  };
864
896
  assert(!immune or miss or (showdown and move.effect == .Binding));
865
897
 
866
898
  var late = showdown and move.effect != .Explode;
867
899
  const skip = status or immune;
868
- if ((!showdown or (!skip or counter)) and !miss) blk: {
900
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
869
901
  if (showdown and move.effect.isMulti()) {
870
902
  try Effects.multiHit(battle, player, move, options);
871
903
  hits = side.active.volatiles.attacks;
@@ -887,8 +919,9 @@ fn doMove(
887
919
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
888
920
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
889
921
  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)) {
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);
892
925
  return @as(?Result, Result.Error);
893
926
  }
894
927
  if (battle.last_damage == 0) {
@@ -902,11 +935,12 @@ fn doMove(
902
935
  }
903
936
  }
904
937
 
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);
938
+ var missed: bool = undefined;
939
+ const zero = battle.last_damage == 0;
940
+ if (!showdown and !skip) {
941
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
942
+ miss = missed or zero;
943
+ }
910
944
 
911
945
  assert(showdown or miss or battle.last_damage > 0 or skip);
912
946
  assert((!showdown and miss) or !(ohko and immune));
@@ -933,11 +967,32 @@ fn doMove(
933
967
  try log.immune(.{ foe_ident, .OHKO });
934
968
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
935
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
+ }
936
991
  } else if (mist) {
937
992
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
938
993
  try log.fail(.{ foe_ident, .None });
939
994
  } else {
940
- 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);
941
996
  try log.lastmiss(.{});
942
997
  try log.miss(.{battle.active(player)});
943
998
  }
@@ -952,17 +1007,15 @@ fn doMove(
952
1007
  try buildRage(battle, player.foe(), options);
953
1008
  } else if (showdown and move.effect == .Disable) {
954
1009
  try buildRage(battle, player.foe(), options);
955
- } else if (!showdown and immune and move.effect == .Binding) {
956
- try options.chance.commit(player, .binding);
957
1010
  }
958
1011
  return null;
959
- } else {
1012
+ } else if (!showdown) {
960
1013
  try options.chance.commit(player, .hit);
961
1014
  }
962
1015
 
963
1016
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
964
1017
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
965
- // it up above before damage calculation).
1018
+ // it up above before damage calculation)
966
1019
  if (!showdown and move.effect.isMulti()) {
967
1020
  try Effects.multiHit(battle, player, move, options);
968
1021
  hits = side.active.volatiles.attacks;
@@ -1001,32 +1054,30 @@ fn doMove(
1001
1054
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1002
1055
  }
1003
1056
  try log.hitcount(.{ battle.active(player.foe()), hit });
1004
- } else if (showdown) {
1057
+ } else if (showdown and move.effect == .Binding) {
1005
1058
  // 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;
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);
1019
1069
  }
1070
+ return null;
1020
1071
  }
1021
1072
  }
1022
1073
 
1023
1074
  // 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.
1075
+ // if an effect was intended to "always happen" it will still get skipped
1025
1076
  if (nullified) return null;
1026
1077
 
1027
1078
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1028
1079
  // 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.
1080
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1030
1081
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1031
1082
 
1032
1083
  if (foe.stored().hp == 0) return null;
@@ -1041,7 +1092,7 @@ fn doMove(
1041
1092
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1042
1093
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1043
1094
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1044
- // puts the |-status| message before |-hitcount| instead of after.
1095
+ // puts the |-status| message before |-hitcount| instead of after
1045
1096
  if (move.effect == .Twineedle) {
1046
1097
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1047
1098
  } else if (move.effect == .Disable) {
@@ -1060,12 +1111,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1060
1111
  const side = battle.side(player);
1061
1112
 
1062
1113
  // Base speed is used for the critical hit calculation, even when Transform-ed
1063
- var rate = @as(u16, Species.chance(side.stored().species));
1114
+ var rate: u16 = Species.chance(side.stored().species);
1064
1115
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1065
1116
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1066
1117
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1067
1118
 
1068
- 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);
1069
1122
  }
1070
1123
 
1071
1124
  fn calcDamage(
@@ -1074,6 +1127,7 @@ fn calcDamage(
1074
1127
  target_player: Player,
1075
1128
  m: ?Move.Data,
1076
1129
  crit: bool,
1130
+ options: anytype,
1077
1131
  ) bool {
1078
1132
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1079
1133
  const cfz = m == null;
@@ -1085,28 +1139,8 @@ fn calcDamage(
1085
1139
 
1086
1140
  const special = move.type.special();
1087
1141
 
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
1142
+ var atk = attack(side, crit, special);
1143
+ var def = defense(target, crit, special, cfz);
1110
1144
 
1111
1145
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1112
1146
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1114,12 +1148,12 @@ fn calcDamage(
1114
1148
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1115
1149
  atk = @max((atk / 4) & 255, 1);
1116
1150
  // 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);
1151
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1118
1152
  }
1119
1153
 
1120
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1154
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1121
1155
 
1122
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1156
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1123
1157
 
1124
1158
  if (def == 0) return false;
1125
1159
 
@@ -1131,11 +1165,41 @@ fn calcDamage(
1131
1165
  d = @min(997, d);
1132
1166
  d += 2;
1133
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
+
1134
1178
  battle.last_damage = @intCast(d);
1135
1179
 
1136
1180
  return true;
1137
1181
  }
1138
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
+
1139
1203
  fn adjustDamage(battle: anytype, player: Player) u16 {
1140
1204
  const side = battle.side(player);
1141
1205
  const foe = battle.foe(player);
@@ -1180,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1180
1244
  const foe = battle.foe(player);
1181
1245
 
1182
1246
  if (!try checkHit(battle, player, move, options)) return null;
1183
- try options.chance.commit(player, .hit);
1184
1247
 
1185
1248
  battle.last_damage = switch (side.last_selected_move) {
1186
1249
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1191,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1191
1254
  .Psywave => power: {
1192
1255
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1193
1256
  // GLITCH: Psywave infinite glitch loop
1194
- if (!showdown and max <= 1) return Result.Error;
1257
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1195
1258
  break :power try Rolls.psywave(battle, player, max, options);
1196
1259
  },
1197
1260
  else => unreachable,
1198
1261
  };
1199
1262
 
1200
- 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;
1201
1264
 
1202
1265
  const sub = showdown and foe.active.volatiles.Substitute;
1203
1266
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1234,16 +1297,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1297
 
1235
1298
  if (!used or !selected) {
1236
1299
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1237
- if (!showdown) return Result.Error;
1300
+ if (!pkmn.options.mod) return Result.Error;
1238
1301
  try options.log.fail(.{ battle.active(player), .None });
1239
1302
  return null;
1240
1303
  }
1241
1304
 
1242
1305
  battle.last_damage *|= 2;
1243
1306
 
1244
- // Pokémon Showdown calls checkHit before Counter
1307
+ // Pokémon Showdown calls moveHit before Counter
1245
1308
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1246
- try options.chance.commit(player, .hit);
1247
1309
 
1248
1310
  const sub = showdown and foe.active.volatiles.Substitute;
1249
1311
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1272,7 +1334,7 @@ fn applyDamage(
1272
1334
  subbed.active.volatiles.Substitute = false;
1273
1335
  // battle.last_damage is not updated with the amount of HP the Substitute had
1274
1336
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1275
- options.calc.capped(target_player);
1337
+ if (reason != .Confusion) options.calc.capped(sub_player);
1276
1338
  return true;
1277
1339
  } else {
1278
1340
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1306,7 +1368,7 @@ fn mirrorMove(
1306
1368
  try options.log.fail(.{ battle.active(player), .None });
1307
1369
  return null;
1308
1370
  } else if (!showdown or foe.last_used_move != .Struggle) {
1309
- incrementPP(side, mslot);
1371
+ incrementPP(side, player, mslot, options);
1310
1372
  }
1311
1373
 
1312
1374
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1327,7 +1389,7 @@ fn metronome(
1327
1389
  var side = battle.side(player);
1328
1390
 
1329
1391
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1330
- incrementPP(side, mslot);
1392
+ incrementPP(side, player, mslot, options);
1331
1393
 
1332
1394
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1333
1395
  return null;
@@ -1339,7 +1401,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1339
1401
  var immune = false;
1340
1402
  var mist = false;
1341
1403
 
1342
- 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
+ }
1343
1408
 
1344
1409
  assert(!immune);
1345
1410
  if (mist) {
@@ -1348,11 +1413,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1348
1413
  try options.log.activate(.{ foe_ident, .Mist });
1349
1414
  try options.log.fail(.{ foe_ident, .None });
1350
1415
  } else {
1351
- try options.chance.commit(player, .miss);
1352
1416
  try options.log.lastmiss(.{});
1353
1417
  try options.log.miss(.{battle.active(player)});
1354
1418
  }
1355
1419
 
1420
+ if (!showdown) try options.chance.commit(player, .miss);
1356
1421
  return false;
1357
1422
  }
1358
1423
 
@@ -1363,7 +1428,7 @@ fn moveHit(
1363
1428
  immune: *bool,
1364
1429
  mist: *bool,
1365
1430
  options: anytype,
1366
- ) bool {
1431
+ ) !bool {
1367
1432
  var side = battle.side(player);
1368
1433
  const foe = battle.foe(player);
1369
1434
 
@@ -1402,7 +1467,7 @@ fn moveHit(
1402
1467
  const overwritten = overwrite and state > 0;
1403
1468
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1404
1469
 
1405
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1470
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1406
1471
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1407
1472
  accuracy = accuracy * boost[0] / boost[1];
1408
1473
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1414,7 +1479,7 @@ fn moveHit(
1414
1479
  if (overwrite) side.active.volatiles.state = accuracy;
1415
1480
 
1416
1481
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1417
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1482
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1418
1483
  };
1419
1484
 
1420
1485
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,13 +1490,17 @@ fn moveHit(
1425
1490
 
1426
1491
  if (!miss) return true;
1427
1492
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1493
+
1428
1494
  side.active.volatiles.Binding = false;
1495
+ options.chance.observe(.binding, player, .None);
1496
+
1429
1497
  return false;
1430
1498
  }
1431
1499
 
1432
1500
  fn checkFaint(
1433
1501
  battle: anytype,
1434
1502
  player: Player,
1503
+ cap: bool,
1435
1504
  options: anytype,
1436
1505
  ) @TypeOf(options.log).Error!?Result {
1437
1506
  const side = battle.side(player);
@@ -1444,8 +1513,8 @@ fn checkFaint(
1444
1513
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1445
1514
  const more = player_out or foe_out;
1446
1515
 
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;
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;
1449
1518
 
1450
1519
  assert(!side.active.volatiles.MultiHit);
1451
1520
  assert(!foe.active.volatiles.MultiHit);
@@ -1466,7 +1535,7 @@ fn checkFaint(
1466
1535
  return .{ .p1 = foe_choice, .p2 = .Switch };
1467
1536
  }
1468
1537
 
1469
- 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 {
1470
1539
  var side = battle.side(player);
1471
1540
  var foe = battle.foe(player);
1472
1541
  assert(side.stored().hp == 0);
@@ -1475,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1475
1544
  assert(!foe_volatiles.MultiHit);
1476
1545
  if (foe_volatiles.Bide) {
1477
1546
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1478
- 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;
1479
1548
  if (foe_volatiles.state != 0) return Result.Error;
1480
1549
  }
1481
1550
 
@@ -1500,7 +1569,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1500
1569
  }
1501
1570
 
1502
1571
  try options.log.faint(.{ battle.active(player), done });
1503
- 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);
1504
1575
  return null;
1505
1576
  }
1506
1577
 
@@ -1564,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1635
 
1565
1636
  battle.turn += 1;
1566
1637
 
1567
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1638
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1568
1639
  try options.log.tie(.{});
1569
1640
  return Result.Tie;
1570
1641
  }
1571
1642
 
1572
- if (showdown and battle.turn >= 1000) {
1643
+ if (pkmn.options.mod and battle.turn >= 1000) {
1573
1644
  try options.log.tie(.{});
1574
1645
  return Result.Tie;
1575
1646
  } else if (battle.turn >= 65535) {
@@ -1660,17 +1731,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1660
1731
  }
1661
1732
  }
1662
1733
 
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
1734
  fn onBegin(
1675
1735
  battle: anytype,
1676
1736
  player: Player,
@@ -1679,7 +1739,6 @@ fn onBegin(
1679
1739
  residual: *bool,
1680
1740
  options: anytype,
1681
1741
  ) !void {
1682
- assert(move.effect.onBegin());
1683
1742
  return switch (move.effect) {
1684
1743
  .Confusion => Effects.confusion(battle, player, move, options),
1685
1744
  .Conversion => Effects.conversion(battle, player, options),
@@ -1702,7 +1761,6 @@ fn onBegin(
1702
1761
  }
1703
1762
 
1704
1763
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
- assert(move.effect.onEnd());
1706
1764
  return switch (move.effect) {
1707
1765
  .Bide => Effects.bide(battle, player, options),
1708
1766
  // zig fmt: off
@@ -1724,7 +1782,6 @@ fn alwaysHappens(
1724
1782
  residual: *bool,
1725
1783
  options: anytype,
1726
1784
  ) !void {
1727
- assert(move.effect.alwaysHappens());
1728
1785
  return switch (move.effect) {
1729
1786
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1730
1787
  .Explode => Effects.explode(battle, player),
@@ -1760,8 +1817,10 @@ pub const Effects = struct {
1760
1817
 
1761
1818
  side.active.volatiles.Bide = true;
1762
1819
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1820
+
1763
1821
  side.active.volatiles.state = 0;
1764
1822
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1823
+ options.chance.observe(.attacking, player, .started);
1765
1824
 
1766
1825
  try options.log.start(.{ battle.active(player), .Bide });
1767
1826
  }
@@ -1823,12 +1882,14 @@ pub const Effects = struct {
1823
1882
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1883
  if (!try checkHit(battle, player, move, options)) return;
1825
1884
  }
1826
- try options.chance.commit(player, .hit);
1827
1885
  }
1828
1886
 
1829
1887
  if (foe.active.volatiles.Confusion) return;
1830
1888
  foe.active.volatiles.Confusion = true;
1831
- 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());
1832
1893
 
1833
1894
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1834
1895
  }
@@ -1856,10 +1917,7 @@ pub const Effects = struct {
1856
1917
  const foe_ident = battle.active(player.foe());
1857
1918
 
1858
1919
  // 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
- }
1920
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1863
1921
 
1864
1922
  if (volatiles.disable_move != 0) {
1865
1923
  try options.log.fail(.{ foe_ident, .None });
@@ -1877,18 +1935,18 @@ pub const Effects = struct {
1877
1935
 
1878
1936
  // Technically this is still considered simply a "miss" on the cartridge,
1879
1937
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1880
- if (n == 0) {
1938
+ if (n == 0 or (pkmn.options.mod and err)) {
1881
1939
  try options.log.fail(.{ foe_ident, .None });
1882
1940
  return null;
1883
1941
  } else if (err) {
1884
1942
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1885
- assert(!showdown);
1886
1943
  return Result.Error;
1887
1944
  }
1888
1945
 
1889
1946
  volatiles.disable_move =
1890
1947
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1891
1948
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1949
+ options.chance.observe(.disable, player.foe(), .started);
1892
1950
 
1893
1951
  const id = foe.active.move(volatiles.disable_move).id;
1894
1952
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1959,7 +2017,7 @@ pub const Effects = struct {
1959
2017
  if (foe.active.types.includes(move.type)) return;
1960
2018
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1961
2019
  // Freeze Clause Mod
1962
- 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;
1963
2021
 
1964
2022
  foe_stored.status = Status.init(.FRZ);
1965
2023
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1972,7 +2030,7 @@ pub const Effects = struct {
1972
2030
  var side = battle.side(player);
1973
2031
  var foe = battle.foe(player);
1974
2032
 
1975
- var side_stored = side.stored();
2033
+ const side_stored = side.stored();
1976
2034
  var foe_stored = foe.stored();
1977
2035
 
1978
2036
  const player_ident = battle.active(player);
@@ -1995,22 +2053,24 @@ pub const Effects = struct {
1995
2053
  if (p != player and Status.any(s.stored().status)) {
1996
2054
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1997
2055
  s.stored().status = 0;
2056
+ options.chance.observe(.sleep, p, .None);
1998
2057
  } else if (showdown and s.stored().status == Status.TOX) {
1999
2058
  s.stored().status = Status.init(.PSN);
2000
2059
  try log.status(.{ battle.active(p), s.stored().status, .None });
2001
2060
  }
2002
- try clearVolatiles(battle, p, p != player, options);
2061
+ try clearVolatiles(battle, p, options);
2003
2062
  }
2004
2063
  } else {
2005
2064
  if (Status.any(foe_stored.status)) {
2006
2065
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2007
2066
  foe.last_selected_move = .SKIP_TURN;
2067
+ options.chance.observe(.sleep, player.foe(), .None);
2008
2068
  }
2009
2069
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2010
2070
  foe_stored.status = 0;
2011
2071
  }
2012
- try clearVolatiles(battle, player, false, options);
2013
- try clearVolatiles(battle, player.foe(), true, options);
2072
+ try clearVolatiles(battle, player, options);
2073
+ try clearVolatiles(battle, player.foe(), options);
2014
2074
  }
2015
2075
  }
2016
2076
 
@@ -2061,7 +2121,6 @@ pub const Effects = struct {
2061
2121
  }
2062
2122
  }
2063
2123
 
2064
- try options.chance.commit(player, .hit);
2065
2124
  foe.active.volatiles.LeechSeed = true;
2066
2125
 
2067
2126
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2102,12 +2161,10 @@ pub const Effects = struct {
2102
2161
  if (!has_mimic) {
2103
2162
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2104
2163
  if (!try checkHit(battle, player, move, options)) return;
2105
- try options.chance.commit(player, .hit);
2106
2164
  return options.log.fail(.{ battle.active(player.foe()), .None });
2107
2165
  }
2108
2166
  }
2109
2167
  if (!try checkHit(battle, player, move, options)) return;
2110
- try options.chance.commit(player, .hit);
2111
2168
 
2112
2169
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2113
2170
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2161,7 +2218,6 @@ pub const Effects = struct {
2161
2218
  if (immune) return log.immune(.{ foe_ident, .None });
2162
2219
  if (!try checkHit(battle, player, move, options)) return;
2163
2220
  }
2164
- try options.chance.commit(player, .hit);
2165
2221
 
2166
2222
  foe_stored.status = Status.init(.PAR);
2167
2223
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2229,7 +2285,6 @@ pub const Effects = struct {
2229
2285
 
2230
2286
  if (move.effect == .Poison) {
2231
2287
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2232
- try options.chance.commit(player, .hit);
2233
2288
  } else {
2234
2289
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2235
2290
  return;
@@ -2298,13 +2353,12 @@ pub const Effects = struct {
2298
2353
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
2354
  });
2300
2355
  }
2301
- // 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
2302
2357
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2303
- try options.chance.commit(player, .hit);
2304
2358
  }
2305
2359
 
2306
2360
  // Sleep Clause Mod
2307
- if (showdown) {
2361
+ if (pkmn.options.mod) {
2308
2362
  for (foe.pokemon) |p| {
2309
2363
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2310
2364
  }
@@ -2312,6 +2366,8 @@ pub const Effects = struct {
2312
2366
  foe.active.volatiles.Recharging = false;
2313
2367
 
2314
2368
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2369
+ options.chance.observe(.sleep, player.foe(), .started);
2370
+
2315
2371
  const last = battle.side(player).last_selected_move;
2316
2372
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2317
2373
  }
@@ -2353,9 +2409,7 @@ pub const Effects = struct {
2353
2409
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2410
  if (showdown) {
2355
2411
  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
- }
2412
+ _ = try checkHit(battle, player, move, options);
2359
2413
  battle.last_damage = 0;
2360
2414
  } else {
2361
2415
  try options.log.fail(.{ battle.active(player), .None });
@@ -2370,6 +2424,8 @@ pub const Effects = struct {
2370
2424
 
2371
2425
  volatiles.Thrashing = true;
2372
2426
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2427
+
2428
+ options.chance.observe(.attacking, player, .started);
2373
2429
  }
2374
2430
 
2375
2431
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2408,13 +2464,14 @@ pub const Effects = struct {
2408
2464
  if (side.active.volatiles.Binding) return;
2409
2465
  side.active.volatiles.Binding = true;
2410
2466
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2411
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2467
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2412
2468
  if (!showdown) {
2413
2469
  foe.active.volatiles.Recharging = false;
2414
2470
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2415
2471
 
2416
2472
  side.active.volatiles.attacks =
2417
2473
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2474
+ options.chance.observe(.binding, player, .started);
2418
2475
  }
2419
2476
 
2420
2477
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,7 +2498,7 @@ pub const Effects = struct {
2441
2498
  }
2442
2499
  return log.fail(.{ ident, .None });
2443
2500
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2501
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2502
  const stat = unmodifiedStats(battle, side).atk;
2446
2503
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
2504
  try log.boost(.{ ident, reason, n });
@@ -2461,7 +2518,7 @@ pub const Effects = struct {
2461
2518
  }
2462
2519
  return log.fail(.{ ident, .None });
2463
2520
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2521
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2522
  const stat = unmodifiedStats(battle, side).def;
2466
2523
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
2524
  try log.boost(.{ ident, .Defense, n });
@@ -2478,7 +2535,7 @@ pub const Effects = struct {
2478
2535
  }
2479
2536
  return log.fail(.{ ident, .None });
2480
2537
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2538
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2539
  const stat = unmodifiedStats(battle, side).spe;
2483
2540
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
2541
  try log.boost(.{ ident, .Speed, 2 });
@@ -2498,7 +2555,7 @@ pub const Effects = struct {
2498
2555
  }
2499
2556
  return log.fail(.{ ident, .None });
2500
2557
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2558
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2559
  const stat = unmodifiedStats(battle, side).spc;
2503
2560
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
2561
  try log.boost(.{ ident, .SpecialAttack, n });
@@ -2531,7 +2588,6 @@ pub const Effects = struct {
2531
2588
  } else {
2532
2589
  // checkHit already checks for Invulnerable
2533
2590
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2591
  }
2536
2592
 
2537
2593
  var stats = &foe.active.stats;
@@ -2551,7 +2607,7 @@ pub const Effects = struct {
2551
2607
  }
2552
2608
  return log.fail(.{ foe_ident, .None });
2553
2609
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2610
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2611
  const stat = unmodifiedStats(battle, foe).atk;
2556
2612
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
2613
  try log.boost(.{ foe_ident, .Attack, -1 });
@@ -2569,7 +2625,7 @@ pub const Effects = struct {
2569
2625
  }
2570
2626
  return log.fail(.{ foe_ident, .None });
2571
2627
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2628
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2629
  const stat = unmodifiedStats(battle, foe).def;
2574
2630
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
2631
  try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
@@ -2586,7 +2642,7 @@ pub const Effects = struct {
2586
2642
  }
2587
2643
  return log.fail(.{ foe_ident, .None });
2588
2644
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2645
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2646
  const stat = unmodifiedStats(battle, foe).spe;
2591
2647
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
2648
  try log.boost(.{ foe_ident, .Speed, -1 });
@@ -2606,7 +2662,7 @@ pub const Effects = struct {
2606
2662
  }
2607
2663
  return log.fail(.{ foe_ident, .None });
2608
2664
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2665
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2666
  const stat = unmodifiedStats(battle, foe).spc;
2611
2667
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
2668
  try log.boost(.{ foe_ident, .SpecialAttack, -1 });
@@ -2645,22 +2701,22 @@ fn isForced(active: anytype) bool {
2645
2701
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2702
  }
2647
2703
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2704
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2705
  var log = options.log;
2650
2706
  var side = battle.side(who);
2651
2707
  var volatiles = &side.active.volatiles;
2652
2708
  const ident = battle.active(who);
2653
2709
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2710
  if (volatiles.disable_move != 0) {
2657
2711
  volatiles.disable_move = 0;
2658
2712
  volatiles.disable_duration = 0;
2713
+ options.chance.observe(.disable, who, .None);
2659
2714
  try log.end(.{ ident, .DisableSilent });
2660
2715
  }
2661
2716
  if (volatiles.Confusion) {
2662
2717
  // volatiles.confusion is left unchanged
2663
2718
  volatiles.Confusion = false;
2719
+ options.chance.observe(.confusion, who, .None);
2664
2720
  try log.end(.{ ident, .ConfusionSilent });
2665
2721
  }
2666
2722
  if (volatiles.Mist) {
@@ -2695,10 +2751,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2695
2751
  }
2696
2752
  }
2697
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
+
2698
2767
  pub const Rolls = struct {
2699
2768
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2769
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2770
  player == .P1
2771
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2772
+ battle.rng.speedTie()
2702
2773
  else if (showdown)
2703
2774
  battle.rng.range(u8, 0, 2) == 0
2704
2775
  else
@@ -2708,21 +2779,31 @@ pub const Rolls = struct {
2708
2779
  return p1;
2709
2780
  }
2710
2781
 
2711
- 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 {
2712
2789
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2790
  val == .true
2791
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2792
+ battle.rng.criticalHit(player, rate)
2714
2793
  else if (showdown)
2715
2794
  battle.rng.chance(u8, rate, 256)
2716
2795
  else
2717
2796
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2797
 
2719
- try options.chance.criticalHit(player, crit, rate);
2798
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2799
  return crit;
2721
2800
  }
2722
2801
 
2723
2802
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2803
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2804
  val
2805
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2806
+ battle.rng.damage(player)
2726
2807
  else roll: {
2727
2808
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2809
  while (true) {
@@ -2736,21 +2817,27 @@ pub const Rolls = struct {
2736
2817
  return roll;
2737
2818
  }
2738
2819
 
2739
- 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;
2740
2823
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2824
  val == .true
2825
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2826
+ battle.rng.hit(player, acc)
2742
2827
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2828
+ battle.rng.chance(uN, acc, 256)
2744
2829
  else
2745
- battle.rng.next() < accuracy;
2830
+ battle.rng.next() < acc;
2746
2831
 
2747
- options.chance.hit(ok, accuracy);
2832
+ try options.chance.hit(player, ok, acc);
2748
2833
  return ok;
2749
2834
  }
2750
2835
 
2751
2836
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2837
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2838
  val == .true
2839
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2840
+ battle.rng.confused(player)
2754
2841
  else if (showdown)
2755
2842
  !battle.rng.chance(u8, 128, 256)
2756
2843
  else
@@ -2763,6 +2850,8 @@ pub const Rolls = struct {
2763
2850
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2851
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2852
  val == .true
2853
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2854
+ battle.rng.paralyzed(player)
2766
2855
  else if (showdown)
2767
2856
  battle.rng.chance(u8, 63, 256)
2768
2857
  else
@@ -2775,6 +2864,8 @@ pub const Rolls = struct {
2775
2864
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2865
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2866
  val == .true
2867
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2868
+ battle.rng.secondaryChance(player, 25)
2778
2869
  else if (showdown)
2779
2870
  battle.rng.chance(u8, 25, 256)
2780
2871
  else
@@ -2789,6 +2880,8 @@ pub const Rolls = struct {
2789
2880
 
2790
2881
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2882
  val == .true
2883
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2884
+ battle.rng.secondaryChance(player, rate)
2792
2885
  else if (showdown)
2793
2886
  battle.rng.chance(u8, rate, 256)
2794
2887
  else
@@ -2803,6 +2896,8 @@ pub const Rolls = struct {
2803
2896
 
2804
2897
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2898
  val == .true
2899
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2900
+ battle.rng.secondaryChance(player, rate)
2806
2901
  else if (showdown)
2807
2902
  battle.rng.chance(u8, rate, 256)
2808
2903
  else
@@ -2815,6 +2910,8 @@ pub const Rolls = struct {
2815
2910
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2911
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2912
  val == .true
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2914
+ battle.rng.secondaryChance(player, 85)
2818
2915
  else if (showdown)
2819
2916
  battle.rng.chance(u8, 85, 256)
2820
2917
  else
@@ -2827,6 +2924,8 @@ pub const Rolls = struct {
2827
2924
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2925
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2926
  val
2927
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2928
+ battle.rng.metronome(player)
2830
2929
  else if (showdown)
2831
2930
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2931
  else move: {
@@ -2845,6 +2944,8 @@ pub const Rolls = struct {
2845
2944
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2945
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2946
  val - 1
2947
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2948
+ battle.rng.psywave(player)
2848
2949
  else if (showdown)
2849
2950
  battle.rng.range(u8, 0, max)
2850
2951
  else power: {
@@ -2860,8 +2961,11 @@ pub const Rolls = struct {
2860
2961
  }
2861
2962
 
2862
2963
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
- @intCast(val)
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"))
2968
+ battle.rng.sleepDuration(player)
2865
2969
  else if (showdown)
2866
2970
  battle.rng.range(u3, 1, 8)
2867
2971
  else duration: {
@@ -2872,46 +2976,55 @@ pub const Rolls = struct {
2872
2976
  };
2873
2977
 
2874
2978
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2979
+ options.chance.duration(player, duration);
2876
2980
  return duration;
2877
2981
  }
2878
2982
 
2879
2983
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
- val
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"))
2988
+ battle.rng.disableDuration(player)
2882
2989
  else if (showdown)
2883
2990
  battle.rng.range(u4, 1, 9)
2884
2991
  else
2885
2992
  @intCast((battle.rng.next() & 7) + 1);
2886
2993
 
2887
2994
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
2995
+ options.chance.duration(player, duration);
2889
2996
  return duration;
2890
2997
  }
2891
2998
 
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)
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)
2895
3005
  else if (showdown)
2896
3006
  battle.rng.range(u3, 2, 6)
2897
3007
  else
2898
3008
  @intCast((battle.rng.next() & 3) + 2);
2899
3009
 
2900
3010
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3011
+ options.chance.duration(player, duration);
2902
3012
  return duration;
2903
3013
  }
2904
3014
 
2905
3015
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
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"))
3020
+ battle.rng.attackingDuration(player)
2908
3021
  else if (showdown)
2909
3022
  battle.rng.range(u3, 2, 4)
2910
3023
  else
2911
3024
  @intCast((battle.rng.next() & 1) + 2);
2912
3025
 
2913
3026
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3027
+ options.chance.duration(player, duration);
2915
3028
  return duration;
2916
3029
  }
2917
3030
 
@@ -2924,8 +3037,11 @@ pub const Rolls = struct {
2924
3037
  options: anytype,
2925
3038
  ) !u3 {
2926
3039
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
- @intCast(val)
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"))
3044
+ battle.rng.distribution(player)
2929
3045
  else if (showdown)
2930
3046
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3047
  else n: {
@@ -2935,7 +3051,7 @@ pub const Rolls = struct {
2935
3051
 
2936
3052
  assert(n >= 2 and n <= 5);
2937
3053
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3054
+ options.chance.duration(player, n);
2939
3055
  } else {
2940
3056
  try options.chance.multiHit(player, n);
2941
3057
  }
@@ -2950,15 +3066,15 @@ pub const Rolls = struct {
2950
3066
  options: anytype,
2951
3067
  ) !u4 {
2952
3068
  // TODO: consider throwing error instead of rerolling?
2953
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3069
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2954
3070
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2955
3071
  val
2956
3072
  else
2957
3073
  null
2958
3074
  else
2959
- null;
2960
-
2961
- const slot: u4 = overridden orelse slot: {
3075
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3076
+ battle.rng.moveSlot(player, moves, check_pp)
3077
+ else slot: {
2962
3078
  if (showdown) {
2963
3079
  if (check_pp == 0) {
2964
3080
  var i: usize = moves.len;
@@ -3038,11 +3154,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3154
  const side = battle.side(player);
3039
3155
  const foe = battle.foe(player);
3040
3156
 
3041
- var active = &side.active;
3157
+ const active = &side.active;
3042
3158
  const stored = side.stored();
3043
3159
 
3044
3160
  // 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.
3161
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3162
  //
3047
3163
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3164
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3166
  // passing due to being forced into using a move. Instead of introducing another option
3051
3167
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3168
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3169
+ // switch at all
3054
3170
  if (isForced(active)) {
3055
3171
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3172
  n += 1;
@@ -3069,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3185
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3186
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3187
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3188
+ // move normally if sleeping/frozen/bound
3073
3189
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3190
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3191
  {
@@ -3082,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3198
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3199
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3200
  // 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.
3201
+ // proc-ed move in their moveset) and disabling all other moves
3086
3202
  if (limited) {
3087
3203
  assert(showdown);
3088
3204
  assert(side.last_selected_move != .None);
@@ -3114,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3114
3230
  // Struggle (Pokémon Showdown would use 'move 1' here)
3115
3231
  if (n == before) {
3116
3232
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3117
- if (!showdown) {
3233
+ if (!pkmn.options.mod) {
3118
3234
  while (slot <= 4) : (slot += 1) {
3119
3235
  if (active.moves[slot - 1].pp != 0) return n;
3120
3236
  }