@pkmn/engine 0.1.0-dev.12e7292c → 0.1.0-dev.130de754

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 (84) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +41 -25
  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 +26 -4
  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 +42 -91
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +29 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +84 -75
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +66 -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 +360 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +157 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +310 -203
  39. package/package.json +60 -134
  40. package/src/bin/install-pkmn-engine +70 -53
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +201 -0
  43. package/src/lib/{binding → bindings}/node.zig +15 -32
  44. package/src/lib/bindings/wasm.zig +51 -0
  45. package/src/lib/common/array.zig +70 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +46 -30
  48. package/src/lib/common/js.zig +5 -9
  49. package/src/lib/common/optional.zig +21 -22
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +253 -223
  52. package/src/lib/common/rational.zig +340 -254
  53. package/src/lib/common/rng.zig +11 -13
  54. package/src/lib/common/util.zig +53 -1
  55. package/src/lib/gen1/calc.zig +724 -175
  56. package/src/lib/gen1/chance.zig +494 -308
  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 +3 -4
  60. package/src/lib/gen1/data.zig +49 -49
  61. package/src/lib/gen1/helpers.zig +115 -562
  62. package/src/lib/gen1/mechanics.zig +411 -293
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +122 -518
  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 +7 -5
  69. package/src/lib/gen2/data.zig +39 -38
  70. package/src/lib/gen2/generated.zig +16 -26
  71. package/src/lib/gen2/helpers.zig +68 -39
  72. package/src/lib/gen2/mechanics.zig +217 -174
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +14 -0
  75. package/src/lib/pkmn.zig +123 -29
  76. package/src/lib/wasm.zig +12 -0
  77. package/src/tools/display/dom.ts +108 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -400
  80. package/build/test/display.js.map +0 -1
  81. package/src/lib/binding/wasm.zig +0 -12
  82. package/src/lib/gen1/README.md +0 -894
  83. package/src/lib/gen2/README.md +0 -329
  84. 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,13 +81,13 @@ 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
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
86
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
96
87
 
97
- var p2_slot = findFirstAlive(p2);
88
+ const p2_slot = findFirstAlive(p2);
98
89
  assert(!showdown or p2_slot == 1);
99
- if (p2_slot == 0) return Result.Win;
90
+ if (p2_slot == 0) return .Win;
100
91
 
101
92
  try switchIn(battle, .P1, p1_slot, true, options);
102
93
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -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 .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,19 +419,19 @@ 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
438
432
  // every Pokémon must have at least one move
439
433
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
440
- return Result.Error;
434
+ return .Error;
441
435
  } else {
442
436
  auto = true;
443
437
  }
@@ -455,7 +449,26 @@ fn executeMove(
455
449
  .skip_can => skip_can = true,
456
450
  .skip_pp => skip_pp = true,
457
451
  .ok => {},
458
- .err => return @as(?Result, Result.Error),
452
+ .err => return .Error,
453
+ }
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(mslot).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;
459
472
  }
460
473
 
461
474
  const can = skip_can or
@@ -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,19 +613,14 @@ 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
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
- // on the targeting behavior of the confused Pokémon's selected move which results
595
- // in the wrong behavior with respect to the Substitute + Confusion glitch
596
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
597
- player
598
- else
599
- player.foe();
600
620
 
601
621
  const uncapped = battle.last_damage;
602
622
  // Skipping adjustDamage / randomizeDamage / checkHit
603
- _ = try applyDamage(battle, player, target, .Confusion, options);
623
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
604
624
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
605
625
  if (showdown) battle.last_damage = uncapped;
606
626
 
@@ -617,7 +637,10 @@ fn beforeMove(
617
637
  volatiles.Thrashing = false;
618
638
  volatiles.Charging = false;
619
639
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
640
+
641
+ options.chance.observe(.attacking, player, .None);
642
+ options.chance.observe(.binding, player, .None);
643
+
621
644
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
645
  try log.cant(.{ ident, .Paralysis });
623
646
  return .done;
@@ -635,14 +658,8 @@ fn beforeMove(
635
658
  volatiles.state +%= battle.last_damage;
636
659
  }
637
660
 
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);
661
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
662
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
646
663
 
647
664
  if (volatiles.attacks != 0) {
648
665
  try log.activate(.{ ident, .Bide });
@@ -676,39 +693,40 @@ fn beforeMove(
676
693
  }
677
694
 
678
695
  if (volatiles.Thrashing) {
679
- assert(volatiles.attacks > 0);
696
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
680
697
 
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);
698
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
699
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
700
+
701
+ if (volatiles.attacks == 0) {
702
+ const overwritten = volatiles.Confusion;
703
+
704
+ volatiles.Thrashing = false;
705
+ volatiles.Confusion = true;
706
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
687
707
 
688
- if (!showdown and handleThrashing(battle, active, player, options)) {
708
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
709
+ // ended (the engine's protocol is always from the omniscient perspective and clients
710
+ // can be expected to recognize this pattern and pass the information only to the
711
+ // correct player)
689
712
  try log.start(.{ battle.active(player), .ConfusionSilent });
713
+ options.chance.observe(.confusion, player, if (overwritten)
714
+ if (pkmn.options.overwrite) .overwritten else .continuing
715
+ else
716
+ .started);
717
+ if (!overwritten) options.calc.confusion(player);
690
718
  }
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
- }
719
+
720
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
721
+ // used in useMove and doesn't have the notion of skipping canMove semantics
722
+ if (showdown) side.last_used_move = side.last_selected_move;
723
+
700
724
  return .skip_can;
701
725
  }
702
726
 
703
727
  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);
728
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
729
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
712
730
 
