@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.3393edb3

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 (67) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +41 -14
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +43 -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 +96 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon.d.ts +2 -0
  10. package/build/pkg/addon.js +35 -58
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data.d.ts +27 -5
  13. package/build/pkg/data.js.map +1 -1
  14. package/build/pkg/gen1.js +1 -1
  15. package/build/pkg/gen1.js.map +1 -1
  16. package/build/pkg/index.d.ts +24 -10
  17. package/build/pkg/index.js +15 -10
  18. package/build/pkg/index.js.map +1 -1
  19. package/build/pkg/protocol.d.ts +20 -1
  20. package/build/pkg/protocol.js +109 -107
  21. package/build/pkg/protocol.js.map +1 -1
  22. package/build/tools/debug.js +36 -70
  23. package/build/tools/debug.js.map +1 -1
  24. package/build/tools/display/debug.jsx +141 -0
  25. package/build/tools/display/debug.jsx.map +1 -0
  26. package/build/tools/display/index.js +132 -0
  27. package/build/tools/display/index.js.map +1 -0
  28. package/build/tools/display/ui.jsx +349 -0
  29. package/build/tools/display/ui.jsx.map +1 -0
  30. package/build/tools/display/util.js +18 -0
  31. package/build/tools/display/util.js.map +1 -0
  32. package/build.zig +206 -81
  33. package/package.json +54 -29
  34. package/src/bin/install-pkmn-engine +29 -19
  35. package/src/bin/pkmn-debug +1 -1
  36. package/src/lib/common/array.zig +77 -0
  37. package/src/lib/common/data.zig +4 -4
  38. package/src/lib/common/debug.zig +32 -6
  39. package/src/lib/common/js.zig +4 -2
  40. package/src/lib/common/optional.zig +14 -10
  41. package/src/lib/common/options.zig +4 -0
  42. package/src/lib/common/protocol.zig +222 -210
  43. package/src/lib/common/rational.zig +121 -27
  44. package/src/lib/common/rng.zig +7 -7
  45. package/src/lib/common/util.zig +50 -1
  46. package/src/lib/gen1/calc.zig +666 -133
  47. package/src/lib/gen1/chance.zig +375 -266
  48. package/src/lib/gen1/data/species.zig +1 -1
  49. package/src/lib/gen1/data.zig +17 -16
  50. package/src/lib/gen1/helpers.zig +89 -538
  51. package/src/lib/gen1/mechanics.zig +461 -372
  52. package/src/lib/gen2/calc.zig +34 -16
  53. package/src/lib/gen2/chance.zig +84 -496
  54. package/src/lib/gen2/data.zig +8 -7
  55. package/src/lib/gen2/generated.zig +1 -3
  56. package/src/lib/gen2/helpers.zig +41 -13
  57. package/src/lib/gen2/mechanics.zig +328 -300
  58. package/src/lib/{binding/node.zig → node.zig} +9 -12
  59. package/src/lib/pkmn.zig +32 -15
  60. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  61. package/src/tools/display/dom.ts +96 -0
  62. package/src/tools/display/ui.css +351 -0
  63. package/build/test/display.js +0 -400
  64. package/build/test/display.js.map +0 -1
  65. package/src/lib/gen1/README.md +0 -894
  66. package/src/lib/gen2/README.md +0 -329
  67. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
10
  const data = @import("data.zig");
11
+ const chance = @import("chance.zig");
11
12
 
12
13
  const assert = std.debug.assert;
13
14
 
@@ -21,21 +22,15 @@ const Player = common.Player;
21
22
  const Result = common.Result;
22
23
 
23
24
  const Boost = protocol.Boost;
24
- const Cant = protocol.Cant;
25
- const CureStatus = protocol.CureStatus;
26
25
  const Damage = protocol.Damage;
27
- const End = protocol.End;
28
26
  const Fail = protocol.Fail;
29
27
  const Heal = protocol.Heal;
30
- const Immune = protocol.Immune;
31
28
 
32
29
  const Gen12 = rng.Gen12;
33
30
 
34
- const ActivePokemon = data.ActivePokemon;
35
31
  const Effectiveness = data.Effectiveness;
36
32
  const Move = data.Move;
37
33
  const MoveSlot = data.MoveSlot;
38
- const Pokemon = data.Pokemon;
39
34
  const Side = data.Side;
40
35
  const Species = data.Species;
41
36
  const Stats = data.Stats;
@@ -75,8 +70,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
75
70
  var p1 = battle.side(.P1);
76
71
  var p2 = battle.side(.P2);
77
72
 
78
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
79
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
73
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
74
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
80
75
 
81
76
  if (try turnOrder(battle, c1, c2, options) == .P1) {
82
77
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -94,11 +89,11 @@ fn start(battle: anytype, options: anytype) !Result {
94
89
  const p1 = battle.side(.P1);
95
90
  const p2 = battle.side(.P2);
96
91
 
97
- var p1_slot = findFirstAlive(p1);
92
+ const p1_slot = findFirstAlive(p1);
98
93
  assert(!showdown or p1_slot == 1);
99
94
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
100
95
 
101
- var p2_slot = findFirstAlive(p2);
96
+ const p2_slot = findFirstAlive(p2);
102
97
  assert(!showdown or p2_slot == 1);
103
98
  if (p2_slot == 0) return Result.Win;
104
99
 
@@ -170,7 +165,7 @@ fn selectMove(
170
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
171
166
  // set the selected move will not actually be used, it will just be reported as having
172
167
  // been used (this differs from how Pokémon Showdown works, but its impossible to
173
- // replicate the incorrect behavior with the correct mechanisms).
168
+ // replicate the incorrect behavior with the correct mechanisms)
174
169
  saveMove(battle, player, choice);
175
170
  } else {
176
171
  assert(choice.data == 0);
@@ -257,17 +252,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
257
252
  active.moves = incoming.moves;
258
253
 
259
254
  statusModify(incoming.status, &active.stats);
255
+ options.chance.switched(player, slot);
260
256
 
261
257
  foe.active.volatiles.Binding = false;
262
258
 
263
259
  try options.log.switched(.{ battle.active(player), incoming });
264
- options.chance.switched(player, side.order[0], out);
265
260
 
266
261
  if (showdown and incoming.status == Status.TOX) {
267
262
  incoming.status = Status.init(.PSN);
268
263
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
269
264
  // rather not clutter up turnOrder just for this (incorrect) log message
270
- try options.log.status(.{ battle.active(player), incoming.status, protocol.Status.Silent });
265
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
271
266
  }
272
267
  }
273
268
 
@@ -346,18 +341,19 @@ fn doTurn(
346
341
  player_choice,
347
342
  player_rewrap,
348
343
  player_skip,
344
+ true,
349
345
  &residual,
350
346
  options,
351
347
  )) |r| return r;
352
348
  if (!replace) {
353
349
  if (player_choice.type != .Switch) {
354
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
355
351
  }
356
352
  if (residual) try handleResidual(battle, player, options);
357
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
358
354
  } else if (foe_choice.type == .Pass) return null;
359
355
 
360
- options.chance.clearPending();
356
+ if (!showdown) options.chance.clearPending();
361
357
 
362
358
  residual = true;
363
359
  replace = battle.side(foe_player).stored().hp == 0;
@@ -368,15 +364,16 @@ fn doTurn(
368
364
  foe_choice,
369
365
  foe_rewrap,
370
366
  foe_skip,
367
+ false,
371
368
  &residual,
372
369
  options,
373
370
  )) |r| return r;
374
371
  if (!replace) {
375
372
  if (!calc and foe_choice.type != .Switch) {
376
- if (try checkFaint(battle, player, options)) |r| return r;
373
+ if (try checkFaint(battle, player, true, options)) |r| return r;
377
374
  }
378
375
  if (residual) try handleResidual(battle, foe_player, options);
379
- if (try checkFaint(battle, foe_player, options)) |r| return r;
376
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
380
377
  }
381
378
 
