@pkmn/engine 0.1.0-dev.b05836cd → 0.1.0-dev.b3197aa3

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