713
731
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
732
  if (showdown or battle.last_damage != 0) {
@@ -738,7 +756,6 @@ fn canMove(
738
756
  const player_ident = battle.active(player);
739
757
  const move = Move.get(side.last_selected_move);
740
758
 
741
- var skip = skip_pp;
742
759
  if (side.active.volatiles.Charging) {
743
760
  side.active.volatiles.Charging = false;
744
761
  side.active.volatiles.Invulnerable = false;
@@ -750,7 +767,7 @@ fn canMove(
750
767
  }
751
768
 
752
769
  side.last_used_move = side.last_selected_move;
753
- if (!skip) decrementPP(side, mslot, auto);
770
+ if (!skip_pp) decrementPP(side, mslot, auto);
754
771
 
755
772
  const target = if (move.target == .Self) player else player.foe();
756
773
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -763,9 +780,14 @@ fn canMove(
763
780
 
764
781
  if (move.effect == .Thrashing) {
765
782
  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);
783
+ } else if (move.effect == .Binding) {
784
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
785
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
786
+ if (showdown) {
787
+ battle.foe(player).active.volatiles.Recharging = false;
788
+ } else {
789
+ try Effects.binding(battle, player, false, options);
790
+ }
769
791
  }
770
792
 
771
793
  return true;
@@ -802,7 +824,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
802
824
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
803
825
  }
804
826
 
805
- fn incrementPP(side: *Side, mslot: u4) void {
827
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
806
828
  var active = &side.active;
807
829
  const volatiles = &active.volatiles;
808
830
 
@@ -810,8 +832,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
810
832
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
811
833
  if (showdown and volatiles.Transform) return;
812
834
 
835
+ const n = options.calc.overridden(player, .pp) orelse 1;
836
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
837
+
813
838
  assert(mslot > 0 and mslot <= 4);
814
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
839
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
815
840
  }
816
841
 
817
842
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,20 +877,20 @@ fn doMove(
852
877
  var miss = showdown and miss: {
853
878
  immune = move.target != .Self and !status and !counter and
854
879
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
855
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
880
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
856
881
  if (immune and move.effect != .Binding) break :miss true;
857
882
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
858
883
  battle.last_damage = 0;
859
884
  break :miss true;
860
885
  }
861
886
  break :miss (move.target != .Self and
862
- !moveHit(battle, player, move, &immune, &mist, options));
887
+ !try moveHit(battle, player, move, &immune, &mist, options));
863
888
  };
864
889
  assert(!immune or miss or (showdown and move.effect == .Binding));
865
890
 
866
891
  var late = showdown and move.effect != .Explode;
867
892
  const skip = status or immune;
868
- if ((!showdown or (!skip or counter)) and !miss) blk: {
893
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
869
894
  if (showdown and move.effect.isMulti()) {
870
895
  try Effects.multiHit(battle, player, move, options);
871
896
  hits = side.active.volatiles.attacks;
@@ -887,9 +912,10 @@ fn doMove(
887
912
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
888
913
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
889
914
  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)) {
892
- return @as(?Result, Result.Error);
915
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
916
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
917
+ if (!showdown) try options.chance.commit(player, .err);
918
+ return .Error;
893
919
  }
894
920
  if (battle.last_damage == 0) {
895
921
  immune = true;
@@ -902,11 +928,12 @@ fn doMove(
902
928
  }
903
929
  }
904
930
 
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);
931
+ var missed: bool = undefined;
932
+ const zero = battle.last_damage == 0;
933
+ if (!showdown and !skip) {
934
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
935
+ miss = missed or zero;
936
+ }
910
937
 
911
938
  assert(showdown or miss or battle.last_damage > 0 or skip);
912
939
  assert((!showdown and miss) or !(ohko and immune));
@@ -933,11 +960,32 @@ fn doMove(
933
960
  try log.immune(.{ foe_ident, .OHKO });
934
961
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
935
962
  try log.immune(.{ foe_ident, .None });
963
+ if (!showdown) {
964
+ if (move.effect == .Binding) {
965
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
966
+ // then simply clears it later on if the move happens to miss in moveHit.
967
+ // However, moveHit can return true and we'll still end up in the outer miss
968
+ // handling block because attacking into an immune Pokemon does zero damage
969
+ // (considered missing)
970
+ assert(zero);
971
+ // Thus we still need to actually save the hit result (but not the crit roll).
972
+ // Using .miss for this commit would accomplish what we want, but is somewhat
973
+ // misleading as its entirely possible the move actually did hit (and the target
974
+ // is now trapped) so we use the .binding value instead which is effectively
975
+ // identical, just doesn't claim to know about the actual results of the hit
976
+ // roll)
977
+ try options.chance.commit(player, .binding);
978
+ } else {
979
+ // The .glitch option allows for conditionally committing if we are in a
980
+ // division-by-zero freeze alternative timeline scenario
981
+ try options.chance.commit(player, .glitch);
982
+ }
983
+ }
936
984
  } else if (mist) {
937
985
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
938
986
  try log.fail(.{ foe_ident, .None });
939
987
  } else {
940
- if (showdown or !zero) try options.chance.commit(player, .miss);
988
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
941
989
  try log.lastmiss(.{});
942
990
  try log.miss(.{battle.active(player)});
943
991
  }
@@ -952,17 +1000,15 @@ fn doMove(
952
1000
  try buildRage(battle, player.foe(), options);
953
1001
  } else if (showdown and move.effect == .Disable) {
954
1002
  try buildRage(battle, player.foe(), options);
955
- } else if (!showdown and immune and move.effect == .Binding) {
956
- try options.chance.commit(player, .binding);
957
1003
  }
958
1004
  return null;
959
- } else {
1005
+ } else if (!showdown) {
960
1006
  try options.chance.commit(player, .hit);
961
1007
  }
962
1008
 