382
379
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -395,6 +392,7 @@ fn executeMove(
395
392
  choice: Choice,
396
393
  rewrap: bool,
397
394
  skip: bool,
395
+ first: bool,
398
396
  residual: *bool,
399
397
  options: anytype,
400
398
  ) !?Result {
@@ -411,7 +409,7 @@ fn executeMove(
411
409
  if (side.last_selected_move == .SKIP_TURN) {
412
410
  assert(!showdown);
413
411
  if (battle.foe(player).active.volatiles.Binding) {
414
- try options.log.cant(.{ battle.active(player), Cant.Bound });
412
+ try options.log.cant(.{ battle.active(player), .Bound });
415
413
  }
416
414
  return null;
417
415
  }
@@ -435,7 +433,7 @@ fn executeMove(
435
433
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
436
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
437
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
438
- // index 1 and the current Pokémon has the same move in its first slot.
436
+ // index 1 and the current Pokémon has the same move in its first slot
439
437
  if (!auto) {
440
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
441
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -454,7 +452,7 @@ fn executeMove(
454
452
 
455
453
  var skip_can = false;
456
454
  var skip_pp = false;
457
- switch (try beforeMove(battle, player, skip, residual, options)) {
455
+ switch (try beforeMove(battle, player, skip, first, residual, options)) {
458
456
  .done => return null,
459
457
  .skip_can => skip_can = true,
460
458
  .skip_pp => skip_pp = true,
@@ -475,6 +473,7 @@ fn beforeMove(
475
473
  battle: anytype,
476
474
  player: Player,
477
475
  skip: bool,
476
+ first: bool,
478
477
  residual: *bool,
479
478
  options: anytype,
480
479
  ) !BeforeMove {
@@ -488,36 +487,41 @@ fn beforeMove(
488
487
 
489
488
  if (Status.is(stored.status, .SLP)) {
490
489
  const before = stored.status;
490
+ const slf = Status.is(stored.status, .EXT);
491
491
  // Even if the EXT bit is set this will still correctly modify the sleep duration
492
- if (options.calc.modify(player, .sleep)) |extend| {
493
- if (!extend) stored.status = 0;
492
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
493
+ .started, .ended => stored.status = 0,
494
+ .continuing => if (Status.duration(stored.status) > 1) {
495
+ stored.status -= 1;
496
+ },
497
+ else => unreachable,
494
498
  } else {
495
499
  stored.status -= 1;
496
500
  }
497
501
 
498
502
  const duration = Status.duration(stored.status);
499
- if (!Status.is(stored.status, .EXT)) {
500
- try options.chance.sleep(player, duration);
501
- }
503
+ try options.chance.sleep(player, if (slf)
504
+ .None
505
+ else if (duration == 0) .ended else .continuing);
502
506
 
503
507
  if (duration == 0) {
504
- try log.curestatus(.{ ident, before, CureStatus.Message });
508
+ try log.curestatus(.{ ident, before, .Message });
505
509
  stored.status = 0; // clears EXT if present
506
510
  } else {
507
- try log.cant(.{ ident, Cant.Sleep });
511
+ try log.cant(.{ ident, .Sleep });
508
512
  }
509
513
  side.last_used_move = .None;
510
514
  return .done;
511
515
  }
512
516
 
513
517
  if (Status.is(stored.status, .FRZ)) {
514
- try log.cant(.{ ident, Cant.Freeze });
518
+ try log.cant(.{ ident, .Freeze });
515
519
  side.last_used_move = .None;
516
520
  return .done;
517
521
  }
518
522
 
519
523
  if (skip or foe.active.volatiles.Binding) {
520
- try log.cant(.{ ident, Cant.Bound });
524
+ try log.cant(.{ ident, .Bound });
521
525
  return .done;
522
526
  }
523
527
 
@@ -525,27 +529,27 @@ fn beforeMove(
525
529
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
526
530
  // Pokémon can sometimes flinch multiple times from the same original hit
527
531
  if (!showdown) volatiles.Flinch = false;
528
- try log.cant(.{ ident, Cant.Flinch });
532
+ try log.cant(.{ ident, .Flinch });
529
533
  return .done;
530
534
  }
531
535
 
532
536
  if (volatiles.Recharging) {
533
537
  volatiles.Recharging = false;
534
- try log.cant(.{ ident, Cant.Recharge });
538
+ try log.cant(.{ ident, .Recharge });
535
539
  return .done;
536
540
  }
537
541
 
538
542
  if (volatiles.disable_duration > 0) {
539
- if (options.calc.modify(player, .disable)) |extend| {
540
- if (!extend) volatiles.disable_duration = 0;
541
- } else {
542
- volatiles.disable_duration -= 1;
543
- }
544
- try options.chance.disable(player, volatiles.disable_duration);
543
+ volatiles.disable_duration =
544
+ decrement(.disable, player, options, volatiles.disable_duration);
545
+ try options.chance.disable(
546
+ player,
547
+ if (volatiles.disable_duration == 0) .ended else .continuing,
548
+ );
545
549
 
546
550
  if (volatiles.disable_duration == 0) {
547
551
  volatiles.disable_move = 0;
548
- try log.end(.{ ident, End.Disable });
552
+ try log.end(.{ ident, .Disable });
549
553
  }
550
554
  }
551
555
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -560,19 +564,31 @@ fn beforeMove(
560
564
 
561
565
  if (volatiles.Confusion) {
562
566
  assert(volatiles.confusion > 0);
563
-
564
- if (options.calc.modify(player, .confusion)) |extend| {
565
- if (!extend) volatiles.confusion = 0;
567
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
568
+ .started => {
569
+ if (!first or (options.calc.overridden(player, .confused) orelse .None) != .None) {
570
+ volatiles.confusion -= 1;
571
+ } else {
572
+ volatiles.confusion = 0;
573
+ }
574
+ },
575
+ .continuing => if (volatiles.confusion > 1) {
576
+ volatiles.confusion -= 1;
577
+ },
578
+ .ended => volatiles.confusion = 0,
579
+ .overwritten => {},
580
+ else => unreachable,
566
581
  } else {
567
582
  volatiles.confusion -= 1;
568
583
  }
569
- try options.chance.confusion(player, volatiles.confusion);
584
+
585
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
570
586
 
571
587
  if (volatiles.confusion == 0) {
572
588
  volatiles.Confusion = false;
573
- try log.end(.{ ident, End.Confusion });
589
+ try log.end(.{ ident, .Confusion });
574
590
  } else {
575
- try log.activate(ident, .Confusion);
591
+ try log.activate(.{ ident, .Confusion });
576
592
 
577
593
  if (try Rolls.confused(battle, player, options)) {
578
594
  assert(!volatiles.MultiHit);
@@ -584,7 +600,9 @@ fn beforeMove(
584
600
  volatiles.Charging = false;
585
601
  volatiles.Binding = false;
586
602
  volatiles.Invulnerable = false;
587
- options.chance.clearDurations(player, null);
603
+
604
+ options.chance.observe(.attacking, player, .None);
605
+ options.chance.observe(.binding, player, .None);
588
606
  {
589
607
  // This feels (and is) disgusting but the cartridge literally just overwrites
590
608
  // the opponent's defense with the user's defense and resets it after. As a
@@ -592,7 +610,9 @@ fn beforeMove(
592
610
  const def = foe.active.stats.def;
593
611
  foe.active.stats.def = active.stats.def;
594
612
  defer foe.active.stats.def = def;
595
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
613
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
614
+ return .err;
615
+ }
596
616
  }
597
617
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
598
618
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -621,9 +641,12 @@ fn beforeMove(
621
641
  volatiles.Thrashing = false;
622
642
  volatiles.Charging = false;
623
643
  volatiles.Binding = false;
624
- options.chance.clearDurations(player, null);
644
+
645
+ options.chance.observe(.attacking, player, .None);
646
+ options.chance.observe(.binding, player, .None);
647
+
625
648
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
626
- try log.cant(.{ ident, Cant.Paralysis });
649
+ try log.cant(.{ ident, .Paralysis });
627
650
  return .done;
628
651
  }
629
652
 
@@ -639,28 +662,22 @@ fn beforeMove(
639
662
  volatiles.state +%= battle.last_damage;
640
663
  }
641
664
 
642
- assert(volatiles.attacks > 0);
643
-
644
- if (options.calc.modify(player, .attacking)) |extend| {
645
- if (!extend) volatiles.attacks = 0;
646
- } else {
647
- volatiles.attacks -= 1;
648
- }
649
- try options.chance.attacking(player, volatiles.attacks);
665
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
666
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
650
667
 
651
668
  if (volatiles.attacks != 0) {
652
- try log.activate(ident, .Bide);
669
+ try log.activate(.{ ident, .Bide });
653
670
  return .done;
654
671
  }
655
672
 
656
673
  volatiles.Bide = false;
657
- try log.end(.{ ident, End.Bide });
674
+ try log.end(.{ ident, .Bide });
658
675
 
659
676
  battle.last_damage = volatiles.state *% 2;
660
677
  volatiles.state = 0;
661
678
 
662
679
  if (battle.last_damage == 0) {
663
- try log.fail(.{ ident, Fail.None });
680
+ try log.fail(.{ ident, .None });
664
681
  return .done;
665
682
  }
666
683
 
@@ -680,39 +697,36 @@ fn beforeMove(
680
697
  }
681
698
 
682
699
  if (volatiles.Thrashing) {
683
- assert(volatiles.attacks > 0);
700
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
684
701
 
685
- if (options.calc.modify(player, .attacking)) |extend| {
686
- if (!extend) volatiles.attacks = 0;
687
- } else {
688
- volatiles.attacks -= 1;
689
- }
690
- try options.chance.attacking(player, volatiles.attacks);
702
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
703
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
691
704
 
692
- if (!showdown and handleThrashing(battle, active, player, options)) {
693
- try log.start(battle.active(player), .ConfusionSilent);
694
- }
695
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
696
- if (showdown) {
697
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
698
- // used in useMove and doesn't have the notion of skipping canMove semantics
699
- side.last_used_move = side.last_selected_move;
700
- if (handleThrashing(battle, active, player, options)) {
701
- try log.start(battle.active(player), .ConfusionSilent);
702
- }
705
+ if (volatiles.attacks == 0) {
706
+ const overwritten = pkmn.options.overwrite and volatiles.Confusion;
707
+
708
+ volatiles.Thrashing = false;
709
+ volatiles.Confusion = true;
710
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
711
+
712
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
713
+ // ended (the engine's protocol is always from the omniscient perspective and clients
714
+ // can be expected to recognize this pattern and pass the information only to the
715
+ // correct player)
716
+ try log.start(.{ battle.active(player), .ConfusionSilent });
717
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
703
718
  }
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
+
704
724
  return .skip_can;
705
725
  }
706
726
 
707
727
  if (volatiles.Binding) {
708
- assert(volatiles.attacks > 0);
709
-
710
- if (options.calc.modify(player, .binding)) |extend| {
711
- if (!extend) volatiles.attacks = 0;
712
- } else {
713
- volatiles.attacks -= 1;
714
- }
715
- 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);
716
730
 
717
731
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
718
732
  if (showdown or battle.last_damage != 0) {
@@ -742,7 +756,6 @@ fn canMove(
742
756
  const player_ident = battle.active(player);
743
757
  const move = Move.get(side.last_selected_move);
744
758
 
745
- var skip = skip_pp;
746
759
  if (side.active.volatiles.Charging) {
747
760
  side.active.volatiles.Charging = false;
748
761
  side.active.volatiles.Invulnerable = false;
@@ -754,7 +767,7 @@ fn canMove(
754
767
  }
755
768
 
756
769
  side.last_used_move = side.last_selected_move;
757
- if (!skip) decrementPP(side, mslot, auto);
770
+ if (!skip_pp) decrementPP(side, mslot, auto);
758
771
 
759
772
  const target = if (move.target == .Self) player else player.foe();
760
773
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -806,7 +819,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
806
819
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
807
820
  }
808
821
 
809
- fn incrementPP(side: *Side, mslot: u4) void {
822
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
810
823
  var active = &side.active;
811
824
  const volatiles = &active.volatiles;
812
825
 
@@ -814,8 +827,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
814
827
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
815
828
  if (showdown and volatiles.Transform) return;
816
829
 
830
+ const n = options.calc.overridden(player, .pp) orelse 1;
831
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
832
+
817
833
  assert(mslot > 0 and mslot <= 4);
818
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
834
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
819
835
  }
820
836
 
821
837
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -863,13 +879,13 @@ fn doMove(
863
879
  break :miss true;
864
880
  }
865
881
  break :miss (move.target != .Self and
866
- !moveHit(battle, player, move, &immune, &mist, options));
882
+ !try moveHit(battle, player, move, &immune, &mist, options));
867
883
  };
868
884
  assert(!immune or miss or (showdown and move.effect == .Binding));
869
885
 
870
886
  var late = showdown and move.effect != .Explode;
871
887
  const skip = status or immune;
872
- if ((!showdown or (!skip or counter)) and !miss) blk: {
888
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
873
889
  if (showdown and move.effect.isMulti()) {
874
890
  try Effects.multiHit(battle, player, move, options);
875
891
  hits = side.active.volatiles.attacks;
@@ -891,8 +907,9 @@ fn doMove(
891
907
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
892
908
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
893
909
  battle.last_damage = if (ohko) 65535 else 0;
894
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
895
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
910
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
911
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
912
+ if (!showdown) try options.chance.commit(player, .err);
896
913
  return @as(?Result, Result.Error);
897
914
  }
898
915
  if (battle.last_damage == 0) {
@@ -906,11 +923,12 @@ fn doMove(
906
923
  }
907
924
  }
908
925
 
909
- var zero = battle.last_damage == 0;
910
- miss = if (showdown or skip)
911
- miss
912
- else
913
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
926
+ var missed: bool = undefined;
927
+ const zero = battle.last_damage == 0;
928
+ if (!showdown and !skip) {
929
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
930
+ miss = missed or zero;
931
+ }
914
932
 
915
933
  assert(showdown or miss or battle.last_damage > 0 or skip);
916
934
  assert((!showdown and miss) or !(ohko and immune));
@@ -934,14 +952,35 @@ fn doMove(
934
952
  ohko = (!showdown or (!immune and !invulnerable)) and
935
953
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
936
954
  if (ohko) {
937
- try log.immune(.{ foe_ident, Immune.OHKO });
955
+ try log.immune(.{ foe_ident, .OHKO });
938
956
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
939
- try log.immune(.{ foe_ident, Immune.None });
957
+ try log.immune(.{ foe_ident, .None });
958
+ if (!showdown) {
959
+ if (move.effect == .Binding) {
960
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
961
+ // then simply clears it later on if the move happens to miss in moveHit.
962
+ // However, moveHit can return true and we'll still end up in the outer miss
963
+ // handling block because attacking into an immune Pokemon does zero damage
964
+ // (considered missing)
965
+ assert(zero);
966
+ // Thus we still need to actually save the hit result (but not the crit roll).
967
+ // Using .miss for this commit would accomplish what we want, but is somewhat
968
+ // misleading as its entirely possible the move actually did hit (and the target
969
+ // is now trapped) so we use the .binding value instead which is effectively
970
+ // identical, just doesn't claim to know about the actual results of the hit
971
+ // roll)
972
+ try options.chance.commit(player, .binding);
973
+ } else {
974
+ // The .glitch option allows for conditionally committing if we are in a
975
+ // division-by-zero freeze alternative timeline scenario
976
+ try options.chance.commit(player, .glitch);
977
+ }
978
+ }
940
979
  } else if (mist) {
941
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
942
- try log.fail(.{ foe_ident, Fail.None });
980
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
981
+ try log.fail(.{ foe_ident, .None });
943
982
  } else {
944
- if (showdown or !zero) try options.chance.commit(player, .miss);
983
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
945
984
  try log.lastmiss(.{});
946
985
  try log.miss(.{battle.active(player)});
947
986
  }
@@ -956,17 +995,15 @@ fn doMove(
956
995
  try buildRage(battle, player.foe(), options);
957
996
  } else if (showdown and move.effect == .Disable) {
958
997
  try buildRage(battle, player.foe(), options);
959
- } else if (!showdown and immune and move.effect == .Binding) {
960
- try options.chance.commit(player, .binding);
961
998
  }
962
999
  return null;
963
- } else {
1000
+ } else if (!showdown) {
964
1001
  try options.chance.commit(player, .hit);
965
1002
  }
966
1003
 
967
1004
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
968
1005
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
969
- // it up above before damage calculation).
1006
+ // it up above before damage calculation)
970
1007
  if (!showdown and move.effect.isMulti()) {
971
1008
  try Effects.multiHit(battle, player, move, options);
972
1009
  hits = side.active.volatiles.attacks;
@@ -1005,32 +1042,30 @@ fn doMove(
1005
1042
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1006
1043
  }
1007
1044
  try log.hitcount(.{ battle.active(player.foe()), hit });
1008
- } else if (showdown) {
1045
+ } else if (showdown and move.effect == .Binding) {
1009
1046
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1010
- if (move.effect == .Binding) {
1011
- try Effects.binding(battle, player, rewrap, options);
1012
- if (immune) {
1013
- battle.last_damage = 0;
1014
- assert(foe.stored().hp > 0);
1015
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1016
- if (sub) {
1017
- try log.activate(battle.active(player.foe()), .Substitute);
1018
- } else {
1019
- try log.damage(.{ battle.active(player.foe()), foe.stored(), Damage.None });
1020
- try buildRage(battle, player.foe(), options);
1021
- }
1022
- return null;
1047
+ try Effects.binding(battle, player, rewrap, options);
1048
+ if (immune) {
1049
+ battle.last_damage = 0;
1050
+ assert(foe.stored().hp > 0);
1051
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1052
+ if (sub) {
1053
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1054
+ } else {
1055
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1056
+ try buildRage(battle, player.foe(), options);
1023
1057
  }
1058
+ return null;
1024
1059
  }
1025
1060
  }
1026
1061
 
1027
1062
  // Substitute being broken nullifies the move's effect completely so even
1028
- // if an effect was intended to "always happen" it will still get skipped.
1063
+ // if an effect was intended to "always happen" it will still get skipped
1029
1064
  if (nullified) return null;
1030
1065
 
1031
1066
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1032
1067
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1033
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1068
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1034
1069
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1035
1070
 
1036
1071
  if (foe.stored().hp == 0) return null;
@@ -1045,7 +1080,7 @@ fn doMove(
1045
1080
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1046
1081
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1047
1082
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1048
- // puts the |-status| message before |-hitcount| instead of after.
1083
+ // puts the |-status| message before |-hitcount| instead of after
1049
1084
  if (move.effect == .Twineedle) {
1050
1085
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1051
1086
  } else if (move.effect == .Disable) {
@@ -1064,12 +1099,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1064
1099
  const side = battle.side(player);
1065
1100
 
1066
1101
  // Base speed is used for the critical hit calculation, even when Transform-ed
1067
- var rate = @as(u16, Species.chance(side.stored().species));
1102
+ var rate: u16 = Species.chance(side.stored().species);
1068
1103
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1069
1104
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1070
1105
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1071
1106
 
1072
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1107
+ const spurious =
1108
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1109
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1073
1110
  }
1074
1111
 
1075
1112
  fn calcDamage(
@@ -1078,6 +1115,7 @@ fn calcDamage(
1078
1115
  target_player: Player,
1079
1116
  m: ?Move.Data,
1080
1117
  crit: bool,
1118
+ options: anytype,
1081
1119
  ) bool {
1082
1120
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1083
1121
  const cfz = m == null;
@@ -1089,28 +1127,8 @@ fn calcDamage(
1089
1127
 
1090
1128
  const special = move.type.special();
1091
1129
 
1092
- // zig fmt: off
1093
- var atk: u32 =
1094
- if (crit)
1095
- if (special) side.stored().stats.spc
1096
- else side.stored().stats.atk
1097
- else
1098
- if (special) side.active.stats.spc
1099
- else side.active.stats.atk;
1100
- var def: u32 =
1101
- if (crit)
1102
- if (special) target.stored().stats.spc
1103
- else target.stored().stats.def
1104
- else
1105
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1106
- if (special)
1107
- target.active.stats.spc *
1108
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1109
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1110
- else
1111
- target.active.stats.def *
1112
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1113
- // zig fmt: on
1130
+ var atk = attack(side, crit, special);
1131
+ var def = defense(target, crit, special, cfz);
1114
1132
 
1115
1133
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1116
1134
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1121,9 +1139,9 @@ fn calcDamage(
1121
1139
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1122
1140
  }
1123
1141
 
1124
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1142
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1125
1143
 
1126
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1144
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1127
1145
 
1128
1146
  if (def == 0) return false;
1129
1147
 
@@ -1135,11 +1153,41 @@ fn calcDamage(
1135
1153
  d = @min(997, d);
1136
1154
  d += 2;
1137
1155
 
1156
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1157
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1158
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1159
+ // that results in counter intuitive probabilities in the common case
1160
+ if (pkmn.options.chance and !showdown) {
1161
+ atk = attack(side, !crit, special);
1162
+ def = defense(target, !crit, special, false);
1163
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1164
+ }
1165
+
1138
1166
  battle.last_damage = @intCast(d);
1139
1167
 
1140
1168
  return true;
1141
1169
  }
1142
1170
 
1171
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1172
+ return if (crit)
1173
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1174
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1175
+ }
1176
+
1177
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) target.stored().stats.spc else target.stored().stats.def
1180
+ else
1181
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1182
+ if (special)
1183
+ target.active.stats.spc *
1184
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1185
+ else
1186
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1187
+ target.active.stats.def *
1188
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1189
+ }
1190
+
1143
1191
  fn adjustDamage(battle: anytype, player: Player) u16 {
1144
1192
  const side = battle.side(player);
1145
1193
  const foe = battle.foe(player);
@@ -1184,7 +1232,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1184
1232
  const foe = battle.foe(player);
1185
1233
 
1186
1234
  if (!try checkHit(battle, player, move, options)) return null;
1187
- try options.chance.commit(player, .hit);
1188
1235
 
1189
1236
  battle.last_damage = switch (side.last_selected_move) {
1190
1237
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1214,7 +1261,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1214
1261
  const foe = battle.foe(player);
1215
1262
 
1216
1263
  if (battle.last_damage == 0) {
1217
- try options.log.fail(.{ battle.active(player), Fail.None });
1264
+ try options.log.fail(.{ battle.active(player), .None });
1218
1265
  return null;
1219
1266
  }
1220
1267
 
@@ -1232,22 +1279,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1232
1279
  foe_last_selected_move.type == .Fighting);
1233
1280
 
1234
1281
  if (!used and !selected) {
1235
- try options.log.fail(.{ battle.active(player), Fail.None });
1282
+ try options.log.fail(.{ battle.active(player), .None });
1236
1283
  return null;
1237
1284
  }
1238
1285
 
1239
1286
  if (!used or !selected) {
1240
1287
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1241
1288
  if (!showdown) return Result.Error;
1242
- try options.log.fail(.{ battle.active(player), Fail.None });
1289
+ try options.log.fail(.{ battle.active(player), .None });
1243
1290
  return null;
1244
1291
  }
1245
1292
 
1246
1293
  battle.last_damage *|= 2;
1247
1294
 
1248
- // Pokémon Showdown calls checkHit before Counter
1295
+ // Pokémon Showdown calls moveHit before Counter
1249
1296
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1250
- try options.chance.commit(player, .hit);
1251
1297
 
1252
1298
  const sub = showdown and foe.active.volatiles.Substitute;
1253
1299
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1275,13 +1321,13 @@ fn applyDamage(
1275
1321
  subbed.active.volatiles.substitute = 0;
1276
1322
  subbed.active.volatiles.Substitute = false;
1277
1323
  // battle.last_damage is not updated with the amount of HP the Substitute had
1278
- try options.log.end(.{ battle.active(sub_player), End.Substitute });
1279
- options.calc.capped(target_player);
1324
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1325
+ if (reason != .Confusion) options.calc.capped(sub_player);
1280
1326
  return true;
1281
1327
  } else {
1282
1328
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1283
1329
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1284
- try options.log.activate(battle.active(sub_player), .Substitute);
1330
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1285
1331
  return false;
1286
1332
  }
1287
1333
  }
@@ -1307,10 +1353,10 @@ fn mirrorMove(
1307
1353
  side.last_selected_move = foe.last_used_move;
1308
1354
 
1309
1355
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1310
- try options.log.fail(.{ battle.active(player), Fail.None });
1356
+ try options.log.fail(.{ battle.active(player), .None });
1311
1357
  return null;
1312
1358
  } else if (!showdown or foe.last_used_move != .Struggle) {
1313
- incrementPP(side, mslot);
1359
+ incrementPP(side, player, mslot, options);
1314
1360
  }
1315
1361
 
1316
1362
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1331,7 +1377,7 @@ fn metronome(
1331
1377
  var side = battle.side(player);
1332
1378
 
1333
1379
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1334
- incrementPP(side, mslot);
1380
+ incrementPP(side, player, mslot, options);
1335
1381
 
1336
1382
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1337
1383
  return null;
@@ -1343,20 +1389,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1343
1389
  var immune = false;
1344
1390
  var mist = false;
1345
1391
 
1346
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1392
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1393
+ if (!showdown) try options.chance.commit(player, .hit);
1394
+ return true;
1395
+ }
1347
1396
 
1348
1397
  assert(!immune);
1349
1398
  if (mist) {
1350
1399
  assert(!showdown);
1351
1400
  const foe_ident = battle.active(player.foe());
1352
- try options.log.activate(foe_ident, .Mist);
1353
- try options.log.fail(.{ foe_ident, Fail.None });
1401
+ try options.log.activate(.{ foe_ident, .Mist });
1402
+ try options.log.fail(.{ foe_ident, .None });
1354
1403
  } else {
1355
- try options.chance.commit(player, .miss);
1356
1404
  try options.log.lastmiss(.{});
1357
1405
  try options.log.miss(.{battle.active(player)});
1358
1406
  }
1359
1407
 
1408
+ if (!showdown) try options.chance.commit(player, .miss);
1360
1409
  return false;
1361
1410
  }
1362
1411
 
@@ -1367,7 +1416,7 @@ fn moveHit(
1367
1416
  immune: *bool,
1368
1417
  mist: *bool,
1369
1418
  options: anytype,
1370
- ) bool {
1419
+ ) !bool {
1371
1420
  var side = battle.side(player);
1372
1421
  const foe = battle.foe(player);
1373
1422
 
@@ -1406,7 +1455,7 @@ fn moveHit(
1406
1455
  const overwritten = overwrite and state > 0;
1407
1456
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1408
1457
 
1409
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1458
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1410
1459
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1411
1460
  accuracy = accuracy * boost[0] / boost[1];
1412
1461
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1418,7 +1467,7 @@ fn moveHit(
1418
1467
  if (overwrite) side.active.volatiles.state = accuracy;
1419
1468
 
1420
1469
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1421
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1470
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1422
1471
  };
1423
1472
 
1424
1473
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1429,13 +1478,17 @@ fn moveHit(
1429
1478
 
1430
1479
  if (!miss) return true;
1431
1480
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1481
+
1432
1482
  side.active.volatiles.Binding = false;
1483
+ options.chance.observe(.binding, player, .None);
1484
+
1433
1485
  return false;
1434
1486
  }
1435
1487
 
1436
1488
  fn checkFaint(
1437
1489
  battle: anytype,
1438
1490
  player: Player,
1491
+ cap: bool,
1439
1492
  options: anytype,
1440
1493
  ) @TypeOf(options.log).Error!?Result {
1441
1494
  const side = battle.side(player);
@@ -1448,8 +1501,8 @@ fn checkFaint(
1448
1501
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1449
1502
  const more = player_out or foe_out;
1450
1503
 
1451
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1452
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1504
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1505
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1453
1506
 
1454
1507
  assert(!side.active.volatiles.MultiHit);
1455
1508
  assert(!foe.active.volatiles.MultiHit);
@@ -1470,7 +1523,7 @@ fn checkFaint(
1470
1523
  return .{ .p1 = foe_choice, .p2 = .Switch };
1471
1524
  }
1472
1525
 
1473
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1526
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1474
1527
  var side = battle.side(player);
1475
1528
  var foe = battle.foe(player);
1476
1529
  assert(side.stored().hp == 0);
@@ -1504,7 +1557,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1504
1557
  }
1505
1558
 
1506
1559
  try options.log.faint(.{ battle.active(player), done });
1507
- options.calc.capped(player);
1560
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1561
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1562
+ if (cap) options.calc.capped(player);
1508
1563
  return null;
1509
1564
  }
1510
1565
 
@@ -1552,13 +1607,13 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1552
1607
  stored.hp -= amount;
1553
1608
 
1554
1609
  // As above, Pokémon Showdown uses damage [of] but its not relevant
1555
- if (amount > 0) try options.log.damage(.{ ident, stored, Damage.LeechSeed });
1610
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1556
1611
 
1557
1612
  const before = foe_stored.hp;
1558
1613
  // Uncapped damage is added back to the foe
1559
1614
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1560
1615
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1561
- if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
1616
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1562
1617
  }
1563
1618
  }
1564
1619
 
@@ -1650,7 +1705,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1650
1705
  side.last_selected_move;
1651
1706
  if (m != .None and m == last) {
1652
1707
  side.active.volatiles.Charging = false;
1653
- try options.log.cant(.{ ident, Cant.Disable, last });
1708
+ try options.log.cant(.{ ident, .Disable, last });
1654
1709
  return true;
1655
1710
  }
1656
1711
  }
@@ -1664,17 +1719,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1664
1719
  }
1665
1720
  }
1666
1721
 
1667
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1668
- var volatiles = &active.volatiles;
1669
- assert(volatiles.Thrashing);
1670
- if (volatiles.attacks > 0) return false;
1671
-
1672
- volatiles.Thrashing = false;
1673
- volatiles.Confusion = true;
1674
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1675
- return true;
1676
- }
1677
-
1678
1722
  fn onBegin(
1679
1723
  battle: anytype,
1680
1724
  player: Player,
@@ -1683,7 +1727,6 @@ fn onBegin(
1683
1727
  residual: *bool,
1684
1728
  options: anytype,
1685
1729
  ) !void {
1686
- assert(move.effect.onBegin());
1687
1730
  return switch (move.effect) {
1688
1731
  .Confusion => Effects.confusion(battle, player, move, options),
1689
1732
  .Conversion => Effects.conversion(battle, player, options),
@@ -1706,7 +1749,6 @@ fn onBegin(
1706
1749
  }
1707
1750
 
1708
1751
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1709
- assert(move.effect.onEnd());
1710
1752
  return switch (move.effect) {
1711
1753
  .Bide => Effects.bide(battle, player, options),
1712
1754
  // zig fmt: off
@@ -1728,7 +1770,6 @@ fn alwaysHappens(
1728
1770
  residual: *bool,
1729
1771
  options: anytype,
1730
1772
  ) !void {
1731
- assert(move.effect.alwaysHappens());
1732
1773
  return switch (move.effect) {
1733
1774
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1734
1775
  .Explode => Effects.explode(battle, player),
@@ -1764,10 +1805,12 @@ pub const Effects = struct {
1764
1805
 
1765
1806
  side.active.volatiles.Bide = true;
1766
1807
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1808
+
1767
1809
  side.active.volatiles.state = 0;
1768
1810
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1811
+ options.chance.observe(.attacking, player, .started);
1769
1812
 
1770
- try options.log.start(battle.active(player), .Bide);
1813
+ try options.log.start(.{ battle.active(player), .Bide });
1771
1814
  }
1772
1815
 
1773
1816
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1784,7 +1827,7 @@ pub const Effects = struct {
1784
1827
  try options.log.curestatus(.{
1785
1828
  battle.active(player.foe()),
1786
1829
  foe_stored.status,
1787
- CureStatus.Message,
1830
+ .Message,
1788
1831
  });
1789
1832
  foe_stored.status = 0;
1790
1833
  }
@@ -1799,8 +1842,7 @@ pub const Effects = struct {
1799
1842
  foe_stored.status = Status.init(.BRN);
1800
1843
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1801
1844
 
1802
- const reason: protocol.Status = .None;
1803
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1845
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1804
1846
  }
1805
1847
 
1806
1848
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1822,26 +1864,21 @@ pub const Effects = struct {
1822
1864
  if (!try Rolls.confusionChance(battle, player, options)) return;
1823
1865
  } else {
1824
1866
  if (showdown) {
1825
- if (!try checkHit(battle, player, move, options)) {
1826
- return;
1827
- } else if (sub) {
1828
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1829
- }
1867
+ if (!try checkHit(battle, player, move, options)) return;
1868
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1830
1869
  } else {
1831
- if (sub) {
1832
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1833
- } else if (!try checkHit(battle, player, move, options)) {
1834
- return;
1835
- }
1870
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1871
+ if (!try checkHit(battle, player, move, options)) return;
1836
1872
  }
1837
- try options.chance.commit(player, .hit);
1838
1873
  }
1839
1874
 
1840
1875
  if (foe.active.volatiles.Confusion) return;
1841
1876
  foe.active.volatiles.Confusion = true;
1842
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1843
1877
 
1844
- try options.log.start(battle.active(player.foe()), .Confusion);
1878
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1879
+ options.chance.observe(.confusion, player.foe(), .started);
1880
+
1881
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1845
1882
  }
1846
1883
 
1847
1884
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
@@ -1853,11 +1890,12 @@ pub const Effects = struct {
1853
1890
  }
1854
1891
 
1855
1892
  battle.side(player).active.types = foe.active.types;
1856
- return options.log.typechange(
1893
+ return options.log.start(.{
1857
1894
  battle.active(player),
1895
+ .TypeChange,
1858
1896
  foe.active.types,
1859
1897
  battle.active(player.foe()),
1860
- );
1898
+ });
1861
1899
  }
1862
1900
 
1863
1901
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1866,13 +1904,10 @@ pub const Effects = struct {
1866
1904
  const foe_ident = battle.active(player.foe());
1867
1905
 
1868
1906
  // Pokémon Showdown handles hit/miss earlier in doMove
1869
- if (!showdown) {
1870
- if (!try checkHit(battle, player, move, options)) return null;
1871
- try options.chance.commit(player, .hit);
1872
- }
1907
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1873
1908
 
1874
1909
  if (volatiles.disable_move != 0) {
1875
- try options.log.fail(.{ foe_ident, Fail.None });
1910
+ try options.log.fail(.{ foe_ident, .None });
1876
1911
  return null;
1877
1912
  }
1878
1913
 
@@ -1888,7 +1923,7 @@ pub const Effects = struct {
1888
1923
  // Technically this is still considered simply a "miss" on the cartridge,
1889
1924
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1890
1925
  if (n == 0) {
1891
- try options.log.fail(.{ foe_ident, Fail.None });
1926
+ try options.log.fail(.{ foe_ident, .None });
1892
1927
  return null;
1893
1928
  } else if (err) {
1894
1929
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1899,9 +1934,10 @@ pub const Effects = struct {
1899
1934
  volatiles.disable_move =
1900
1935
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1901
1936
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1937
+ options.chance.observe(.disable, player.foe(), .started);
1902
1938
 
1903
1939
  const id = foe.active.move(volatiles.disable_move).id;
1904
- try options.log.startEffect(foe_ident, .Disable, id);
1940
+ try options.log.start(.{ foe_ident, .Disable, id });
1905
1941
  return null;
1906
1942
  }
1907
1943
 
@@ -1920,7 +1956,7 @@ pub const Effects = struct {
1920
1956
  if (stored.hp == stored.stats.hp) return;
1921
1957
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1922
1958
 
1923
- try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1959
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1924
1960
  }
1925
1961
 
1926
1962
  fn explode(battle: anytype, player: Player) !void {
@@ -1952,7 +1988,7 @@ pub const Effects = struct {
1952
1988
  if (side.active.volatiles.FocusEnergy) return;
1953
1989
  side.active.volatiles.FocusEnergy = true;
1954
1990
 
1955
- try options.log.start(battle.active(player), .FocusEnergy);
1991
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1956
1992
  }
1957
1993
 
1958
1994
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1974,7 +2010,7 @@ pub const Effects = struct {
1974
2010
  foe_stored.status = Status.init(.FRZ);
1975
2011
  // GLITCH: Hyper Beam recharging status is not cleared
1976
2012
 
1977
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2013
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1978
2014
  }
1979
2015
 
1980
2016
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1982,7 +2018,7 @@ pub const Effects = struct {
1982
2018
  var side = battle.side(player);
1983
2019
  var foe = battle.foe(player);
1984
2020
 
1985
- var side_stored = side.stored();
2021
+ const side_stored = side.stored();
1986
2022
  var foe_stored = foe.stored();
1987
2023
 
1988
2024
  const player_ident = battle.active(player);
@@ -1994,7 +2030,7 @@ pub const Effects = struct {
1994
2030
  side.active.stats = side_stored.stats;
1995
2031
  foe.active.stats = foe_stored.stats;
1996
2032
 
1997
- try log.activate(player_ident, .Haze);
2033
+ try log.activate(.{ player_ident, .Haze });
1998
2034
  try log.clearallboost(.{});
1999
2035
 
2000
2036
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
@@ -2003,24 +2039,26 @@ pub const Effects = struct {
2003
2039
  const p: Player = @enumFromInt(i);
2004
2040
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2005
2041
  if (p != player and Status.any(s.stored().status)) {
2006
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2042
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2007
2043
  s.stored().status = 0;
2044
+ options.chance.observe(.sleep, p, .None);
2008
2045
  } else if (showdown and s.stored().status == Status.TOX) {
2009
2046
  s.stored().status = Status.init(.PSN);
2010
- try log.status(.{ battle.active(p), s.stored().status, protocol.Status.None });
2047
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2011
2048
  }
2012
- try clearVolatiles(battle, p, p != player, options);
2049
+ try clearVolatiles(battle, p, options);
2013
2050
  }
2014
2051
  } else {
2015
2052
  if (Status.any(foe_stored.status)) {
2016
2053
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2017
2054
  foe.last_selected_move = .SKIP_TURN;
2055
+ options.chance.observe(.sleep, player.foe(), .None);
2018
2056
  }
2019
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2057
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2020
2058
  foe_stored.status = 0;
2021
2059
  }
2022
- try clearVolatiles(battle, player, false, options);
2023
- try clearVolatiles(battle, player.foe(), true, options);
2060
+ try clearVolatiles(battle, player, options);
2061
+ try clearVolatiles(battle, player.foe(), options);
2024
2062
  }
2025
2063
  }
2026
2064
 
@@ -2032,7 +2070,7 @@ pub const Effects = struct {
2032
2070
  // GLITCH: HP recovery move failure glitches
2033
2071
  const delta = stored.stats.hp - stored.hp;
2034
2072
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2035
- return options.log.fail(.{ ident, Fail.None });
2073
+ return options.log.fail(.{ ident, .None });
2036
2074
  }
2037
2075
 
2038
2076
  const rest = side.last_selected_move == .Rest;
@@ -2040,7 +2078,7 @@ pub const Effects = struct {
2040
2078
  // Adding the sleep status runs the sleep condition handler to roll duration
2041
2079
  if (showdown) battle.rng.advance(1);
2042
2080
  stored.status = Status.slf(2);
2043
- try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
2081
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2044
2082
  stored.hp = stored.stats.hp;
2045
2083
  } else {
2046
2084
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -2059,7 +2097,7 @@ pub const Effects = struct {
2059
2097
  if (showdown) {
2060
2098
  // Invulnerability trumps type immunity on Pokémon Showdown
2061
2099
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2062
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
2100
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2063
2101
  }
2064
2102
  if (!try checkHit(battle, player, move, options)) return;
2065
2103
  if (foe.active.volatiles.LeechSeed) return;
@@ -2071,21 +2109,19 @@ pub const Effects = struct {
2071
2109
  }
2072
2110
  }
2073
2111
 
2074
- try options.chance.commit(player, .hit);
2075
2112
  foe.active.volatiles.LeechSeed = true;
2076
2113
 
2077
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2114
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2078
2115
  }
2079
2116
 
2080
2117
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2081
2118
  var side = battle.side(player);
2119
+ const ident = battle.active(player);
2082
2120
 
2083
- if (side.active.volatiles.LightScreen) {
2084
- return options.log.fail(.{ battle.active(player), Fail.None });
2085
- }
2121
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2086
2122
  side.active.volatiles.LightScreen = true;
2087
2123
 
2088
- try options.log.start(battle.active(player), .LightScreen);
2124
+ try options.log.start(.{ ident, .LightScreen });
2089
2125
  }
2090
2126
 
2091
2127
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2113,17 +2149,15 @@ pub const Effects = struct {
2113
2149
  if (!has_mimic) {
2114
2150
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2115
2151
  if (!try checkHit(battle, player, move, options)) return;
2116
- try options.chance.commit(player, .hit);
2117
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2152
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2118
2153
  }
2119
2154
  }
2120
2155
  if (!try checkHit(battle, player, move, options)) return;
2121
- try options.chance.commit(player, .hit);
2122
2156
 
2123
2157
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2124
2158
  side.active.move(oslot).id = foe.active.move(rslot).id;
2125
2159
 
2126
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2160
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2127
2161
  }
2128
2162
 
2129
2163
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2132,7 +2166,7 @@ pub const Effects = struct {
2132
2166
  if (side.active.volatiles.Mist) return;
2133
2167
  side.active.volatiles.Mist = true;
2134
2168
 
2135
- try options.log.start(battle.active(player), .Mist);
2169
+ try options.log.start(.{ battle.active(player), .Mist });
2136
2170
  }
2137
2171
 
2138
2172
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2159,7 +2193,7 @@ pub const Effects = struct {
2159
2193
  if (showdown) {
2160
2194
  // Invulnerability trumps type immunity on Pokémon Showdown
2161
2195
  if (immune and !foe.active.volatiles.Invulnerable) {
2162
- return log.immune(.{ foe_ident, Immune.None });
2196
+ return log.immune(.{ foe_ident, .None });
2163
2197
  } else if (!try checkHit(battle, player, move, options)) return;
2164
2198
  }
2165
2199
  if (Status.any(foe_stored.status)) {
@@ -2169,15 +2203,14 @@ pub const Effects = struct {
2169
2203
  });
2170
2204
  }
2171
2205
  if (!showdown) {
2172
- if (immune) return log.immune(.{ foe_ident, Immune.None });
2206
+ if (immune) return log.immune(.{ foe_ident, .None });
2173
2207
  if (!try checkHit(battle, player, move, options)) return;
2174
2208
  }
2175
- try options.chance.commit(player, .hit);
2176
2209
 
2177
2210
  foe_stored.status = Status.init(.PAR);
2178
2211
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2179
2212
 
2180
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2213
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2181
2214
  }
2182
2215
 
2183
2216
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2199,8 +2232,7 @@ pub const Effects = struct {
2199
2232
  foe_stored.status = Status.init(.PAR);
2200
2233
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2201
2234
 
2202
- const reason: protocol.Status = .None;
2203
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2235
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2204
2236
  }
2205
2237
 
2206
2238
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
@@ -2220,7 +2252,7 @@ pub const Effects = struct {
2220
2252
  return;
2221
2253
  } else if (foe.active.volatiles.Substitute) {
2222
2254
  if (move.effect != .Poison) return;
2223
- return log.fail(.{ foe_ident, Fail.None });
2255
+ return log.fail(.{ foe_ident, .None });
2224
2256
  } else if (Status.any(foe_stored.status)) {
2225
2257
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2226
2258
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
@@ -2236,12 +2268,11 @@ pub const Effects = struct {
2236
2268
  Fail.None });
2237
2269
  } else if (foe.active.types.includes(.Poison)) {
2238
2270
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2239
- return log.immune(.{ foe_ident, Immune.None });
2271
+ return log.immune(.{ foe_ident, .None });
2240
2272
  }
2241
2273
 
2242
2274
  if (move.effect == .Poison) {
2243
2275
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2244
- try options.chance.commit(player, .hit);
2245
2276
  } else {
2246
2277
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2247
2278
  return;
@@ -2255,7 +2286,7 @@ pub const Effects = struct {
2255
2286
  foe.active.volatiles.toxic = 0;
2256
2287
  }
2257
2288
 
2258
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2289
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2259
2290
  }
2260
2291
 
2261
2292
  fn rage(battle: anytype, player: Player) !void {
@@ -2278,7 +2309,7 @@ pub const Effects = struct {
2278
2309
  try options.log.damage(.{
2279
2310
  battle.active(player),
2280
2311
  stored,
2281
- Damage.RecoilOf,
2312
+ .RecoilOf,
2282
2313
  battle.active(player.foe()),
2283
2314
  });
2284
2315
  if (showdown and stored.hp == 0) residual.* = false;
@@ -2286,13 +2317,12 @@ pub const Effects = struct {
2286
2317
 
2287
2318
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2288
2319
  var side = battle.side(player);
2320
+ const ident = battle.active(player);
2289
2321
 
2290
- if (side.active.volatiles.Reflect) {
2291
- return options.log.fail(.{ battle.active(player), Fail.None });
2292
- }
2322
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2293
2323
  side.active.volatiles.Reflect = true;
2294
2324
 
2295
- try options.log.start(battle.active(player), .Reflect);
2325
+ try options.log.start(.{ ident, .Reflect });
2296
2326
  }
2297
2327
 
2298
2328
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2311,9 +2341,8 @@ pub const Effects = struct {
2311
2341
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2312
2342
  });
2313
2343
  }
2314
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2344
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2315
2345
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2316
- try options.chance.commit(player, .hit);
2317
2346
  }
2318
2347
 
2319
2348
  // Sleep Clause Mod
@@ -2325,18 +2354,20 @@ pub const Effects = struct {
2325
2354
  foe.active.volatiles.Recharging = false;
2326
2355
 
2327
2356
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2357
+ options.chance.observe(.sleep, player.foe(), .started);
2358
+
2328
2359
  const last = battle.side(player).last_selected_move;
2329
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.From, last });
2360
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2330
2361
  }
2331
2362
 
2332
2363
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2333
- try options.log.activate(battle.active(player), .Splash);
2364
+ try options.log.activate(.{ battle.active(player), .Splash });
2334
2365
  }
2335
2366
 
2336
2367
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2337
2368
  var side = battle.side(player);
2338
2369
  if (side.active.volatiles.Substitute) {
2339
- return options.log.fail(.{ battle.active(player), Fail.Substitute });
2370
+ return options.log.fail(.{ battle.active(player), .Substitute });
2340
2371
  }
2341
2372
 
2342
2373
  assert(side.stored().stats.hp <= 1023);
@@ -2348,16 +2379,16 @@ pub const Effects = struct {
2348
2379
  // avoid having to convert to floating point)
2349
2380
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2350
2381
  if (side.stored().hp < required_hp) {
2351
- return options.log.fail(.{ battle.active(player), Fail.Weak });
2382
+ return options.log.fail(.{ battle.active(player), .Weak });
2352
2383
  }
2353
2384
 
2354
2385
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2355
2386
  side.stored().hp -= hp;
2356
2387
  side.active.volatiles.substitute = hp + 1;
2357
2388
  side.active.volatiles.Substitute = true;
2358
- try options.log.start(battle.active(player), .Substitute);
2389
+ try options.log.start(.{ battle.active(player), .Substitute });
2359
2390
  if (hp > 0) {
2360
- try options.log.damage(.{ battle.active(player), side.stored(), Damage.None });
2391
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2361
2392
  if (showdown and side.stored().hp == 0) residual.* = false;
2362
2393
  }
2363
2394
  }
@@ -2366,12 +2397,10 @@ pub const Effects = struct {
2366
2397
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2367
2398
  if (showdown) {
2368
2399
  if (battle.side(player).last_selected_move == .Teleport) return;
2369
- if (try checkHit(battle, player, move, options)) {
2370
- try options.chance.commit(player, .hit);
2371
- }
2400
+ _ = try checkHit(battle, player, move, options);
2372
2401
  battle.last_damage = 0;
2373
2402
  } else {
2374
- try options.log.fail(.{ battle.active(player), Fail.None });
2403
+ try options.log.fail(.{ battle.active(player), .None });
2375
2404
  try options.log.laststill(.{});
2376
2405
  }
2377
2406
  }
@@ -2383,6 +2412,8 @@ pub const Effects = struct {
2383
2412
 
2384
2413
  volatiles.Thrashing = true;
2385
2414
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2415
+
2416
+ options.chance.observe(.attacking, player, .started);
2386
2417
  }
2387
2418
 
2388
2419
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2428,6 +2459,7 @@ pub const Effects = struct {
2428
2459
 
2429
2460
  side.active.volatiles.attacks =
2430
2461
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2462
+ options.chance.observe(.binding, player, .started);
2431
2463
  }
2432
2464
 
2433
2465
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,20 +2473,20 @@ pub const Effects = struct {
2441
2473
  switch (move.effect) {
2442
2474
  .AttackUp1, .AttackUp2, .Rage => {
2443
2475
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2444
- if (boosts.atk == 6) return log.fail(.{ ident, Fail.None });
2476
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2445
2477
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2446
2478
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2447
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2479
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2448
2480
  if (stats.atk == MAX_STAT_VALUE) {
2449
2481
  boosts.atk -= 1;
2450
2482
  if (showdown) {
2451
2483
  try log.boost(.{ ident, reason, n });
2452
- try log.boost(.{ ident, Boost.Attack, -1 });
2484
+ try log.boost(.{ ident, .Attack, -1 });
2453
2485
  if (move.effect == .Rage) return;
2454
2486
  }
2455
- return log.fail(.{ ident, Fail.None });
2487
+ return log.fail(.{ ident, .None });
2456
2488
  }
2457
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2489
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2458
2490
  const stat = unmodifiedStats(battle, side).atk;
2459
2491
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2460
2492
  try log.boost(.{ ident, reason, n });
@@ -2463,65 +2495,65 @@ pub const Effects = struct {
2463
2495
  },
2464
2496
  .DefenseUp1, .DefenseUp2 => {
2465
2497
  assert(boosts.def >= -6 and boosts.def <= 6);
2466
- if (boosts.def == 6) return log.fail(.{ ident, Fail.None });
2498
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2467
2499
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2468
2500
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2469
2501
  if (stats.def == MAX_STAT_VALUE) {
2470
2502
  boosts.def -= 1;
2471
2503
  if (showdown) {
2472
- try log.boost(.{ ident, Boost.Defense, n });
2473
- try log.boost(.{ ident, Boost.Defense, -1 });
2504
+ try log.boost(.{ ident, .Defense, n });
2505
+ try log.boost(.{ ident, .Defense, -1 });
2474
2506
  }
2475
- return log.fail(.{ ident, Fail.None });
2507
+ return log.fail(.{ ident, .None });
2476
2508
  }
2477
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2509
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2478
2510
  const stat = unmodifiedStats(battle, side).def;
2479
2511
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2480
- try log.boost(.{ ident, Boost.Defense, n });
2512
+ try log.boost(.{ ident, .Defense, n });
2481
2513
  },
2482
2514
  .SpeedUp2 => {
2483
2515
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2484
- if (boosts.spe == 6) return log.fail(.{ ident, Fail.None });
2516
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2485
2517
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2486
2518
  if (stats.spe == MAX_STAT_VALUE) {
2487
2519
  boosts.spe -= 1;
2488
2520
  if (showdown) {
2489
- try log.boost(.{ ident, Boost.Speed, 2 });
2490
- try log.boost(.{ ident, Boost.Speed, -1 });
2521
+ try log.boost(.{ ident, .Speed, 2 });
2522
+ try log.boost(.{ ident, .Speed, -1 });
2491
2523
  }
2492
- return log.fail(.{ ident, Fail.None });
2524
+ return log.fail(.{ ident, .None });
2493
2525
  }
2494
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2526
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2495
2527
  const stat = unmodifiedStats(battle, side).spe;
2496
2528
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2497
- try log.boost(.{ ident, Boost.Speed, 2 });
2529
+ try log.boost(.{ ident, .Speed, 2 });
2498
2530
  },
2499
2531
  .SpecialUp1, .SpecialUp2 => {
2500
2532
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2501
- if (boosts.spc == 6) return log.fail(.{ ident, Fail.None });
2533
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2502
2534
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2503
2535
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2504
2536
  if (stats.spc == MAX_STAT_VALUE) {
2505
2537
  boosts.spc -= 1;
2506
2538
  if (showdown) {
2507
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2508
- try log.boost(.{ ident, Boost.SpecialAttack, -1 });
2509
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2510
- try log.boost(.{ ident, Boost.SpecialDefense, -1 });
2539
+ try log.boost(.{ ident, .SpecialAttack, n });
2540
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2541
+ try log.boost(.{ ident, .SpecialDefense, n });
2542
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2511
2543
  }
2512
- return log.fail(.{ ident, Fail.None });
2544
+ return log.fail(.{ ident, .None });
2513
2545
  }
2514
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2546
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2515
2547
  const stat = unmodifiedStats(battle, side).spc;
2516
2548
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2517
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2518
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2549
+ try log.boost(.{ ident, .SpecialAttack, n });
2550
+ try log.boost(.{ ident, .SpecialDefense, n });
2519
2551
  },
2520
2552
  .EvasionUp1 => {
2521
2553
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2522
- if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2554
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2523
2555
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2524
- try log.boost(.{ ident, Boost.Evasion, 1 });
2556
+ try log.boost(.{ ident, .Evasion, 1 });
2525
2557
  },
2526
2558
  else => unreachable,
2527
2559
  }
@@ -2536,9 +2568,7 @@ pub const Effects = struct {
2536
2568
  const foe_ident = battle.active(player.foe());
2537
2569
  const secondary = move.effect.isStatDownChance();
2538
2570
 
2539
- if (foe.active.volatiles.Substitute) {
2540
- return if (!secondary) log.fail(.{ foe_ident, Fail.None });
2541
- }
2571
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2542
2572
 
2543
2573
  if (secondary) {
2544
2574
  const proc = try Rolls.unboost(battle, player, options);
@@ -2546,7 +2576,6 @@ pub const Effects = struct {
2546
2576
  } else {
2547
2577
  // checkHit already checks for Invulnerable
2548
2578
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2549
- try options.chance.commit(player, .hit);
2550
2579
  }
2551
2580
 
2552
2581
  var stats = &foe.active.stats;
@@ -2556,82 +2585,82 @@ pub const Effects = struct {
2556
2585
  switch (move.effect) {
2557
2586
  .AttackDown1, .AttackDownChance => {
2558
2587
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2559
- if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2588
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2560
2589
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2561
2590
  if (stats.atk == 1) {
2562
2591
  boosts.atk += 1;
2563
2592
  if (showdown) {
2564
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2565
- try log.boost(.{ foe_ident, Boost.Attack, 1 });
2593
+ try log.boost(.{ foe_ident, .Attack, -1 });
2594
+ try log.boost(.{ foe_ident, .Attack, 1 });
2566
2595
  }
2567
- return log.fail(.{ foe_ident, Fail.None });
2596
+ return log.fail(.{ foe_ident, .None });
2568
2597
  }
2569
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2598
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2570
2599
  const stat = unmodifiedStats(battle, foe).atk;
2571
2600
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2572
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2601
+ try log.boost(.{ foe_ident, .Attack, -1 });
2573
2602
  },
2574
2603
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2575
2604
  assert(boosts.def >= -6 and boosts.def <= 6);
2576
- if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2605
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2577
2606
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2578
2607
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2579
2608
  if (stats.def == 1) {
2580
2609
  boosts.def += 1;
2581
2610
  if (showdown) {
2582
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2583
- try log.boost(.{ foe_ident, Boost.Defense, 1 });
2611
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2612
+ try log.boost(.{ foe_ident, .Defense, 1 });
2584
2613
  }
2585
- return log.fail(.{ foe_ident, Fail.None });
2614
+ return log.fail(.{ foe_ident, .None });
2586
2615
  }
2587
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2616
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2588
2617
  const stat = unmodifiedStats(battle, foe).def;
2589
2618
  stats.def = @max(1, stat * mod[0] / mod[1]);
2590
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2619
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2591
2620
  },
2592
2621
  .SpeedDown1, .SpeedDownChance => {
2593
2622
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2594
- if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2623
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2595
2624
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2596
2625
  if (stats.spe == 1) {
2597
2626
  boosts.spe += 1;
2598
2627
  if (showdown) {
2599
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2600
- try log.boost(.{ foe_ident, Boost.Speed, 1 });
2628
+ try log.boost(.{ foe_ident, .Speed, -1 });
2629
+ try log.boost(.{ foe_ident, .Speed, 1 });
2601
2630
  }
2602
- return log.fail(.{ foe_ident, Fail.None });
2631
+ return log.fail(.{ foe_ident, .None });
2603
2632
  }
2604
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2633
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2605
2634
  const stat = unmodifiedStats(battle, foe).spe;
2606
2635
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2607
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2636
+ try log.boost(.{ foe_ident, .Speed, -1 });
2608
2637
  assert(boosts.spe >= -6);
2609
2638
  },
2610
2639
  .SpecialDownChance => {
2611
2640
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2612
- if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2641
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2613
2642
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2614
2643
  if (stats.spc == 1) {
2615
2644
  boosts.spc += 1;
2616
2645
  if (showdown) {
2617
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2618
- try log.boost(.{ foe_ident, Boost.SpecialAttack, 1 });
2619
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2620
- try log.boost(.{ foe_ident, Boost.SpecialDefense, 1 });
2646
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2647
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2648
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2649
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2621
2650
  }
2622
- return log.fail(.{ foe_ident, Fail.None });
2651
+ return log.fail(.{ foe_ident, .None });
2623
2652
  }
2624
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2653
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2625
2654
  const stat = unmodifiedStats(battle, foe).spc;
2626
2655
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2627
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2628
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2656
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2657
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2629
2658
  },
2630
2659
  .AccuracyDown1 => {
2631
2660
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2632
- if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2661
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2633
2662
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2634
- try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2663
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2635
2664
  },
2636
2665
  else => unreachable,
2637
2666
  }
@@ -2660,35 +2689,35 @@ fn isForced(active: anytype) bool {
2660
2689
  active.volatiles.Thrashing or active.volatiles.Charging;
2661
2690
  }
2662
2691
 
2663
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2692
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2664
2693
  var log = options.log;
2665
2694
  var side = battle.side(who);
2666
2695
  var volatiles = &side.active.volatiles;
2667
2696
  const ident = battle.active(who);
2668
2697
 
2669
- options.chance.clearDurations(who, clear);
2670
-
2671
2698
  if (volatiles.disable_move != 0) {
2672
2699
  volatiles.disable_move = 0;
2673
2700
  volatiles.disable_duration = 0;
2674
- try log.end(.{ ident, End.DisableSilent });
2701
+ options.chance.observe(.disable, who, .None);
2702
+ try log.end(.{ ident, .DisableSilent });
2675
2703
  }
2676
2704
  if (volatiles.Confusion) {
2677
2705
  // volatiles.confusion is left unchanged
2678
2706
  volatiles.Confusion = false;
2679
- try log.end(.{ ident, End.ConfusionSilent });
2707
+ options.chance.observe(.confusion, who, .None);
2708
+ try log.end(.{ ident, .ConfusionSilent });
2680
2709
  }
2681
2710
  if (volatiles.Mist) {
2682
2711
  volatiles.Mist = false;
2683
- try log.end(.{ ident, End.MistSilent });
2712
+ try log.end(.{ ident, .MistSilent });
2684
2713
  }
2685
2714
  if (volatiles.FocusEnergy) {
2686
2715
  volatiles.FocusEnergy = false;
2687
- try log.end(.{ ident, End.FocusEnergySilent });
2716
+ try log.end(.{ ident, .FocusEnergySilent });
2688
2717
  }
2689
2718
  if (volatiles.LeechSeed) {
2690
2719
  volatiles.LeechSeed = false;
2691
- try log.end(.{ ident, End.LeechSeedSilent });
2720
+ try log.end(.{ ident, .LeechSeedSilent });
2692
2721
  }
2693
2722
  if (!showdown and volatiles.Toxic) {
2694
2723
  volatiles.Toxic = false;
@@ -2696,24 +2725,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2696
2725
  }
2697
2726
  if (volatiles.LightScreen) {
2698
2727
  volatiles.LightScreen = false;
2699
- try log.end(.{ ident, End.LightScreenSilent });
2728
+ try log.end(.{ ident, .LightScreenSilent });
2700
2729
  }
2701
2730
  if (volatiles.Reflect) {
2702
2731
  volatiles.Reflect = false;
2703
- try log.end(.{ ident, End.ReflectSilent });
2732
+ try log.end(.{ ident, .ReflectSilent });
2704
2733
  }
2705
2734
  if (showdown and volatiles.Toxic) {
2706
2735
  volatiles.Toxic = false;
2707
2736
  // Pokémon Showdown erroneously clears the toxic counter
2708
2737
  volatiles.toxic = 0;
2709
- try log.end(.{ ident, End.ToxicSilent });
2738
+ try log.end(.{ ident, .ToxicSilent });
2710
2739
  }
2711
2740
  }
2712
2741
 
2742
+ fn decrement(
2743
+ comptime field: chance.Action.Field,
2744
+ player: Player,
2745
+ options: anytype,
2746
+ n: anytype,
2747
+ ) @TypeOf(n) {
2748
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2749
+ .started, .ended => 0,
2750
+ .continuing => if (n > 1) n - 1 else n,
2751
+ else => unreachable,
2752
+ } else n - 1;
2753
+ }
2754
+
2713
2755
  pub const Rolls = struct {
2714
2756
  fn speedTie(battle: anytype, options: anytype) !bool {
2715
2757
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2716
2758
  player == .P1
2759
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2760
+ battle.rng.speedTie()
2717
2761
  else if (showdown)
2718
2762
  battle.rng.range(u8, 0, 2) == 0
2719
2763
  else
@@ -2723,21 +2767,31 @@ pub const Rolls = struct {
2723
2767
  return p1;
2724
2768
  }
2725
2769
 
2726
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2770
+ fn criticalHit(
2771
+ battle: anytype,
2772
+ player: Player,
2773
+ rate: u8,
2774
+ spurious: bool,
2775
+ options: anytype,
2776
+ ) !bool {
2727
2777
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2728
2778
  val == .true
2779
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2780
+ battle.rng.criticalHit(player, rate)
2729
2781
  else if (showdown)
2730
2782
  battle.rng.chance(u8, rate, 256)
2731
2783
  else
2732
2784
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2733
2785
 
2734
- try options.chance.criticalHit(player, crit, rate);
2786
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2735
2787
  return crit;
2736
2788
  }
2737
2789
 
2738
2790
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2739
2791
  const roll = if (options.calc.overridden(player, .damage)) |val|
2740
2792
  val
2793
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2794
+ battle.rng.damage(player)
2741
2795
  else roll: {
2742
2796
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2743
2797
  while (true) {
@@ -2751,21 +2805,27 @@ pub const Rolls = struct {
2751
2805
  return roll;
2752
2806
  }
2753
2807
 
2754
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2808
+ const uN = if (pkmn.options.miss) u8 else u9;
2809
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2810
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2755
2811
  const ok = if (options.calc.overridden(player, .hit)) |val|
2756
2812
  val == .true
2813
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2814
+ battle.rng.hit(player, acc)
2757
2815
  else if (showdown)
2758
- battle.rng.chance(u8, accuracy, 256)
2816
+ battle.rng.chance(uN, acc, 256)
2759
2817
  else
2760
- battle.rng.next() < accuracy;
2818
+ battle.rng.next() < acc;
2761
2819
 
2762
- options.chance.hit(ok, accuracy);
2820
+ try options.chance.hit(player, ok, acc);
2763
2821
  return ok;
2764
2822
  }
2765
2823
 
2766
2824
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2767
2825
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2768
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2828
+ battle.rng.confused(player)
2769
2829
  else if (showdown)
2770
2830
  !battle.rng.chance(u8, 128, 256)
2771
2831
  else
@@ -2778,6 +2838,8 @@ pub const Rolls = struct {
2778
2838
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2779
2839
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2780
2840
  val == .true
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2842
+ battle.rng.paralyzed(player)
2781
2843
  else if (showdown)
2782
2844
  battle.rng.chance(u8, 63, 256)
2783
2845
  else
@@ -2790,6 +2852,8 @@ pub const Rolls = struct {
2790
2852
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2791
2853
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2792
2854
  val == .true
2855
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2856
+ battle.rng.secondaryChance(player, 25)
2793
2857
  else if (showdown)
2794
2858
  battle.rng.chance(u8, 25, 256)
2795
2859
  else
@@ -2804,6 +2868,8 @@ pub const Rolls = struct {
2804
2868
 
2805
2869
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2806
2870
  val == .true
2871
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2872
+ battle.rng.secondaryChance(player, rate)
2807
2873
  else if (showdown)
2808
2874
  battle.rng.chance(u8, rate, 256)
2809
2875
  else
@@ -2818,6 +2884,8 @@ pub const Rolls = struct {
2818
2884
 
2819
2885
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2820
2886
  val == .true
2887
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2888
+ battle.rng.secondaryChance(player, rate)
2821
2889
  else if (showdown)
2822
2890
  battle.rng.chance(u8, rate, 256)
2823
2891
  else
@@ -2830,6 +2898,8 @@ pub const Rolls = struct {
2830
2898
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2831
2899
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2832
2900
  val == .true
2901
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2902
+ battle.rng.secondaryChance(player, 85)
2833
2903
  else if (showdown)
2834
2904
  battle.rng.chance(u8, 85, 256)
2835
2905
  else
@@ -2842,6 +2912,8 @@ pub const Rolls = struct {
2842
2912
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2843
2913
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2844
2914
  val
2915
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2916
+ battle.rng.metronome(player)
2845
2917
  else if (showdown)
2846
2918
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2847
2919
  else move: {
@@ -2860,6 +2932,8 @@ pub const Rolls = struct {
2860
2932
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2861
2933
  const power = if (options.calc.overridden(player, .psywave)) |val|
2862
2934
  val - 1
2935
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2936
+ battle.rng.psywave(player)
2863
2937
  else if (showdown)
2864
2938
  battle.rng.range(u8, 0, max)
2865
2939
  else power: {
@@ -2875,8 +2949,11 @@ pub const Rolls = struct {
2875
2949
  }
2876
2950
 
2877
2951
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2878
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2879
- @intCast(val)
2952
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2953
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2954
+ else
2955
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2956
+ battle.rng.sleepDuration(player)
2880
2957
  else if (showdown)
2881
2958
  battle.rng.range(u3, 1, 8)
2882
2959
  else duration: {
@@ -2887,46 +2964,55 @@ pub const Rolls = struct {
2887
2964
  };
2888
2965
 
2889
2966
  assert(duration >= 1 and duration <= 7);
2890
- options.chance.duration(.sleep, player, player.foe(), duration);
2967
+ options.chance.duration(player, duration);
2891
2968
  return duration;
2892
2969
  }
2893
2970
 
2894
2971
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2896
- val
2972
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2973
+ if (val >= 1 and val <= 8) val else null
2974
+ else
2975
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2976
+ battle.rng.disableDuration(player)
2897
2977
  else if (showdown)
2898
2978
  battle.rng.range(u4, 1, 9)
2899
2979
  else
2900
2980
  @intCast((battle.rng.next() & 7) + 1);
2901
2981
 
2902
2982
  assert(duration >= 1 and duration <= 8);
2903
- options.chance.duration(.disable, player, player.foe(), duration);
2983
+ options.chance.duration(player, duration);
2904
2984
  return duration;
2905
2985
  }
2906
2986
 
2907
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2908
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2909
- @intCast(val)
2987
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2988
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2989
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2990
+ else
2991
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2992
+ battle.rng.confusionDuration(player)
2910
2993
  else if (showdown)
2911
2994
  battle.rng.range(u3, 2, 6)
2912
2995
  else
2913
2996
  @intCast((battle.rng.next() & 3) + 2);
2914
2997
 
2915
2998
  assert(duration >= 2 and duration <= 5);
2916
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2999
+ options.chance.duration(player, duration);
2917
3000
  return duration;
2918
3001
  }
2919
3002
 
2920
3003
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2921
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2922
- @intCast(val)
3004
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3005
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3006
+ else
3007
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3008
+ battle.rng.attackingDuration(player)
2923
3009
  else if (showdown)
2924
3010
  battle.rng.range(u3, 2, 4)
2925
3011
  else
2926
3012
  @intCast((battle.rng.next() & 1) + 2);
2927
3013
 
2928
3014
  assert(duration >= 2 and duration <= 3);
2929
- options.chance.duration(.attacking, player, player, duration);
3015
+ options.chance.duration(player, duration);
2930
3016
  return duration;
2931
3017
  }
2932
3018
 
@@ -2939,8 +3025,11 @@ pub const Rolls = struct {
2939
3025
  options: anytype,
2940
3026
  ) !u3 {
2941
3027
  const roll = if (effect == .Binding) .duration else .multi_hit;
2942
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2943
- @intCast(val)
3028
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3029
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3030
+ else
3031
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3032
+ battle.rng.distribution(player)
2944
3033
  else if (showdown)
2945
3034
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2946
3035
  else n: {
@@ -2950,7 +3039,7 @@ pub const Rolls = struct {
2950
3039
 
2951
3040
  assert(n >= 2 and n <= 5);
2952
3041
  if (effect == .Binding) {
2953
- options.chance.duration(.binding, player, player, n);
3042
+ options.chance.duration(player, n);
2954
3043
  } else {
2955
3044
  try options.chance.multiHit(player, n);
2956
3045
  }
@@ -2965,15 +3054,15 @@ pub const Rolls = struct {
2965
3054
  options: anytype,
2966
3055
  ) !u4 {
2967
3056
  // TODO: consider throwing error instead of rerolling?
2968
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3057
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2969
3058
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2970
3059
  val
2971
3060
  else
2972
3061
  null
2973
3062
  else
2974
- null;
2975
-
2976
- const slot: u4 = overridden orelse slot: {
3063
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3064
+ battle.rng.moveSlot(player, moves, check_pp)
3065
+ else slot: {
2977
3066
  if (showdown) {
2978
3067
  if (check_pp == 0) {
2979
3068
  var i: usize = moves.len;
@@ -3053,11 +3142,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3053
3142
  const side = battle.side(player);
3054
3143
  const foe = battle.foe(player);
3055
3144
 
3056
- var active = &side.active;
3145
+ const active = &side.active;
3057
3146
  const stored = side.stored();
3058
3147
 
3059
3148
  // While players are not given any input options on the cartridge in these cases,
3060
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3149
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3061
3150
  //
3062
3151
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3063
3152
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3065,7 +3154,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3065
3154
  // passing due to being forced into using a move. Instead of introducing another option
3066
3155
  // we simply repurpose Move with no move slot, even though pedantically this is not
3067
3156
  // strictly correct as the player would not have been presented the option to move or
3068
- // switch at all.
3157
+ // switch at all
3069
3158
  if (isForced(active)) {
3070
3159
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3071
3160
  n += 1;
@@ -3084,7 +3173,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3084
3173
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3085
3174
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3086
3175
  // either disables all other moves in the case of limited or requires you to select a
3087
- // move normally if sleeping/frozen/bound.
3176
+ // move normally if sleeping/frozen/bound
3088
3177
  if (!showdown and (limited or foe.active.volatiles.Binding or
3089
3178
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3090
3179
  {
@@ -3097,7 +3186,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3097
3186
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3098
3187
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3099
3188
  // Move will not result in forcing the subsequent use unless the user also had the
3100
- // proc-ed move in their moveset) and disabling all other moves.
3189
+ // proc-ed move in their moveset) and disabling all other moves
3101
3190
  if (limited) {
3102
3191
  assert(showdown);
3103
3192
  assert(side.last_selected_move != .None);