963
1009
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
964
1010
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
965
- // it up above before damage calculation).
1011
+ // it up above before damage calculation)
966
1012
  if (!showdown and move.effect.isMulti()) {
967
1013
  try Effects.multiHit(battle, player, move, options);
968
1014
  hits = side.active.volatiles.attacks;
@@ -972,7 +1018,7 @@ fn doMove(
972
1018
  const sub = showdown and foe.active.volatiles.Substitute;
973
1019
 
974
1020
  var nullified = false;
975
- var hit: u4 = 0;
1021
+ var hit: usize = 0;
976
1022
  while (hit < hits) {
977
1023
  if (hit == 0) {
978
1024
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1000,33 +1046,31 @@ fn doMove(
1000
1046
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1001
1047
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1002
1048
  }
1003
- try log.hitcount(.{ battle.active(player.foe()), hit });
1004
- } else if (showdown) {
1049
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1050
+ } else if (showdown and move.effect == .Binding) {
1005
1051
  // 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;
1052
+ try Effects.binding(battle, player, rewrap, options);
1053
+ if (immune) {
1054
+ battle.last_damage = 0;
1055
+ assert(foe.stored().hp > 0);
1056
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1057
+ if (sub) {
1058
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1059
+ } else {
1060
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1061
+ try buildRage(battle, player.foe(), options);
1019
1062
  }
1063
+ return null;
1020
1064
  }
1021
1065
  }
1022
1066
 
1023
1067
  // 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.
1068
+ // if an effect was intended to "always happen" it will still get skipped
1025
1069
  if (nullified) return null;
1026
1070
 
1027
1071
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1028
1072
  // 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.
1073
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1030
1074
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1031
1075
 
1032
1076
  if (foe.stored().hp == 0) return null;
@@ -1041,7 +1085,7 @@ fn doMove(
1041
1085
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1042
1086
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1043
1087
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1044
- // puts the |-status| message before |-hitcount| instead of after.
1088
+ // puts the |-status| message before |-hitcount| instead of after
1045
1089
  if (move.effect == .Twineedle) {
1046
1090
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1047
1091
  } else if (move.effect == .Disable) {
@@ -1060,12 +1104,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1060
1104
  const side = battle.side(player);
1061
1105
 
1062
1106
  // Base speed is used for the critical hit calculation, even when Transform-ed
1063
- var rate = @as(u16, Species.chance(side.stored().species));
1107
+ var rate: u16 = Species.chance(side.stored().species);
1064
1108
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1065
1109
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1066
1110
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1067
1111
 
1068
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1112
+ const spurious =
1113
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1114
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1069
1115
  }
1070
1116
 
1071
1117
  fn calcDamage(
@@ -1074,6 +1120,7 @@ fn calcDamage(
1074
1120
  target_player: Player,
1075
1121
  m: ?Move.Data,
1076
1122
  crit: bool,
1123
+ options: anytype,
1077
1124
  ) bool {
1078
1125
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1079
1126
  const cfz = m == null;
@@ -1085,28 +1132,8 @@ fn calcDamage(
1085
1132
 
1086
1133
  const special = move.type.special();
1087
1134
 
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
1135
+ var atk = attack(side, crit, special);
1136
+ var def = defense(target, crit, special, cfz);
1110
1137
 
1111
1138
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1112
1139
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1114,12 +1141,12 @@ fn calcDamage(
1114
1141
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1115
1142
  atk = @max((atk / 4) & 255, 1);
1116
1143
  // 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);
1144
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1118
1145
  }
1119
1146
 
1120
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1147
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1121
1148
 
1122
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1149
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1123
1150
 
1124
1151
  if (def == 0) return false;
1125
1152
 
@@ -1131,11 +1158,41 @@ fn calcDamage(
1131
1158
  d = @min(997, d);
1132
1159
  d += 2;
1133
1160
 
1161
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1162
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1163
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1164
+ // that results in counter intuitive probabilities in the common case
1165
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1166
+ atk = attack(side, !crit, special);
1167
+ def = defense(target, !crit, special, false);
1168
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1169
+ }
1170
+
1134
1171
  battle.last_damage = @intCast(d);
1135
1172
 
1136
1173
  return true;
1137
1174
  }
1138
1175
 
1176
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1177
+ return if (crit)
1178
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1179
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1180
+ }
1181
+
1182
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1183
+ return if (crit)
1184
+ if (special) target.stored().stats.spc else target.stored().stats.def
1185
+ else
1186
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1187
+ if (special)
1188
+ target.active.stats.spc *
1189
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1190
+ else
1191
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1192
+ target.active.stats.def *
1193
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1194
+ }
1195
+
1139
1196
  fn adjustDamage(battle: anytype, player: Player) u16 {
1140
1197
  const side = battle.side(player);
1141
1198
  const foe = battle.foe(player);
@@ -1180,7 +1237,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1180
1237
  const foe = battle.foe(player);
1181
1238
 
1182
1239
  if (!try checkHit(battle, player, move, options)) return null;
1183
- try options.chance.commit(player, .hit);
1184
1240
 
1185
1241
  battle.last_damage = switch (side.last_selected_move) {
1186
1242
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1191,13 +1247,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1191
1247
  .Psywave => power: {
1192
1248
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1193
1249
  // GLITCH: Psywave infinite glitch loop
1194
- if (!showdown and max <= 1) return Result.Error;
1250
+ if (!pkmn.options.mod and max <= 1) return .Error;
1195
1251
  break :power try Rolls.psywave(battle, player, max, options);
1196
1252
  },
1197
1253
  else => unreachable,
1198
1254
  };
1199
1255
 
1200
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1256
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
1201
1257
 
1202
1258
  const sub = showdown and foe.active.volatiles.Substitute;
1203
1259
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1217,15 +1273,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1217
1273
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1218
1274
  const foe_last_selected_move =
1219
1275
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1220
- .Splash
1221
- else
1222
- foe.last_selected_move);
1276
+ .Splash
1277
+ else
1278
+ foe.last_selected_move);
1223
1279
 
1224
1280
  const used = battle.lastMove(player.foe()).counterable != 0;
1225
1281
  const selected = foe_last_selected_move.bp > 0 and
1226
1282
  foe.last_selected_move != .Counter and
1227
1283
  (foe_last_selected_move.type == .Normal or
1228
- foe_last_selected_move.type == .Fighting);
1284
+ foe_last_selected_move.type == .Fighting);
1229
1285
 
1230
1286
  if (!used and !selected) {
1231
1287
  try options.log.fail(.{ battle.active(player), .None });
@@ -1234,16 +1290,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1290
 
1235
1291
  if (!used or !selected) {
1236
1292
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1237
- if (!showdown) return Result.Error;
1293
+ if (!pkmn.options.mod) return .Error;
1238
1294
  try options.log.fail(.{ battle.active(player), .None });
1239
1295
  return null;
1240
1296
  }
1241
1297
 
1242
1298
  battle.last_damage *|= 2;
1243
1299
 
1244
- // Pokémon Showdown calls checkHit before Counter
1300
+ // Pokémon Showdown calls moveHit before Counter
1245
1301
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1246
- try options.chance.commit(player, .hit);
1247
1302
 
1248
1303
  const sub = showdown and foe.active.volatiles.Substitute;
1249
1304
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1272,7 +1327,7 @@ fn applyDamage(
1272
1327
  subbed.active.volatiles.Substitute = false;
1273
1328
  // battle.last_damage is not updated with the amount of HP the Substitute had
1274
1329
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1275
- options.calc.capped(target_player);
1330
+ if (reason != .Confusion) options.calc.capped(sub_player);
1276
1331
  return true;
1277
1332
  } else {
1278
1333
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1306,7 +1361,7 @@ fn mirrorMove(
1306
1361
  try options.log.fail(.{ battle.active(player), .None });
1307
1362
  return null;
1308
1363
  } else if (!showdown or foe.last_used_move != .Struggle) {
1309
- incrementPP(side, mslot);
1364
+ incrementPP(side, player, mslot, options);
1310
1365
  }
1311
1366
 
1312
1367
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1327,7 +1382,7 @@ fn metronome(
1327
1382
  var side = battle.side(player);
1328
1383
 
1329
1384
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1330
- incrementPP(side, mslot);
1385
+ incrementPP(side, player, mslot, options);
1331
1386
 
1332
1387
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1333
1388
  return null;
@@ -1339,7 +1394,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1339
1394
  var immune = false;
1340
1395
  var mist = false;
1341
1396
 
1342
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1397
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1398
+ if (!showdown) try options.chance.commit(player, .hit);
1399
+ return true;
1400
+ }
1343
1401
 
1344
1402
  assert(!immune);
1345
1403
  if (mist) {
@@ -1348,11 +1406,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1348
1406
  try options.log.activate(.{ foe_ident, .Mist });
1349
1407
  try options.log.fail(.{ foe_ident, .None });
1350
1408
  } else {
1351
- try options.chance.commit(player, .miss);
1352
1409
  try options.log.lastmiss(.{});
1353
1410
  try options.log.miss(.{battle.active(player)});
1354
1411
  }
1355
1412
 
1413
+ if (!showdown) try options.chance.commit(player, .miss);
1356
1414
  return false;
1357
1415
  }
1358
1416
 
@@ -1363,7 +1421,7 @@ fn moveHit(
1363
1421
  immune: *bool,
1364
1422
  mist: *bool,
1365
1423
  options: anytype,
1366
- ) bool {
1424
+ ) !bool {
1367
1425
  var side = battle.side(player);
1368
1426
  const foe = battle.foe(player);
1369
1427
 
@@ -1383,7 +1441,16 @@ fn moveHit(
1383
1441
  break :miss true;
1384
1442
  }
1385
1443
  if (!showdown) {
1386
- if (move.effect == .Swift) return true;
1444
+ if (move.effect == .Swift) {
1445
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1446
+ // Showdown, hence why the block above doesn't include this code), we need the
1447
+ // damage roll to be committed. However, the commit code will not save this roll if
1448
+ // the glitch occurs and the move was not marked as having hit. To work around this
1449
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1450
+ // the hit as zero to ensure it doesn't actually get saved.
1451
+ try options.chance.hit(player, true, 0);
1452
+ return true;
1453
+ }
1387
1454
  if (foe.active.volatiles.Invulnerable) break :miss true;
1388
1455
  }
1389
1456
 
@@ -1402,7 +1469,7 @@ fn moveHit(
1402
1469
  const overwritten = overwrite and state > 0;
1403
1470
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1404
1471
 
1405
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1472
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1406
1473
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1407
1474
  accuracy = accuracy * boost[0] / boost[1];
1408
1475
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1414,7 +1481,7 @@ fn moveHit(
1414
1481
  if (overwrite) side.active.volatiles.state = accuracy;
1415
1482
 
1416
1483
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1417
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1484
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1418
1485
  };
1419
1486
 
1420
1487
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,13 +1492,17 @@ fn moveHit(
1425
1492
 
1426
1493
  if (!miss) return true;
1427
1494
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1495
+
1428
1496
  side.active.volatiles.Binding = false;
1497
+ options.chance.observe(.binding, player, .None);
1498
+
1429
1499
  return false;
1430
1500
  }
1431
1501
 
1432
1502
  fn checkFaint(
1433
1503
  battle: anytype,
1434
1504
  player: Player,
1505
+ cap: bool,
1435
1506
  options: anytype,
1436
1507
  ) @TypeOf(options.log).Error!?Result {
1437
1508
  const side = battle.side(player);
@@ -1444,21 +1515,21 @@ fn checkFaint(
1444
1515
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1445
1516
  const more = player_out or foe_out;
1446
1517
 
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;
1518
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1519
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1449
1520
 
1450
1521
  assert(!side.active.volatiles.MultiHit);
1451
1522
  assert(!foe.active.volatiles.MultiHit);
1452
1523
 
1453
1524
  if (player_out and foe_out) {
1454
1525
  try options.log.tie(.{});
1455
- return Result.Tie;
1526
+ return .Tie;
1456
1527
  } else if (player_out) {
1457
1528
  try options.log.win(.{player.foe()});
1458
- return if (player == .P1) Result.Lose else Result.Win;
1529
+ return if (player == .P1) .Lose else .Win;
1459
1530
  } else if (foe_out) {
1460
1531
  try options.log.win(.{player});
1461
- return if (player == .P1) Result.Win else Result.Lose;
1532
+ return if (player == .P1) .Win else .Lose;
1462
1533
  }
1463
1534
 
1464
1535
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1466,7 +1537,7 @@ fn checkFaint(
1466
1537
  return .{ .p1 = foe_choice, .p2 = .Switch };
1467
1538
  }
1468
1539
 
1469
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1540
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1470
1541
  var side = battle.side(player);
1471
1542
  var foe = battle.foe(player);
1472
1543
  assert(side.stored().hp == 0);
@@ -1475,8 +1546,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1475
1546
  assert(!foe_volatiles.MultiHit);
1476
1547
  if (foe_volatiles.Bide) {
1477
1548
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1478
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1479
- if (foe_volatiles.state != 0) return Result.Error;
1549
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1550
+ if (foe_volatiles.state != 0) return .Error;
1480
1551
  }
1481
1552
 
1482
1553
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1500,7 +1571,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1500
1571
  }
1501
1572
 
1502
1573
  try options.log.faint(.{ battle.active(player), done });
1503
- options.calc.capped(player);
1574
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1575
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1576
+ if (cap) options.calc.capped(player);
1504
1577
  return null;
1505
1578
  }
1506
1579
 
@@ -1564,21 +1637,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1637
 
1565
1638
  battle.turn += 1;
1566
1639
 
1567
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1640
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1568
1641
  try options.log.tie(.{});
1569
- return Result.Tie;
1642
+ return .Tie;
1570
1643
  }
1571
1644
 
1572
- if (showdown and battle.turn >= 1000) {
1645
+ if (pkmn.options.mod and battle.turn >= 1000) {
1573
1646
  try options.log.tie(.{});
1574
- return Result.Tie;
1647
+ return .Tie;
1575
1648
  } else if (battle.turn >= 65535) {
1576
- return Result.Error;
1649
+ return .Error;
1577
1650
  }
1578
1651
 
1579
1652
  try options.log.turn(.{battle.turn});
1580
1653
 
1581
- return Result.Default;
1654
+ return .Default;
1582
1655
  }
1583
1656
 
1584
1657
  fn checkEBC(battle: anytype) bool {
@@ -1660,17 +1733,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1660
1733
  }
1661
1734
  }
1662
1735
 
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
1736
  fn onBegin(
1675
1737
  battle: anytype,
1676
1738
  player: Player,
@@ -1679,7 +1741,6 @@ fn onBegin(
1679
1741
  residual: *bool,
1680
1742
  options: anytype,
1681
1743
  ) !void {
1682
- assert(move.effect.onBegin());
1683
1744
  return switch (move.effect) {
1684
1745
  .Confusion => Effects.confusion(battle, player, move, options),
1685
1746
  .Conversion => Effects.conversion(battle, player, options),
@@ -1702,7 +1763,6 @@ fn onBegin(
1702
1763
  }
1703
1764
 
1704
1765
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
- assert(move.effect.onEnd());
1706
1766
  return switch (move.effect) {
1707
1767
  .Bide => Effects.bide(battle, player, options),
1708
1768
  // zig fmt: off
@@ -1724,7 +1784,6 @@ fn alwaysHappens(
1724
1784
  residual: *bool,
1725
1785
  options: anytype,
1726
1786
  ) !void {
1727
- assert(move.effect.alwaysHappens());
1728
1787
  return switch (move.effect) {
1729
1788
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1730
1789
  .Explode => Effects.explode(battle, player),
@@ -1760,8 +1819,10 @@ pub const Effects = struct {
1760
1819
 
1761
1820
  side.active.volatiles.Bide = true;
1762
1821
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1822
+
1763
1823
  side.active.volatiles.state = 0;
1764
1824
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1825
+ options.chance.observe(.attacking, player, .started);
1765
1826
 
1766
1827
  try options.log.start(.{ battle.active(player), .Bide });
1767
1828
  }
@@ -1823,12 +1884,14 @@ pub const Effects = struct {
1823
1884
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1885
  if (!try checkHit(battle, player, move, options)) return;
1825
1886
  }
1826
- try options.chance.commit(player, .hit);
1827
1887
  }
1828
1888
 
1829
1889
  if (foe.active.volatiles.Confusion) return;
1830
1890
  foe.active.volatiles.Confusion = true;
1831
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1891
+
1892
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1893
+ options.chance.observe(.confusion, player.foe(), .started);
1894
+ options.calc.confusion(player.foe());
1832
1895
 
1833
1896
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1834
1897
  }
@@ -1856,10 +1919,7 @@ pub const Effects = struct {
1856
1919
  const foe_ident = battle.active(player.foe());
1857
1920
 
1858
1921
  // 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
- }
1922
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1863
1923
 
1864
1924
  if (volatiles.disable_move != 0) {
1865
1925
  try options.log.fail(.{ foe_ident, .None });
@@ -1877,18 +1937,18 @@ pub const Effects = struct {
1877
1937
 
1878
1938
  // Technically this is still considered simply a "miss" on the cartridge,
1879
1939
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1880
- if (n == 0) {
1940
+ if (n == 0 or (pkmn.options.mod and err)) {
1881
1941
  try options.log.fail(.{ foe_ident, .None });
1882
1942
  return null;
1883
1943
  } else if (err) {
1884
1944
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1885
- assert(!showdown);
1886
- return Result.Error;
1945
+ return .Error;
1887
1946
  }
1888
1947
 
1889
1948
  volatiles.disable_move =
1890
1949
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1891
1950
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1951
+ options.chance.observe(.disable, player.foe(), .started);
1892
1952
 
1893
1953
  const id = foe.active.move(volatiles.disable_move).id;
1894
1954
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1959,7 +2019,7 @@ pub const Effects = struct {
1959
2019
  if (foe.active.types.includes(move.type)) return;
1960
2020
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1961
2021
  // Freeze Clause Mod
1962
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2022
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1963
2023
 
1964
2024
  foe_stored.status = Status.init(.FRZ);
1965
2025
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1972,7 +2032,7 @@ pub const Effects = struct {
1972
2032
  var side = battle.side(player);
1973
2033
  var foe = battle.foe(player);
1974
2034
 
1975
- var side_stored = side.stored();
2035
+ const side_stored = side.stored();
1976
2036
  var foe_stored = foe.stored();
1977
2037
 
1978
2038
  const player_ident = battle.active(player);
@@ -1995,22 +2055,24 @@ pub const Effects = struct {
1995
2055
  if (p != player and Status.any(s.stored().status)) {
1996
2056
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1997
2057
  s.stored().status = 0;
2058
+ options.chance.observe(.sleep, p, .None);
1998
2059
  } else if (showdown and s.stored().status == Status.TOX) {
1999
2060
  s.stored().status = Status.init(.PSN);
2000
2061
  try log.status(.{ battle.active(p), s.stored().status, .None });
2001
2062
  }
2002
- try clearVolatiles(battle, p, p != player, options);
2063
+ try clearVolatiles(battle, p, options);
2003
2064
  }
2004
2065
  } else {
2005
2066
  if (Status.any(foe_stored.status)) {
2006
2067
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2007
2068
  foe.last_selected_move = .SKIP_TURN;
2069
+ options.chance.observe(.sleep, player.foe(), .None);
2008
2070
  }
2009
2071
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2010
2072
  foe_stored.status = 0;
2011
2073
  }
2012
- try clearVolatiles(battle, player, false, options);
2013
- try clearVolatiles(battle, player.foe(), true, options);
2074
+ try clearVolatiles(battle, player, options);
2075
+ try clearVolatiles(battle, player.foe(), options);
2014
2076
  }
2015
2077
  }
2016
2078
 
@@ -2061,7 +2123,6 @@ pub const Effects = struct {
2061
2123
  }
2062
2124
  }
2063
2125
 
2064
- try options.chance.commit(player, .hit);
2065
2126
  foe.active.volatiles.LeechSeed = true;
2066
2127
 
2067
2128
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2102,12 +2163,10 @@ pub const Effects = struct {
2102
2163
  if (!has_mimic) {
2103
2164
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2104
2165
  if (!try checkHit(battle, player, move, options)) return;
2105
- try options.chance.commit(player, .hit);
2106
2166
  return options.log.fail(.{ battle.active(player.foe()), .None });
2107
2167
  }
2108
2168
  }
2109
2169
  if (!try checkHit(battle, player, move, options)) return;
2110
- try options.chance.commit(player, .hit);
2111
2170
 
2112
2171
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2113
2172
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2161,7 +2220,6 @@ pub const Effects = struct {
2161
2220
  if (immune) return log.immune(.{ foe_ident, .None });
2162
2221
  if (!try checkHit(battle, player, move, options)) return;
2163
2222
  }
2164
- try options.chance.commit(player, .hit);
2165
2223
 
2166
2224
  foe_stored.status = Status.init(.PAR);
2167
2225
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2229,7 +2287,6 @@ pub const Effects = struct {
2229
2287
 
2230
2288
  if (move.effect == .Poison) {
2231
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2232
- try options.chance.commit(player, .hit);
2233
2290
  } else {
2234
2291
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2235
2292
  return;
@@ -2298,13 +2355,12 @@ pub const Effects = struct {
2298
2355
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
2356
  });
2300
2357
  }
2301
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2358
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2302
2359
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2303
- try options.chance.commit(player, .hit);
2304
2360
  }
2305
2361
 
2306
2362
  // Sleep Clause Mod
2307
- if (showdown) {
2363
+ if (pkmn.options.mod) {
2308
2364
  for (foe.pokemon) |p| {
2309
2365
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2310
2366
  }
@@ -2312,6 +2368,8 @@ pub const Effects = struct {
2312
2368
  foe.active.volatiles.Recharging = false;
2313
2369
 
2314
2370
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2371
+ options.chance.observe(.sleep, player.foe(), .started);
2372
+
2315
2373
  const last = battle.side(player).last_selected_move;
2316
2374
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2317
2375
  }
@@ -2353,9 +2411,7 @@ pub const Effects = struct {
2353
2411
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2412
  if (showdown) {
2355
2413
  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
- }
2414
+ _ = try checkHit(battle, player, move, options);
2359
2415
  battle.last_damage = 0;
2360
2416
  } else {
2361
2417
  try options.log.fail(.{ battle.active(player), .None });
@@ -2370,6 +2426,8 @@ pub const Effects = struct {
2370
2426
 
2371
2427
  volatiles.Thrashing = true;
2372
2428
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2429
+
2430
+ options.chance.observe(.attacking, player, .started);
2373
2431
  }
2374
2432
 
2375
2433
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2408,13 +2466,14 @@ pub const Effects = struct {
2408
2466
  if (side.active.volatiles.Binding) return;
2409
2467
  side.active.volatiles.Binding = true;
2410
2468
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2411
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2469
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2412
2470
  if (!showdown) {
2413
2471
  foe.active.volatiles.Recharging = false;
2414
2472
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2415
2473
 
2416
2474
  side.active.volatiles.attacks =
2417
2475
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2476
+ options.chance.observe(.binding, player, .started);
2418
2477
  }
2419
2478
 
2420
2479
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,7 +2500,7 @@ pub const Effects = struct {
2441
2500
  }
2442
2501
  return log.fail(.{ ident, .None });
2443
2502
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2503
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2504
  const stat = unmodifiedStats(battle, side).atk;
2446
2505
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
2506
  try log.boost(.{ ident, reason, n });
@@ -2461,7 +2520,7 @@ pub const Effects = struct {
2461
2520
  }
2462
2521
  return log.fail(.{ ident, .None });
2463
2522
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2523
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2524
  const stat = unmodifiedStats(battle, side).def;
2466
2525
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
2526
  try log.boost(.{ ident, .Defense, n });
@@ -2478,7 +2537,7 @@ pub const Effects = struct {
2478
2537
  }
2479
2538
  return log.fail(.{ ident, .None });
2480
2539
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2540
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2541
  const stat = unmodifiedStats(battle, side).spe;
2483
2542
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
2543
  try log.boost(.{ ident, .Speed, 2 });
@@ -2498,7 +2557,7 @@ pub const Effects = struct {
2498
2557
  }
2499
2558
  return log.fail(.{ ident, .None });
2500
2559
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2560
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2561
  const stat = unmodifiedStats(battle, side).spc;
2503
2562
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
2563
  try log.boost(.{ ident, .SpecialAttack, n });
@@ -2531,7 +2590,6 @@ pub const Effects = struct {
2531
2590
  } else {
2532
2591
  // checkHit already checks for Invulnerable
2533
2592
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2593
  }
2536
2594
 
2537
2595
  var stats = &foe.active.stats;
@@ -2551,7 +2609,7 @@ pub const Effects = struct {
2551
2609
  }
2552
2610
  return log.fail(.{ foe_ident, .None });
2553
2611
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2612
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2613
  const stat = unmodifiedStats(battle, foe).atk;
2556
2614
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
2615
  try log.boost(.{ foe_ident, .Attack, -1 });
@@ -2569,7 +2627,7 @@ pub const Effects = struct {
2569
2627
  }
2570
2628
  return log.fail(.{ foe_ident, .None });
2571
2629
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2630
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2631
  const stat = unmodifiedStats(battle, foe).def;
2574
2632
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
2633
  try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
@@ -2586,7 +2644,7 @@ pub const Effects = struct {
2586
2644
  }
2587
2645
  return log.fail(.{ foe_ident, .None });
2588
2646
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2647
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2648
  const stat = unmodifiedStats(battle, foe).spe;
2591
2649
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
2650
  try log.boost(.{ foe_ident, .Speed, -1 });
@@ -2606,7 +2664,7 @@ pub const Effects = struct {
2606
2664
  }
2607
2665
  return log.fail(.{ foe_ident, .None });
2608
2666
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2667
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2668
  const stat = unmodifiedStats(battle, foe).spc;
2611
2669
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
2670
  try log.boost(.{ foe_ident, .SpecialAttack, -1 });
@@ -2645,22 +2703,22 @@ fn isForced(active: anytype) bool {
2645
2703
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2704
  }
2647
2705
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2706
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2707
  var log = options.log;
2650
2708
  var side = battle.side(who);
2651
2709
  var volatiles = &side.active.volatiles;
2652
2710
  const ident = battle.active(who);
2653
2711
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2712
  if (volatiles.disable_move != 0) {
2657
2713
  volatiles.disable_move = 0;
2658
2714
  volatiles.disable_duration = 0;
2715
+ options.chance.observe(.disable, who, .None);
2659
2716
  try log.end(.{ ident, .DisableSilent });
2660
2717
  }
2661
2718
  if (volatiles.Confusion) {
2662
2719
  // volatiles.confusion is left unchanged
2663
2720
  volatiles.Confusion = false;
2721
+ options.chance.observe(.confusion, who, .None);
2664
2722
  try log.end(.{ ident, .ConfusionSilent });
2665
2723
  }
2666
2724
  if (volatiles.Mist) {
@@ -2695,10 +2753,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2695
2753
  }
2696
2754
  }
2697
2755
 
2756
+ fn decrement(
2757
+ comptime field: chance.Action.Field,
2758
+ player: Player,
2759
+ options: anytype,
2760
+ n: anytype,
2761
+ ) @TypeOf(n) {
2762
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2763
+ .started, .ended => 0,
2764
+ .continuing => if (n > 1) n - 1 else n,
2765
+ else => unreachable,
2766
+ } else n - 1;
2767
+ }
2768
+
2698
2769
  pub const Rolls = struct {
2699
2770
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2771
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2772
  player == .P1
2773
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2774
+ battle.rng.speedTie()
2702
2775
  else if (showdown)
2703
2776
  battle.rng.range(u8, 0, 2) == 0
2704
2777
  else
@@ -2708,21 +2781,31 @@ pub const Rolls = struct {
2708
2781
  return p1;
2709
2782
  }
2710
2783
 
2711
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2784
+ fn criticalHit(
2785
+ battle: anytype,
2786
+ player: Player,
2787
+ rate: u8,
2788
+ spurious: bool,
2789
+ options: anytype,
2790
+ ) !bool {
2712
2791
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2792
  val == .true
2793
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2794
+ battle.rng.criticalHit(player, rate)
2714
2795
  else if (showdown)
2715
2796
  battle.rng.chance(u8, rate, 256)
2716
2797
  else
2717
2798
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2799
 
2719
- try options.chance.criticalHit(player, crit, rate);
2800
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2801
  return crit;
2721
2802
  }
2722
2803
 
2723
2804
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2805
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2806
  val
2807
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2808
+ battle.rng.damage(player)
2726
2809
  else roll: {
2727
2810
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2811
  while (true) {
@@ -2736,21 +2819,27 @@ pub const Rolls = struct {
2736
2819
  return roll;
2737
2820
  }
2738
2821
 
2739
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2822
+ const uN = if (pkmn.options.miss) u8 else u9;
2823
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2824
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2740
2825
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2828
+ battle.rng.hit(player, acc)
2742
2829
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2830
+ battle.rng.chance(uN, acc, 256)
2744
2831
  else
2745
- battle.rng.next() < accuracy;
2832
+ battle.rng.next() < acc;
2746
2833
 
2747
- options.chance.hit(ok, accuracy);
2834
+ try options.chance.hit(player, ok, acc);
2748
2835
  return ok;
2749
2836
  }
2750
2837
 
2751
2838
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2839
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2840
  val == .true
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2842
+ battle.rng.confused(player)
2754
2843
  else if (showdown)
2755
2844
  !battle.rng.chance(u8, 128, 256)
2756
2845
  else
@@ -2763,6 +2852,8 @@ pub const Rolls = struct {
2763
2852
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2853
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2854
  val == .true
2855
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2856
+ battle.rng.paralyzed(player)
2766
2857
  else if (showdown)
2767
2858
  battle.rng.chance(u8, 63, 256)
2768
2859
  else
@@ -2775,6 +2866,8 @@ pub const Rolls = struct {
2775
2866
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2867
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2868
  val == .true
2869
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2870
+ battle.rng.secondaryChance(player, 25)
2778
2871
  else if (showdown)
2779
2872
  battle.rng.chance(u8, 25, 256)
2780
2873
  else
@@ -2789,6 +2882,8 @@ pub const Rolls = struct {
2789
2882
 
2790
2883
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2884
  val == .true
2885
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2886
+ battle.rng.secondaryChance(player, rate)
2792
2887
  else if (showdown)
2793
2888
  battle.rng.chance(u8, rate, 256)
2794
2889
  else
@@ -2803,6 +2898,8 @@ pub const Rolls = struct {
2803
2898
 
2804
2899
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2900
  val == .true
2901
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2902
+ battle.rng.secondaryChance(player, rate)
2806
2903
  else if (showdown)
2807
2904
  battle.rng.chance(u8, rate, 256)
2808
2905
  else
@@ -2815,6 +2912,8 @@ pub const Rolls = struct {
2815
2912
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2913
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2914
  val == .true
2915
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2916
+ battle.rng.secondaryChance(player, 85)
2818
2917
  else if (showdown)
2819
2918
  battle.rng.chance(u8, 85, 256)
2820
2919
  else
@@ -2827,6 +2926,8 @@ pub const Rolls = struct {
2827
2926
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2927
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2928
  val
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2930
+ battle.rng.metronome(player)
2830
2931
  else if (showdown)
2831
2932
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2933
  else move: {
@@ -2845,6 +2946,8 @@ pub const Rolls = struct {
2845
2946
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2947
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2948
  val - 1
2949
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2950
+ battle.rng.psywave(player)
2848
2951
  else if (showdown)
2849
2952
  battle.rng.range(u8, 0, max)
2850
2953
  else power: {
@@ -2860,8 +2963,11 @@ pub const Rolls = struct {
2860
2963
  }
2861
2964
 
2862
2965
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
- @intCast(val)
2966
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2967
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2968
+ else
2969
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2970
+ battle.rng.sleepDuration(player)
2865
2971
  else if (showdown)
2866
2972
  battle.rng.range(u3, 1, 8)
2867
2973
  else duration: {
@@ -2872,46 +2978,55 @@ pub const Rolls = struct {
2872
2978
  };
2873
2979
 
2874
2980
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2981
+ options.chance.duration(player, duration);
2876
2982
  return duration;
2877
2983
  }
2878
2984
 
2879
2985
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
- val
2986
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2987
+ if (val >= 1 and val <= 8) val else null
2988
+ else
2989
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2990
+ battle.rng.disableDuration(player)
2882
2991
  else if (showdown)
2883
2992
  battle.rng.range(u4, 1, 9)
2884
2993
  else
2885
2994
  @intCast((battle.rng.next() & 7) + 1);
2886
2995
 
2887
2996
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
2997
+ options.chance.duration(player, duration);
2889
2998
  return duration;
2890
2999
  }
2891
3000
 
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)
3001
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3002
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3003
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3004
+ else
3005
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3006
+ battle.rng.confusionDuration(player)
2895
3007
  else if (showdown)
2896
3008
  battle.rng.range(u3, 2, 6)
2897
3009
  else
2898
3010
  @intCast((battle.rng.next() & 3) + 2);
2899
3011
 
2900
3012
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3013
+ options.chance.duration(player, duration);
2902
3014
  return duration;
2903
3015
  }
2904
3016
 
2905
3017
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
3018
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3019
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3020
+ else
3021
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3022
+ battle.rng.attackingDuration(player)
2908
3023
  else if (showdown)
2909
3024
  battle.rng.range(u3, 2, 4)
2910
3025
  else
2911
3026
  @intCast((battle.rng.next() & 1) + 2);
2912
3027
 
2913
3028
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3029
+ options.chance.duration(player, duration);
2915
3030
  return duration;
2916
3031
  }
2917
3032
 
@@ -2924,8 +3039,11 @@ pub const Rolls = struct {
2924
3039
  options: anytype,
2925
3040
  ) !u3 {
2926
3041
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
- @intCast(val)
3042
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3043
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3044
+ else
3045
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3046
+ battle.rng.distribution(player)
2929
3047
  else if (showdown)
2930
3048
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3049
  else n: {
@@ -2935,7 +3053,7 @@ pub const Rolls = struct {
2935
3053
 
2936
3054
  assert(n >= 2 and n <= 5);
2937
3055
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3056
+ options.chance.duration(player, n);
2939
3057
  } else {
2940
3058
  try options.chance.multiHit(player, n);
2941
3059
  }
@@ -2950,15 +3068,15 @@ pub const Rolls = struct {
2950
3068
  options: anytype,
2951
3069
  ) !u4 {
2952
3070
  // TODO: consider throwing error instead of rerolling?
2953
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3071
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2954
3072
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2955
3073
  val
2956
3074
  else
2957
3075
  null
2958
3076
  else
2959
- null;
2960
-
2961
- const slot: u4 = overridden orelse slot: {
3077
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3078
+ battle.rng.moveSlot(player, moves, check_pp)
3079
+ else slot: {
2962
3080
  if (showdown) {
2963
3081
  if (check_pp == 0) {
2964
3082
  var i: usize = moves.len;
@@ -3022,11 +3140,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3022
3140
  },
3023
3141
  .Switch => {
3024
3142
  const side = battle.side(player);
3025
- var slot: u4 = 2;
3143
+ var slot: usize = 2;
3026
3144
  while (slot <= 6) : (slot += 1) {
3027
3145
  const id = side.order[slot - 1];
3028
3146
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3029
- out[n] = .{ .type = .Switch, .data = slot };
3147
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3030
3148
  n += 1;
3031
3149
  }
3032
3150
  if (n == 0) {
@@ -3038,11 +3156,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3156
  const side = battle.side(player);
3039
3157
  const foe = battle.foe(player);
3040
3158
 
3041
- var active = &side.active;
3159
+ const active = &side.active;
3042
3160
  const stored = side.stored();
3043
3161
 
3044
3162
  // 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.
3163
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3164
  //
3047
3165
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3166
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,18 +3168,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3168
  // passing due to being forced into using a move. Instead of introducing another option
3051
3169
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3170
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3171
+ // switch at all
3054
3172
  if (isForced(active)) {
3055
3173
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3174
  n += 1;
3057
3175
  return n;
3058
3176
  }
3059
3177
 
3060
- var slot: u4 = 2;
3178
+ var slot: usize = 2;
3061
3179
  while (slot <= 6) : (slot += 1) {
3062
3180
  const id = side.order[slot - 1];
3063
3181
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3064
- out[n] = .{ .type = .Switch, .data = slot };
3182
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3065
3183
  n += 1;
3066
3184
  }
3067
3185
 
@@ -3069,7 +3187,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3187
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3188
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3189
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3190
+ // move normally if sleeping/frozen/bound
3073
3191
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3192
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3193
  {
@@ -3082,7 +3200,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3200
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3201
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3202
  // 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.
3203
+ // proc-ed move in their moveset) and disabling all other moves
3086
3204
  if (limited) {
3087
3205
  assert(showdown);
3088
3206
  assert(side.last_selected_move != .None);
@@ -3094,7 +3212,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3094
3212
  const struggle =
3095
3213
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3096
3214
  const s = if (struggle) 0 else slot;
3097
- out[n] = .{ .type = .Move, .data = s };
3215
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3098
3216
  n += 1;
3099
3217
  return n;
3100
3218
  }
@@ -3108,13 +3226,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3226
  if (m.id == .None) break;
3109
3227
  if (m.pp == 0) continue;
3110
3228
  if (active.volatiles.disable_move == slot) continue;
3111
- out[n] = .{ .type = .Move, .data = slot };
3229
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3112
3230
  n += 1;
3113
3231
  }
3114
3232
  // Struggle (Pokémon Showdown would use 'move 1' here)
3115
3233
  if (n == before) {
3116
3234
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3117
- if (!showdown) {
3235
+ if (!pkmn.options.mod) {
3118
3236
  while (slot <= 4) : (slot += 1) {
3119
3237
  if (active.moves[slot - 1].pp != 0) return n;
3120
3238
  }