@pkmn/engine 0.1.0-dev.89cc1c98 → 0.1.0-dev.8b192003

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 (42) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -4
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +2 -1
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +5 -5
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/test/display.js +2 -2
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +5 -4
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +100 -102
  17. package/package.json +31 -23
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +4 -4
  20. package/src/lib/common/debug.zig +2 -2
  21. package/src/lib/common/optional.zig +1 -1
  22. package/src/lib/common/options.zig +2 -0
  23. package/src/lib/common/protocol.zig +393 -396
  24. package/src/lib/common/rational.zig +109 -15
  25. package/src/lib/common/rng.zig +20 -11
  26. package/src/lib/gen1/calc.zig +478 -92
  27. package/src/lib/gen1/chance.zig +195 -428
  28. package/src/lib/gen1/data/species.zig +1 -1
  29. package/src/lib/gen1/data.zig +21 -21
  30. package/src/lib/gen1/helpers.zig +88 -534
  31. package/src/lib/gen1/mechanics.zig +377 -360
  32. package/src/lib/gen2/chance.zig +25 -457
  33. package/src/lib/gen2/data.zig +13 -11
  34. package/src/lib/gen2/generated.zig +27 -78
  35. package/src/lib/gen2/helpers.zig +37 -7
  36. package/src/lib/gen2/mechanics.zig +1022 -456
  37. package/src/lib/{binding/node.zig → node.zig} +9 -12
  38. package/src/lib/pkmn.zig +27 -12
  39. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  40. package/src/test/display/pkmn.html.tmpl +5 -5
  41. package/src/lib/gen1/README.md +0 -894
  42. package/src/lib/gen2/README.md +0 -330
@@ -4,10 +4,12 @@ const pkmn = @import("../pkmn.zig");
4
4
 
5
5
  const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
+ const optional = @import("../common/optional.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
10
 
10
11
  const data = @import("data.zig");
12
+ const chance = @import("chance.zig");
11
13
 
12
14
  const assert = std.debug.assert;
13
15
 
@@ -22,6 +24,7 @@ const Result = common.Result;
22
24
 
23
25
  const Boost = protocol.Boost;
24
26
  const Damage = protocol.Damage;
27
+ const Fail = protocol.Fail;
25
28
  const Heal = protocol.Heal;
26
29
 
27
30
  const Gen12 = rng.Gen12;
@@ -70,8 +73,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
70
73
  var p1 = battle.side(.P1);
71
74
  var p2 = battle.side(.P2);
72
75
 
73
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
74
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
75
78
 
76
79
  if (try turnOrder(battle, c1, c2, options) == .P1) {
77
80
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -89,11 +92,11 @@ fn start(battle: anytype, options: anytype) !Result {
89
92
  const p1 = battle.side(.P1);
90
93
  const p2 = battle.side(.P2);
91
94
 
92
- var p1_slot = findFirstAlive(p1);
95
+ const p1_slot = findFirstAlive(p1);
93
96
  assert(!showdown or p1_slot == 1);
94
97
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
95
98
 
96
- var p2_slot = findFirstAlive(p2);
99
+ const p2_slot = findFirstAlive(p2);
97
100
  assert(!showdown or p2_slot == 1);
98
101
  if (p2_slot == 0) return Result.Win;
99
102
 
@@ -165,7 +168,7 @@ fn selectMove(
165
168
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
166
169
  // set the selected move will not actually be used, it will just be reported as having
167
170
  // been used (this differs from how Pokémon Showdown works, but its impossible to
168
- // replicate the incorrect behavior with the correct mechanisms).
171
+ // replicate the incorrect behavior with the correct mechanisms)
169
172
  saveMove(battle, player, choice);
170
173
  } else {
171
174
  assert(choice.data == 0);
@@ -255,14 +258,13 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
255
258
 
256
259
  foe.active.volatiles.Binding = false;
257
260
 
258
- try options.log.switched(battle.active(player), incoming);
259
- options.chance.switched(player, side.order[0], out);
261
+ try options.log.switched(.{ battle.active(player), incoming });
260
262
 
261
263
  if (showdown and incoming.status == Status.TOX) {
262
264
  incoming.status = Status.init(.PSN);
263
265
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
264
266
  // rather not clutter up turnOrder just for this (incorrect) log message
265
- try options.log.status(battle.active(player), incoming.status, .Silent);
267
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
266
268
  }
267
269
  }
268
270
 
@@ -346,10 +348,10 @@ fn doTurn(
346
348
  )) |r| return r;
347
349
  if (!replace) {
348
350
  if (player_choice.type != .Switch) {
349
- if (try checkFaint(battle, foe_player, options)) |r| return r;
351
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
350
352
  }
351
353
  if (residual) try handleResidual(battle, player, options);
352
- if (try checkFaint(battle, player, options)) |r| return r;
354
+ if (try checkFaint(battle, player, false, options)) |r| return r;
353
355
  } else if (foe_choice.type == .Pass) return null;
354
356
 
355
357
  options.chance.clearPending();
@@ -368,10 +370,10 @@ fn doTurn(
368
370
  )) |r| return r;
369
371
  if (!replace) {
370
372
  if (!calc and foe_choice.type != .Switch) {
371
- if (try checkFaint(battle, player, options)) |r| return r;
373
+ if (try checkFaint(battle, player, true, options)) |r| return r;
372
374
  }
373
375
  if (residual) try handleResidual(battle, foe_player, options);
374
- if (try checkFaint(battle, foe_player, options)) |r| return r;
376
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
375
377
  }
376
378
 
377
379
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -406,7 +408,7 @@ fn executeMove(
406
408
  if (side.last_selected_move == .SKIP_TURN) {
407
409
  assert(!showdown);
408
410
  if (battle.foe(player).active.volatiles.Binding) {
409
- try options.log.cant(battle.active(player), .Bound);
411
+ try options.log.cant(.{ battle.active(player), .Bound });
410
412
  }
411
413
  return null;
412
414
  }
@@ -430,7 +432,7 @@ fn executeMove(
430
432
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
431
433
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
432
434
  // thawed, in which case we will desync unless the last_selected_move happened to be at
433
- // index 1 and the current Pokémon has the same move in its first slot.
435
+ // index 1 and the current Pokémon has the same move in its first slot
434
436
  if (!auto) {
435
437
  // side.active.moves(slot) is safe to check even though the slot in question might not
436
438
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -484,35 +486,29 @@ fn beforeMove(
484
486
  if (Status.is(stored.status, .SLP)) {
485
487
  const before = stored.status;
486
488
  // Even if the EXT bit is set this will still correctly modify the sleep duration
487
- if (options.calc.modify(player, .sleep)) |extend| {
488
- if (!extend) stored.status = 0;
489
- } else {
490
- stored.status -= 1;
491
- }
489
+ stored.status -= 1;
492
490
 
493
491
  const duration = Status.duration(stored.status);
494
- if (!Status.is(stored.status, .EXT)) {
495
- try options.chance.sleep(player, duration);
496
- }
492
+ options.chance.durations(.sleep, player, observation(duration));
497
493
 
498
494
  if (duration == 0) {
499
- try log.curestatus(ident, before, .Message);
495
+ try log.curestatus(.{ ident, before, .Message });
500
496
  stored.status = 0; // clears EXT if present
501
497
  } else {
502
- try log.cant(ident, .Sleep);
498
+ try log.cant(.{ ident, .Sleep });
503
499
  }
504
500
  side.last_used_move = .None;
505
501
  return .done;
506
502
  }
507
503
 
508
504
  if (Status.is(stored.status, .FRZ)) {
509
- try log.cant(ident, .Freeze);
505
+ try log.cant(.{ ident, .Freeze });
510
506
  side.last_used_move = .None;
511
507
  return .done;
512
508
  }
513
509
 
514
510
  if (skip or foe.active.volatiles.Binding) {
515
- try log.cant(ident, .Bound);
511
+ try log.cant(.{ ident, .Bound });
516
512
  return .done;
517
513
  }
518
514
 
@@ -520,27 +516,23 @@ fn beforeMove(
520
516
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
521
517
  // Pokémon can sometimes flinch multiple times from the same original hit
522
518
  if (!showdown) volatiles.Flinch = false;
523
- try log.cant(ident, .Flinch);
519
+ try log.cant(.{ ident, .Flinch });
524
520
  return .done;
525
521
  }
526
522
 
527
523
  if (volatiles.Recharging) {
528
524
  volatiles.Recharging = false;
529
- try log.cant(ident, .Recharge);
525
+ try log.cant(.{ ident, .Recharge });
530
526
  return .done;
531
527
  }
532
528
 
533
529
  if (volatiles.disable_duration > 0) {
534
- if (options.calc.modify(player, .disable)) |extend| {
535
- if (!extend) volatiles.disable_duration = 0;
536
- } else {
537
- volatiles.disable_duration -= 1;
538
- }
539
- try options.chance.disable(player, volatiles.disable_duration);
530
+ volatiles.disable_duration -= 1;
531
+ options.chance.durations(.disable, player, observation(volatiles.disable_duration));
540
532
 
541
533
  if (volatiles.disable_duration == 0) {
542
534
  volatiles.disable_move = 0;
543
- try log.end(ident, .Disable);
535
+ try log.end(.{ ident, .Disable });
544
536
  }
545
537
  }
546
538
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -555,19 +547,14 @@ fn beforeMove(
555
547
 
556
548
  if (volatiles.Confusion) {
557
549
  assert(volatiles.confusion > 0);
558
-
559
- if (options.calc.modify(player, .confusion)) |extend| {
560
- if (!extend) volatiles.confusion = 0;
561
- } else {
562
- volatiles.confusion -= 1;
563
- }
564
- try options.chance.confusion(player, volatiles.confusion);
550
+ volatiles.confusion -= 1;
551
+ options.chance.durations(.confusion, player, observation(volatiles.confusion));
565
552
 
566
553
  if (volatiles.confusion == 0) {
567
554
  volatiles.Confusion = false;
568
- try log.end(ident, .Confusion);
555
+ try log.end(.{ ident, .Confusion });
569
556
  } else {
570
- try log.activate(ident, .Confusion);
557
+ try log.activate(.{ ident, .Confusion });
571
558
 
572
559
  if (try Rolls.confused(battle, player, options)) {
573
560
  assert(!volatiles.MultiHit);
@@ -579,7 +566,10 @@ fn beforeMove(
579
566
  volatiles.Charging = false;
580
567
  volatiles.Binding = false;
581
568
  volatiles.Invulnerable = false;
582
- options.chance.clearDurations(player, null);
569
+
570
+ options.chance.durations(.bide, player, .None);
571
+ options.chance.durations(.thrashing, player, .None);
572
+ options.chance.durations(.binding, player, .None);
583
573
  {
584
574
  // This feels (and is) disgusting but the cartridge literally just overwrites
585
575
  // the opponent's defense with the user's defense and resets it after. As a
@@ -616,9 +606,13 @@ fn beforeMove(
616
606
  volatiles.Thrashing = false;
617
607
  volatiles.Charging = false;
618
608
  volatiles.Binding = false;
619
- options.chance.clearDurations(player, null);
609
+
610
+ options.chance.durations(.bide, player, .None);
611
+ options.chance.durations(.thrashing, player, .None);
612
+ options.chance.durations(.binding, player, .None);
613
+
620
614
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
621
- try log.cant(ident, .Paralysis);
615
+ try log.cant(.{ ident, .Paralysis });
622
616
  return .done;
623
617
  }
624
618
 
@@ -634,28 +628,22 @@ fn beforeMove(
634
628
  volatiles.state +%= battle.last_damage;
635
629
  }
636
630
 
637
- assert(volatiles.attacks > 0);
638
-
639
- if (options.calc.modify(player, .attacking)) |extend| {
640
- if (!extend) volatiles.attacks = 0;
641
- } else {
642
- volatiles.attacks -= 1;
643
- }
644
- try options.chance.attacking(player, volatiles.attacks);
631
+ volatiles.attacks -= 1;
632
+ options.chance.durations(.bide, player, observation(volatiles.attacks));
645
633
 
646
634
  if (volatiles.attacks != 0) {
647
- try log.activate(ident, .Bide);
635
+ try log.activate(.{ ident, .Bide });
648
636
  return .done;
649
637
  }
650
638
 
651
639
  volatiles.Bide = false;
652
- try log.end(ident, .Bide);
640
+ try log.end(.{ ident, .Bide });
653
641
 
654
642
  battle.last_damage = volatiles.state *% 2;
655
643
  volatiles.state = 0;
656
644
 
657
645
  if (battle.last_damage == 0) {
658
- try log.fail(ident, .None);
646
+ try log.fail(.{ ident, .None });
659
647
  return .done;
660
648
  }
661
649
 
@@ -675,41 +663,39 @@ fn beforeMove(
675
663
  }
676
664
 
677
665
  if (volatiles.Thrashing) {
678
- assert(volatiles.attacks > 0);
666
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
679
667
 
680
- if (options.calc.modify(player, .attacking)) |extend| {
681
- if (!extend) volatiles.attacks = 0;
682
- } else {
683
- volatiles.attacks -= 1;
684
- }
685
- try options.chance.attacking(player, volatiles.attacks);
668
+ volatiles.attacks -= 1;
669
+ options.chance.durations(
670
+ .thrashing,
671
+ player,
672
+ if (volatiles.attacks <= 1) .ended else .continuing,
673
+ );
686
674
 
687
- if (!showdown and handleThrashing(battle, active, player, options)) {
688
- try log.start(battle.active(player), .ConfusionSilent);
689
- }
690
- try log.move(ident, side.last_selected_move, battle.active(player.foe()));
691
- if (showdown) {
692
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
693
- // used in useMove and doesn't have the notion of skipping canMove semantics
694
- side.last_used_move = side.last_selected_move;
695
- if (handleThrashing(battle, active, player, options)) {
696
- try log.start(battle.active(player), .ConfusionSilent);
697
- }
675
+ if (volatiles.attacks == 0) {
676
+ volatiles.Thrashing = false;
677
+ volatiles.Confusion = true;
678
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
679
+
680
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
681
+ // ended (the engine's protocol is always from the omniscient perspective and clients
682
+ // can be expected to recognize this pattern and pass the information only to the
683
+ // correct player)
684
+ try log.start(.{ battle.active(player), .ConfusionSilent });
698
685
  }
686
+
687
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
688
+ // used in useMove and doesn't have the notion of skipping canMove semantics
689
+ if (showdown) side.last_used_move = side.last_selected_move;
690
+
699
691
  return .skip_can;
700
692
  }
701
693
 
702
694
  if (volatiles.Binding) {
703
- assert(volatiles.attacks > 0);
704
-
705
- if (options.calc.modify(player, .binding)) |extend| {
706
- if (!extend) volatiles.attacks = 0;
707
- } else {
708
- volatiles.attacks -= 1;
709
- }
710
- try options.chance.binding(player, volatiles.attacks);
695
+ volatiles.attacks -= 1;
696
+ options.chance.durations(.binding, player, observation(volatiles.attacks));
711
697
 
712
- try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
698
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
713
699
  if (showdown or battle.last_damage != 0) {
714
700
  const sub = showdown and foe.active.volatiles.Substitute;
715
701
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -737,22 +723,21 @@ fn canMove(
737
723
  const player_ident = battle.active(player);
738
724
  const move = Move.get(side.last_selected_move);
739
725
 
740
- var skip = skip_pp;
741
726
  if (side.active.volatiles.Charging) {
742
727
  side.active.volatiles.Charging = false;
743
728
  side.active.volatiles.Invulnerable = false;
744
729
  } else if (move.effect == .Charge) {
745
- try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
730
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
746
731
  setCounterable(battle, player, side, move);
747
732
  try Effects.charge(battle, player, options);
748
733
  return false;
749
734
  }
750
735
 
751
736
  side.last_used_move = side.last_selected_move;
752
- if (!skip) decrementPP(side, mslot, auto);
737
+ if (!skip_pp) decrementPP(side, mslot, auto);
753
738
 
754
739
  const target = if (move.target == .Self) player else player.foe();
755
- try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
740
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
756
741
  setCounterable(battle, player, side, move);
757
742
 
758
743
  if (move.effect.onBegin()) {
@@ -858,7 +843,7 @@ fn doMove(
858
843
  break :miss true;
859
844
  }
860
845
  break :miss (move.target != .Self and
861
- !moveHit(battle, player, move, &immune, &mist, options));
846
+ !try moveHit(battle, player, move, &immune, &mist, options));
862
847
  };
863
848
  assert(!immune or miss or (showdown and move.effect == .Binding));
864
849
 
@@ -901,11 +886,11 @@ fn doMove(
901
886
  }
902
887
  }
903
888
 
904
- var zero = battle.last_damage == 0;
889
+ const zero = battle.last_damage == 0;
905
890
  miss = if (showdown or skip)
906
891
  miss
907
892
  else
908
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
893
+ (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
909
894
 
910
895
  assert(showdown or miss or battle.last_damage > 0 or skip);
911
896
  assert((!showdown and miss) or !(ohko and immune));
@@ -929,16 +914,16 @@ fn doMove(
929
914
  ohko = (!showdown or (!immune and !invulnerable)) and
930
915
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
916
  if (ohko) {
932
- try log.immune(foe_ident, .OHKO);
917
+ try log.immune(.{ foe_ident, .OHKO });
933
918
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
934
- try log.immune(foe_ident, .None);
919
+ try log.immune(.{ foe_ident, .None });
935
920
  } else if (mist) {
936
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
937
- try log.fail(foe_ident, .None);
921
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
922
+ try log.fail(.{ foe_ident, .None });
938
923
  } else {
939
- if (showdown or !zero) try options.chance.commit(player, .miss);
940
- try log.lastmiss();
941
- try log.miss(battle.active(player));
924
+ if (!showdown and !zero) try options.chance.commit(player, .miss);
925
+ try log.lastmiss(.{});
926
+ try log.miss(.{battle.active(player)});
942
927
  }
943
928
  if (move.effect == .JumpKick) {
944
929
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -951,17 +936,15 @@ fn doMove(
951
936
  try buildRage(battle, player.foe(), options);
952
937
  } else if (showdown and move.effect == .Disable) {
953
938
  try buildRage(battle, player.foe(), options);
954
- } else if (!showdown and immune and move.effect == .Binding) {
955
- try options.chance.commit(player, .binding);
956
939
  }
957
940
  return null;
958
- } else {
941
+ } else if (!showdown) {
959
942
  try options.chance.commit(player, .hit);
960
943
  }
961
944
 
962
945
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
963
946
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
964
- // it up above before damage calculation).
947
+ // it up above before damage calculation)
965
948
  if (!showdown and move.effect.isMulti()) {
966
949
  try Effects.multiHit(battle, player, move, options);
967
950
  hits = side.active.volatiles.attacks;
@@ -974,15 +957,15 @@ fn doMove(
974
957
  var hit: u4 = 0;
975
958
  while (hit < hits) {
976
959
  if (hit == 0) {
977
- if (crit) try log.crit(battle.active(player.foe()));
960
+ if (crit) try log.crit(.{battle.active(player.foe())});
978
961
  if (effectiveness > Effectiveness.neutral) {
979
- try log.supereffective(battle.active(player.foe()));
962
+ try log.supereffective(.{battle.active(player.foe())});
980
963
  } else if (effectiveness < Effectiveness.neutral) {
981
- try log.resisted(battle.active(player.foe()));
964
+ try log.resisted(.{battle.active(player.foe())});
982
965
  }
983
966
  }
984
967
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
985
- if (hit == 0 and ohko) try log.ohko();
968
+ if (hit == 0 and ohko) try log.ohko(.{});
986
969
  hit += 1;
987
970
  if (foe.stored().hp == 0) break;
988
971
  if (!late and (!sub or move.effect == .Explode)) {
@@ -999,7 +982,7 @@ fn doMove(
999
982
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1000
983
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1001
984
  }
1002
- try log.hitcount(battle.active(player.foe()), hit);
985
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1003
986
  } else if (showdown) {
1004
987
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1005
988
  if (move.effect == .Binding) {
@@ -1009,9 +992,9 @@ fn doMove(
1009
992
  assert(foe.stored().hp > 0);
1010
993
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1011
994
  if (sub) {
1012
- try log.activate(battle.active(player.foe()), .Substitute);
995
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1013
996
  } else {
1014
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
997
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1015
998
  try buildRage(battle, player.foe(), options);
1016
999
  }
1017
1000
  return null;
@@ -1020,12 +1003,12 @@ fn doMove(
1020
1003
  }
1021
1004
 
1022
1005
  // Substitute being broken nullifies the move's effect completely so even
1023
- // if an effect was intended to "always happen" it will still get skipped.
1006
+ // if an effect was intended to "always happen" it will still get skipped
1024
1007
  if (nullified) return null;
1025
1008
 
1026
1009
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1027
1010
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1028
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1011
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1029
1012
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1030
1013
 
1031
1014
  if (foe.stored().hp == 0) return null;
@@ -1040,7 +1023,7 @@ fn doMove(
1040
1023
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1041
1024
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1042
1025
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1043
- // puts the |-status| message before |-hitcount| instead of after.
1026
+ // puts the |-status| message before |-hitcount| instead of after
1044
1027
  if (move.effect == .Twineedle) {
1045
1028
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1046
1029
  } else if (move.effect == .Disable) {
@@ -1059,12 +1042,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1059
1042
  const side = battle.side(player);
1060
1043
 
1061
1044
  // Base speed is used for the critical hit calculation, even when Transform-ed
1062
- var rate = @as(u16, Species.chance(side.stored().species));
1045
+ var rate: u16 = Species.chance(side.stored().species);
1063
1046
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1064
1047
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1065
1048
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1066
1049
 
1067
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1050
+ const spurious =
1051
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1052
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1068
1053
  }
1069
1054
 
1070
1055
  fn calcDamage(
@@ -1116,9 +1101,9 @@ fn calcDamage(
1116
1101
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1117
1102
  }
1118
1103
 
1119
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1104
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1120
1105
 
1121
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1106
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1122
1107
 
1123
1108
  if (def == 0) return false;
1124
1109
 
@@ -1179,7 +1164,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1179
1164
  const foe = battle.foe(player);
1180
1165
 
1181
1166
  if (!try checkHit(battle, player, move, options)) return null;
1182
- try options.chance.commit(player, .hit);
1183
1167
 
1184
1168
  battle.last_damage = switch (side.last_selected_move) {
1185
1169
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1209,7 +1193,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1193
  const foe = battle.foe(player);
1210
1194
 
1211
1195
  if (battle.last_damage == 0) {
1212
- try options.log.fail(battle.active(player), .None);
1196
+ try options.log.fail(.{ battle.active(player), .None });
1213
1197
  return null;
1214
1198
  }
1215
1199
 
@@ -1227,22 +1211,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1211
  foe_last_selected_move.type == .Fighting);
1228
1212
 
1229
1213
  if (!used and !selected) {
1230
- try options.log.fail(battle.active(player), .None);
1214
+ try options.log.fail(.{ battle.active(player), .None });
1231
1215
  return null;
1232
1216
  }
1233
1217
 
1234
1218
  if (!used or !selected) {
1235
1219
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1236
1220
  if (!showdown) return Result.Error;
1237
- try options.log.fail(battle.active(player), .None);
1221
+ try options.log.fail(.{ battle.active(player), .None });
1238
1222
  return null;
1239
1223
  }
1240
1224
 
1241
1225
  battle.last_damage *|= 2;
1242
1226
 
1243
- // Pokémon Showdown calls checkHit before Counter
1227
+ // Pokémon Showdown calls moveHit before Counter
1244
1228
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1245
- try options.chance.commit(player, .hit);
1246
1229
 
1247
1230
  const sub = showdown and foe.active.volatiles.Substitute;
1248
1231
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1270,20 +1253,20 @@ fn applyDamage(
1270
1253
  subbed.active.volatiles.substitute = 0;
1271
1254
  subbed.active.volatiles.Substitute = false;
1272
1255
  // battle.last_damage is not updated with the amount of HP the Substitute had
1273
- try options.log.end(battle.active(sub_player), .Substitute);
1274
- options.calc.capped(target_player);
1256
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1257
+ options.calc.capped(sub_player);
1275
1258
  return true;
1276
1259
  } else {
1277
1260
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1278
1261
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1279
- try options.log.activate(battle.active(sub_player), .Substitute);
1262
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1280
1263
  return false;
1281
1264
  }
1282
1265
  }
1283
1266
 
1284
1267
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1285
1268
  target.stored().hp -= battle.last_damage;
1286
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1269
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1287
1270
  return false;
1288
1271
  }
1289
1272
 
@@ -1302,7 +1285,7 @@ fn mirrorMove(
1302
1285
  side.last_selected_move = foe.last_used_move;
1303
1286
 
1304
1287
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1305
- try options.log.fail(battle.active(player), .None);
1288
+ try options.log.fail(.{ battle.active(player), .None });
1306
1289
  return null;
1307
1290
  } else if (!showdown or foe.last_used_move != .Struggle) {
1308
1291
  incrementPP(side, mslot);
@@ -1338,20 +1321,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1338
1321
  var immune = false;
1339
1322
  var mist = false;
1340
1323
 
1341
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1324
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1325
+ if (!showdown) try options.chance.commit(player, .hit);
1326
+ return true;
1327
+ }
1342
1328
 
1343
1329
  assert(!immune);
1344
1330
  if (mist) {
1345
1331
  assert(!showdown);
1346
1332
  const foe_ident = battle.active(player.foe());
1347
- try options.log.activate(foe_ident, .Mist);
1348
- try options.log.fail(foe_ident, .None);
1333
+ try options.log.activate(.{ foe_ident, .Mist });
1334
+ try options.log.fail(.{ foe_ident, .None });
1349
1335
  } else {
1350
- try options.chance.commit(player, .miss);
1351
- try options.log.lastmiss();
1352
- try options.log.miss(battle.active(player));
1336
+ try options.log.lastmiss(.{});
1337
+ try options.log.miss(.{battle.active(player)});
1353
1338
  }
1354
1339
 
1340
+ if (!showdown) try options.chance.commit(player, .miss);
1355
1341
  return false;
1356
1342
  }
1357
1343
 
@@ -1362,7 +1348,7 @@ fn moveHit(
1362
1348
  immune: *bool,
1363
1349
  mist: *bool,
1364
1350
  options: anytype,
1365
- ) bool {
1351
+ ) !bool {
1366
1352
  var side = battle.side(player);
1367
1353
  const foe = battle.foe(player);
1368
1354
 
@@ -1401,7 +1387,7 @@ fn moveHit(
1401
1387
  const overwritten = overwrite and state > 0;
1402
1388
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1389
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1390
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1391
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1392
  accuracy = accuracy * boost[0] / boost[1];
1407
1393
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1413,7 +1399,7 @@ fn moveHit(
1413
1399
  if (overwrite) side.active.volatiles.state = accuracy;
1414
1400
 
1415
1401
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1416
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1402
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1417
1403
  };
1418
1404
 
1419
1405
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1431,6 +1417,7 @@ fn moveHit(
1431
1417
  fn checkFaint(
1432
1418
  battle: anytype,
1433
1419
  player: Player,
1420
+ cap: bool,
1434
1421
  options: anytype,
1435
1422
  ) @TypeOf(options.log).Error!?Result {
1436
1423
  const side = battle.side(player);
@@ -1443,20 +1430,20 @@ fn checkFaint(
1443
1430
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
1431
  const more = player_out or foe_out;
1445
1432
 
1446
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1447
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1433
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1434
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1448
1435
 
1449
1436
  assert(!side.active.volatiles.MultiHit);
1450
1437
  assert(!foe.active.volatiles.MultiHit);
1451
1438
 
1452
1439
  if (player_out and foe_out) {
1453
- try options.log.tie();
1440
+ try options.log.tie(.{});
1454
1441
  return Result.Tie;
1455
1442
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1443
+ try options.log.win(.{player.foe()});
1457
1444
  return if (player == .P1) Result.Lose else Result.Win;
1458
1445
  } else if (foe_out) {
1459
- try options.log.win(player);
1446
+ try options.log.win(.{player});
1460
1447
  return if (player == .P1) Result.Win else Result.Lose;
1461
1448
  }
1462
1449
 
@@ -1465,7 +1452,7 @@ fn checkFaint(
1465
1452
  return .{ .p1 = foe_choice, .p2 = .Switch };
1466
1453
  }
1467
1454
 
1468
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1455
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1469
1456
  var side = battle.side(player);
1470
1457
  var foe = battle.foe(player);
1471
1458
  assert(side.stored().hp == 0);
@@ -1498,8 +1485,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1485
  side.stored().stats.spe;
1499
1486
  }
1500
1487
 
1501
- try options.log.faint(battle.active(player), done);
1502
- options.calc.capped(player);
1488
+ try options.log.faint(.{ battle.active(player), done });
1489
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1490
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1491
+ if (cap) options.calc.capped(player);
1503
1492
  return null;
1504
1493
  }
1505
1494
 
@@ -1520,8 +1509,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1509
 
1521
1510
  if (!showdown or damage > 0) {
1522
1511
  stored.hp -= damage;
1523
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1524
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1512
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1513
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1525
1514
  }
1526
1515
  }
1527
1516
 
@@ -1546,14 +1535,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1535
  const amount = @min(damage, stored.hp);
1547
1536
  stored.hp -= amount;
1548
1537
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1538
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1539
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1551
1540
 
1552
1541
  const before = foe_stored.hp;
1553
1542
  // Uncapped damage is added back to the foe
1554
1543
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1544
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1556
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1545
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1546
  }
1558
1547
  }
1559
1548
 
@@ -1564,18 +1553,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1553
  battle.turn += 1;
1565
1554
 
1566
1555
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1556
+ try options.log.tie(.{});
1568
1557
  return Result.Tie;
1569
1558
  }
1570
1559
 
1571
1560
  if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1561
+ try options.log.tie(.{});
1573
1562
  return Result.Tie;
1574
1563
  } else if (battle.turn >= 65535) {
1575
1564
  return Result.Error;
1576
1565
  }
1577
1566
 
1578
- try options.log.turn(battle.turn);
1567
+ try options.log.turn(.{battle.turn});
1579
1568
 
1580
1569
  return Result.Default;
1581
1570
  }
@@ -1645,7 +1634,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1634
  side.last_selected_move;
1646
1635
  if (m != .None and m == last) {
1647
1636
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1637
+ try options.log.cant(.{ ident, .Disable, last });
1649
1638
  return true;
1650
1639
  }
1651
1640
  }
@@ -1659,17 +1648,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1648
  }
1660
1649
  }
1661
1650
 
1662
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1663
- var volatiles = &active.volatiles;
1664
- assert(volatiles.Thrashing);
1665
- if (volatiles.attacks > 0) return false;
1666
-
1667
- volatiles.Thrashing = false;
1668
- volatiles.Confusion = true;
1669
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1670
- return true;
1671
- }
1672
-
1673
1651
  fn onBegin(
1674
1652
  battle: anytype,
1675
1653
  player: Player,
@@ -1678,7 +1656,6 @@ fn onBegin(
1678
1656
  residual: *bool,
1679
1657
  options: anytype,
1680
1658
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1659
  return switch (move.effect) {
1683
1660
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1661
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,7 +1678,6 @@ fn onBegin(
1701
1678
  }
1702
1679
 
1703
1680
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1705
1681
  return switch (move.effect) {
1706
1682
  .Bide => Effects.bide(battle, player, options),
1707
1683
  // zig fmt: off
@@ -1723,7 +1699,6 @@ fn alwaysHappens(
1723
1699
  residual: *bool,
1724
1700
  options: anytype,
1725
1701
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1702
  return switch (move.effect) {
1728
1703
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1704
  .Explode => Effects.explode(battle, player),
@@ -1759,10 +1734,12 @@ pub const Effects = struct {
1759
1734
 
1760
1735
  side.active.volatiles.Bide = true;
1761
1736
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1737
+
1762
1738
  side.active.volatiles.state = 0;
1763
1739
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1740
+ options.chance.durations(.bide, player, .started);
1764
1741
 
1765
- try options.log.start(battle.active(player), .Bide);
1742
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1743
  }
1767
1744
 
1768
1745
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1753,11 @@ pub const Effects = struct {
1776
1753
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1754
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1755
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1756
+ try options.log.curestatus(.{
1780
1757
  battle.active(player.foe()),
1781
1758
  foe_stored.status,
1782
1759
  .Message,
1783
- );
1760
+ });
1784
1761
  foe_stored.status = 0;
1785
1762
  }
1786
1763
  return;
@@ -1794,7 +1771,7 @@ pub const Effects = struct {
1794
1771
  foe_stored.status = Status.init(.BRN);
1795
1772
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1773
 
1797
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1774
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1798
1775
  }
1799
1776
 
1800
1777
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1781,8 @@ pub const Effects = struct {
1804
1781
  volatiles.Charging = true;
1805
1782
  const move = side.last_selected_move;
1806
1783
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1784
+ try options.log.laststill(.{});
1785
+ try options.log.prepare(.{ battle.active(player), move });
1809
1786
  }
1810
1787
 
1811
1788
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,42 +1793,38 @@ pub const Effects = struct {
1816
1793
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1794
  } else {
1818
1795
  if (showdown) {
1819
- if (!try checkHit(battle, player, move, options)) {
1820
- return;
1821
- } else if (sub) {
1822
- return options.log.fail(battle.active(player.foe()), .None);
1823
- }
1796
+ if (!try checkHit(battle, player, move, options)) return;
1797
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1798
  } else {
1825
- if (sub) {
1826
- return options.log.fail(battle.active(player.foe()), .None);
1827
- } else if (!try checkHit(battle, player, move, options)) {
1828
- return;
1829
- }
1799
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1800
+ if (!try checkHit(battle, player, move, options)) return;
1830
1801
  }
1831
- try options.chance.commit(player, .hit);
1832
1802
  }
1833
1803
 
1834
1804
  if (foe.active.volatiles.Confusion) return;
1835
1805
  foe.active.volatiles.Confusion = true;
1836
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1806
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1807
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1808
+ options.chance.durations(.confusion, player.foe(), .started);
1809
+
1810
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1811
  }
1840
1812
 
1841
1813
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1814
  const foe = battle.foe(player);
1843
1815
 
1844
1816
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1817
+ try options.log.lastmiss(.{});
1818
+ return options.log.miss(.{battle.active(player)});
1847
1819
  }
1848
1820
 
1849
1821
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1822
+ return options.log.start(.{
1851
1823
  battle.active(player),
1824
+ .TypeChange,
1852
1825
  foe.active.types,
1853
1826
  battle.active(player.foe()),
1854
- );
1827
+ });
1855
1828
  }
1856
1829
 
1857
1830
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1860,13 +1833,10 @@ pub const Effects = struct {
1860
1833
  const foe_ident = battle.active(player.foe());
1861
1834
 
1862
1835
  // Pokémon Showdown handles hit/miss earlier in doMove
1863
- if (!showdown) {
1864
- if (!try checkHit(battle, player, move, options)) return null;
1865
- try options.chance.commit(player, .hit);
1866
- }
1836
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1867
1837
 
1868
1838
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1839
+ try options.log.fail(.{ foe_ident, .None });
1870
1840
  return null;
1871
1841
  }
1872
1842
 
@@ -1882,7 +1852,7 @@ pub const Effects = struct {
1882
1852
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1853
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
1854
  if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1855
+ try options.log.fail(.{ foe_ident, .None });
1886
1856
  return null;
1887
1857
  } else if (err) {
1888
1858
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1893,14 +1863,16 @@ pub const Effects = struct {
1893
1863
  volatiles.disable_move =
1894
1864
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1895
1865
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1866
+ options.chance.durations(.disable, player.foe(), .started);
1896
1867
 
1897
1868
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1869
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1870
  return null;
1900
1871
  }
1901
1872
 
1902
1873
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1874
  var stored = battle.side(player).stored();
1875
+ const ident = battle.active(player);
1904
1876
 
1905
1877
  if (battle.last_damage == 0) {
1906
1878
  assert(showdown);
@@ -1913,7 +1885,7 @@ pub const Effects = struct {
1913
1885
  if (stored.hp == stored.stats.hp) return;
1914
1886
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1887
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1888
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1889
  }
1918
1890
 
1919
1891
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +1917,7 @@ pub const Effects = struct {
1945
1917
  if (side.active.volatiles.FocusEnergy) return;
1946
1918
  side.active.volatiles.FocusEnergy = true;
1947
1919
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
1920
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
1921
  }
1950
1922
 
1951
1923
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1967,7 +1939,7 @@ pub const Effects = struct {
1967
1939
  foe_stored.status = Status.init(.FRZ);
1968
1940
  // GLITCH: Hyper Beam recharging status is not cleared
1969
1941
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
1942
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
1943
  }
1972
1944
 
1973
1945
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1975,7 +1947,7 @@ pub const Effects = struct {
1975
1947
  var side = battle.side(player);
1976
1948
  var foe = battle.foe(player);
1977
1949
 
1978
- var side_stored = side.stored();
1950
+ const side_stored = side.stored();
1979
1951
  var foe_stored = foe.stored();
1980
1952
 
1981
1953
  const player_ident = battle.active(player);
@@ -1987,8 +1959,8 @@ pub const Effects = struct {
1987
1959
  side.active.stats = side_stored.stats;
1988
1960
  foe.active.stats = foe_stored.stats;
1989
1961
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
1962
+ try log.activate(.{ player_ident, .Haze });
1963
+ try log.clearallboost(.{});
1992
1964
 
1993
1965
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
1966
  if (showdown) {
@@ -1996,24 +1968,26 @@ pub const Effects = struct {
1996
1968
  const p: Player = @enumFromInt(i);
1997
1969
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
1970
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1971
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
1972
  s.stored().status = 0;
1973
+ options.chance.durations(.sleep, p, .None);
2001
1974
  } else if (showdown and s.stored().status == Status.TOX) {
2002
1975
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
1976
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
1977
  }
2005
- try clearVolatiles(battle, p, p != player, options);
1978
+ try clearVolatiles(battle, p, options);
2006
1979
  }
2007
1980
  } else {
2008
1981
  if (Status.any(foe_stored.status)) {
2009
1982
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
1983
  foe.last_selected_move = .SKIP_TURN;
1984
+ options.chance.durations(.sleep, player.foe(), .None);
2011
1985
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
1987
  foe_stored.status = 0;
2014
1988
  }
2015
- try clearVolatiles(battle, player, false, options);
2016
- try clearVolatiles(battle, player.foe(), true, options);
1989
+ try clearVolatiles(battle, player, options);
1990
+ try clearVolatiles(battle, player.foe(), options);
2017
1991
  }
2018
1992
  }
2019
1993
 
@@ -2025,7 +1999,7 @@ pub const Effects = struct {
2025
1999
  // GLITCH: HP recovery move failure glitches
2026
2000
  const delta = stored.stats.hp - stored.hp;
2027
2001
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return try options.log.fail(ident, .None);
2002
+ return options.log.fail(.{ ident, .None });
2029
2003
  }
2030
2004
 
2031
2005
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2007,17 @@ pub const Effects = struct {
2033
2007
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2008
  if (showdown) battle.rng.advance(1);
2035
2009
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2010
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2011
  stored.hp = stored.stats.hp;
2038
2012
  } else {
2039
2013
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2014
  }
2041
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2015
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2042
2016
  }
2043
2017
 
2044
2018
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2019
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2020
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2021
  }
2048
2022
 
2049
2023
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2026,31 @@ pub const Effects = struct {
2052
2026
  if (showdown) {
2053
2027
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2028
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2029
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2030
  }
2057
2031
  if (!try checkHit(battle, player, move, options)) return;
2058
2032
  if (foe.active.volatiles.LeechSeed) return;
2059
2033
  } else {
2060
2034
  if (!try checkHit(battle, player, move, options)) return;
2061
2035
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2036
+ try options.log.lastmiss(.{});
2037
+ return options.log.miss(.{battle.active(player)});
2064
2038
  }
2065
2039
  }
2066
2040
 
2067
- try options.chance.commit(player, .hit);
2068
2041
  foe.active.volatiles.LeechSeed = true;
2069
2042
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2043
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2044
  }
2072
2045
 
2073
2046
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2047
  var side = battle.side(player);
2048
+ const ident = battle.active(player);
2075
2049
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2050
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2051
  side.active.volatiles.LightScreen = true;
2080
2052
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2053
+ try options.log.start(.{ ident, .LightScreen });
2082
2054
  }
2083
2055
 
2084
2056
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2106,17 +2078,15 @@ pub const Effects = struct {
2106
2078
  if (!has_mimic) {
2107
2079
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2080
  if (!try checkHit(battle, player, move, options)) return;
2109
- try options.chance.commit(player, .hit);
2110
- return try options.log.fail(battle.active(player.foe()), .None);
2081
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2111
2082
  }
2112
2083
  }
2113
2084
  if (!try checkHit(battle, player, move, options)) return;
2114
- try options.chance.commit(player, .hit);
2115
2085
 
2116
2086
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2087
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2088
 
2119
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2089
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2120
2090
  }
2121
2091
 
2122
2092
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2095,7 @@ pub const Effects = struct {
2125
2095
  if (side.active.volatiles.Mist) return;
2126
2096
  side.active.volatiles.Mist = true;
2127
2097
 
2128
- try options.log.start(battle.active(player), .Mist);
2098
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2099
  }
2130
2100
 
2131
2101
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,25 +2121,25 @@ pub const Effects = struct {
2151
2121
 
2152
2122
  if (showdown) {
2153
2123
  // Invulnerability trumps type immunity on Pokémon Showdown
2154
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2155
- if (!try checkHit(battle, player, move, options)) return;
2124
+ if (immune and !foe.active.volatiles.Invulnerable) {
2125
+ return log.immune(.{ foe_ident, .None });
2126
+ } else if (!try checkHit(battle, player, move, options)) return;
2156
2127
  }
2157
2128
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2129
+ return log.fail(.{
2159
2130
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2131
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2132
+ });
2162
2133
  }
2163
2134
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2135
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2136
  if (!try checkHit(battle, player, move, options)) return;
2166
2137
  }
2167
- try options.chance.commit(player, .hit);
2168
2138
 
2169
2139
  foe_stored.status = Status.init(.PAR);
2170
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2141
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2142
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2143
  }
2174
2144
 
2175
2145
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2161,12 @@ pub const Effects = struct {
2191
2161
  foe_stored.status = Status.init(.PAR);
2192
2162
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2163
 
2194
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2164
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2195
2165
  }
2196
2166
 
2197
2167
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2168
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2169
+ try options.log.fieldactivate(.{});
2200
2170
  }
2201
2171
  }
2202
2172
 
@@ -2211,28 +2181,27 @@ pub const Effects = struct {
2211
2181
  return;
2212
2182
  } else if (foe.active.volatiles.Substitute) {
2213
2183
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2184
+ return log.fail(.{ foe_ident, .None });
2215
2185
  } else if (Status.any(foe_stored.status)) {
2216
2186
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2187
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2188
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2219
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2189
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2190
  if (!showdown)
2221
- .Poison
2191
+ Fail.Poison
2222
2192
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2193
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2194
  else
2225
- .None
2195
+ Fail.None
2226
2196
  else
2227
- .None);
2197
+ Fail.None });
2228
2198
  } else if (foe.active.types.includes(.Poison)) {
2229
2199
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2200
+ return log.immune(.{ foe_ident, .None });
2231
2201
  }
2232
2202
 
2233
2203
  if (move.effect == .Poison) {
2234
2204
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2235
- try options.chance.commit(player, .hit);
2236
2205
  } else {
2237
2206
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2238
2207
  return;
@@ -2246,7 +2215,7 @@ pub const Effects = struct {
2246
2215
  foe.active.volatiles.toxic = 0;
2247
2216
  }
2248
2217
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2218
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2219
  }
2251
2220
 
2252
2221
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2235,23 @@ pub const Effects = struct {
2266
2235
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2236
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2237
 
2269
- try options.log.damageOf(
2238
+ try options.log.damage(.{
2270
2239
  battle.active(player),
2271
2240
  stored,
2272
2241
  .RecoilOf,
2273
2242
  battle.active(player.foe()),
2274
- );
2243
+ });
2275
2244
  if (showdown and stored.hp == 0) residual.* = false;
2276
2245
  }
2277
2246
 
2278
2247
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2248
  var side = battle.side(player);
2249
+ const ident = battle.active(player);
2280
2250
 
2281
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2251
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2282
2252
  side.active.volatiles.Reflect = true;
2283
2253
 
2284
- try options.log.start(battle.active(player), .Reflect);
2254
+ try options.log.start(.{ ident, .Reflect });
2285
2255
  }
2286
2256
 
2287
2257
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,14 +2265,13 @@ pub const Effects = struct {
2295
2265
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2266
  } else {
2297
2267
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2268
+ return options.log.fail(.{
2299
2269
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2270
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2271
+ });
2302
2272
  }
2303
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2273
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2304
2274
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2305
- try options.chance.commit(player, .hit);
2306
2275
  }
2307
2276
 
2308
2277
  // Sleep Clause Mod
@@ -2314,19 +2283,20 @@ pub const Effects = struct {
2314
2283
  foe.active.volatiles.Recharging = false;
2315
2284
 
2316
2285
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2286
+ options.chance.durations(.sleep, player.foe(), .started);
2287
+
2317
2288
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2289
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2290
  }
2320
2291
 
2321
2292
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2293
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2294
  }
2324
2295
 
2325
2296
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2297
  var side = battle.side(player);
2327
2298
  if (side.active.volatiles.Substitute) {
2328
- try options.log.fail(battle.active(player), .Substitute);
2329
- return;
2299
+ return options.log.fail(.{ battle.active(player), .Substitute });
2330
2300
  }
2331
2301
 
2332
2302
  assert(side.stored().stats.hp <= 1023);
@@ -2338,17 +2308,16 @@ pub const Effects = struct {
2338
2308
  // avoid having to convert to floating point)
2339
2309
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2340
2310
  if (side.stored().hp < required_hp) {
2341
- try options.log.fail(battle.active(player), .Weak);
2342
- return;
2311
+ return options.log.fail(.{ battle.active(player), .Weak });
2343
2312
  }
2344
2313
 
2345
2314
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2346
2315
  side.stored().hp -= hp;
2347
2316
  side.active.volatiles.substitute = hp + 1;
2348
2317
  side.active.volatiles.Substitute = true;
2349
- try options.log.start(battle.active(player), .Substitute);
2318
+ try options.log.start(.{ battle.active(player), .Substitute });
2350
2319
  if (hp > 0) {
2351
- try options.log.damage(battle.active(player), side.stored(), .None);
2320
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2352
2321
  if (showdown and side.stored().hp == 0) residual.* = false;
2353
2322
  }
2354
2323
  }
@@ -2357,13 +2326,11 @@ pub const Effects = struct {
2357
2326
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2358
2327
  if (showdown) {
2359
2328
  if (battle.side(player).last_selected_move == .Teleport) return;
2360
- if (try checkHit(battle, player, move, options)) {
2361
- try options.chance.commit(player, .hit);
2362
- }
2329
+ _ = try checkHit(battle, player, move, options);
2363
2330
  battle.last_damage = 0;
2364
2331
  } else {
2365
- try options.log.fail(battle.active(player), .None);
2366
- try options.log.laststill();
2332
+ try options.log.fail(.{ battle.active(player), .None });
2333
+ try options.log.laststill(.{});
2367
2334
  }
2368
2335
  }
2369
2336
 
@@ -2374,6 +2341,8 @@ pub const Effects = struct {
2374
2341
 
2375
2342
  volatiles.Thrashing = true;
2376
2343
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2344
+
2345
+ options.chance.durations(.thrashing, player, .started);
2377
2346
  }
2378
2347
 
2379
2348
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2402,7 +2371,7 @@ pub const Effects = struct {
2402
2371
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2403
2372
  }
2404
2373
 
2405
- try options.log.transform(battle.active(player), foe_ident);
2374
+ try options.log.transform(.{ battle.active(player), foe_ident });
2406
2375
  }
2407
2376
 
2408
2377
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2419,6 +2388,7 @@ pub const Effects = struct {
2419
2388
 
2420
2389
  side.active.volatiles.attacks =
2421
2390
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2391
+ options.chance.durations(.binding, player, .started);
2422
2392
  }
2423
2393
 
2424
2394
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2432,87 +2402,87 @@ pub const Effects = struct {
2432
2402
  switch (move.effect) {
2433
2403
  .AttackUp1, .AttackUp2, .Rage => {
2434
2404
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2435
- if (boosts.atk == 6) return try log.fail(ident, .None);
2405
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2436
2406
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2437
2407
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2438
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2408
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2439
2409
  if (stats.atk == MAX_STAT_VALUE) {
2440
2410
  boosts.atk -= 1;
2441
2411
  if (showdown) {
2442
- try log.boost(ident, reason, n);
2443
- try log.boost(ident, Boost.Attack, -1);
2412
+ try log.boost(.{ ident, reason, n });
2413
+ try log.boost(.{ ident, .Attack, -1 });
2444
2414
  if (move.effect == .Rage) return;
2445
2415
  }
2446
- return try log.fail(ident, .None);
2416
+ return log.fail(.{ ident, .None });
2447
2417
  }
2448
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2418
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2449
2419
  const stat = unmodifiedStats(battle, side).atk;
2450
2420
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2451
- try log.boost(ident, reason, n);
2421
+ try log.boost(.{ ident, reason, n });
2452
2422
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2453
2423
  if (showdown and move.effect == .Rage) return;
2454
2424
  },
2455
2425
  .DefenseUp1, .DefenseUp2 => {
2456
2426
  assert(boosts.def >= -6 and boosts.def <= 6);
2457
- if (boosts.def == 6) return try log.fail(ident, .None);
2427
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2458
2428
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2459
2429
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2460
2430
  if (stats.def == MAX_STAT_VALUE) {
2461
2431
  boosts.def -= 1;
2462
2432
  if (showdown) {
2463
- try log.boost(ident, .Defense, n);
2464
- try log.boost(ident, .Defense, -1);
2433
+ try log.boost(.{ ident, .Defense, n });
2434
+ try log.boost(.{ ident, .Defense, -1 });
2465
2435
  }
2466
- return try log.fail(ident, .None);
2436
+ return log.fail(.{ ident, .None });
2467
2437
  }
2468
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2438
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2469
2439
  const stat = unmodifiedStats(battle, side).def;
2470
2440
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2471
- try log.boost(ident, .Defense, n);
2441
+ try log.boost(.{ ident, .Defense, n });
2472
2442
  },
2473
2443
  .SpeedUp2 => {
2474
2444
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2475
- if (boosts.spe == 6) return try log.fail(ident, .None);
2445
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2476
2446
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2477
2447
  if (stats.spe == MAX_STAT_VALUE) {
2478
2448
  boosts.spe -= 1;
2479
2449
  if (showdown) {
2480
- try log.boost(ident, .Speed, 2);
2481
- try log.boost(ident, .Speed, -1);
2450
+ try log.boost(.{ ident, .Speed, 2 });
2451
+ try log.boost(.{ ident, .Speed, -1 });
2482
2452
  }
2483
- return try log.fail(ident, .None);
2453
+ return log.fail(.{ ident, .None });
2484
2454
  }
2485
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2455
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2486
2456
  const stat = unmodifiedStats(battle, side).spe;
2487
2457
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2488
- try log.boost(ident, .Speed, 2);
2458
+ try log.boost(.{ ident, .Speed, 2 });
2489
2459
  },
2490
2460
  .SpecialUp1, .SpecialUp2 => {
2491
2461
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2492
- if (boosts.spc == 6) return try log.fail(ident, .None);
2462
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2493
2463
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2494
2464
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2495
2465
  if (stats.spc == MAX_STAT_VALUE) {
2496
2466
  boosts.spc -= 1;
2497
2467
  if (showdown) {
2498
- try log.boost(ident, .SpecialAttack, n);
2499
- try log.boost(ident, .SpecialAttack, -1);
2500
- try log.boost(ident, .SpecialDefense, n);
2501
- try log.boost(ident, .SpecialDefense, -1);
2468
+ try log.boost(.{ ident, .SpecialAttack, n });
2469
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2470
+ try log.boost(.{ ident, .SpecialDefense, n });
2471
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2502
2472
  }
2503
- return try log.fail(ident, .None);
2473
+ return log.fail(.{ ident, .None });
2504
2474
  }
2505
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2475
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2506
2476
  const stat = unmodifiedStats(battle, side).spc;
2507
2477
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2508
- try log.boost(ident, .SpecialAttack, n);
2509
- try log.boost(ident, .SpecialDefense, n);
2478
+ try log.boost(.{ ident, .SpecialAttack, n });
2479
+ try log.boost(.{ ident, .SpecialDefense, n });
2510
2480
  },
2511
2481
  .EvasionUp1 => {
2512
2482
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2513
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2483
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2514
2484
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2515
- try log.boost(ident, .Evasion, 1);
2485
+ try log.boost(.{ ident, .Evasion, 1 });
2516
2486
  },
2517
2487
  else => unreachable,
2518
2488
  }
@@ -2527,7 +2497,7 @@ pub const Effects = struct {
2527
2497
  const foe_ident = battle.active(player.foe());
2528
2498
  const secondary = move.effect.isStatDownChance();
2529
2499
 
2530
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2500
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2531
2501
 
2532
2502
  if (secondary) {
2533
2503
  const proc = try Rolls.unboost(battle, player, options);
@@ -2535,7 +2505,6 @@ pub const Effects = struct {
2535
2505
  } else {
2536
2506
  // checkHit already checks for Invulnerable
2537
2507
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2538
- try options.chance.commit(player, .hit);
2539
2508
  }
2540
2509
 
2541
2510
  var stats = &foe.active.stats;
@@ -2545,82 +2514,82 @@ pub const Effects = struct {
2545
2514
  switch (move.effect) {
2546
2515
  .AttackDown1, .AttackDownChance => {
2547
2516
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2548
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2517
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2549
2518
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2550
2519
  if (stats.atk == 1) {
2551
2520
  boosts.atk += 1;
2552
2521
  if (showdown) {
2553
- try log.boost(foe_ident, .Attack, -1);
2554
- try log.boost(foe_ident, .Attack, 1);
2522
+ try log.boost(.{ foe_ident, .Attack, -1 });
2523
+ try log.boost(.{ foe_ident, .Attack, 1 });
2555
2524
  }
2556
- return try log.fail(foe_ident, .None);
2525
+ return log.fail(.{ foe_ident, .None });
2557
2526
  }
2558
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2527
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2559
2528
  const stat = unmodifiedStats(battle, foe).atk;
2560
2529
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2561
- try log.boost(foe_ident, .Attack, -1);
2530
+ try log.boost(.{ foe_ident, .Attack, -1 });
2562
2531
  },
2563
2532
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2564
2533
  assert(boosts.def >= -6 and boosts.def <= 6);
2565
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2534
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2566
2535
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2567
2536
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2568
2537
  if (stats.def == 1) {
2569
2538
  boosts.def += 1;
2570
2539
  if (showdown) {
2571
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2572
- try log.boost(foe_ident, .Defense, 1);
2540
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2541
+ try log.boost(.{ foe_ident, .Defense, 1 });
2573
2542
  }
2574
- return try log.fail(foe_ident, .None);
2543
+ return log.fail(.{ foe_ident, .None });
2575
2544
  }
2576
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2545
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2577
2546
  const stat = unmodifiedStats(battle, foe).def;
2578
2547
  stats.def = @max(1, stat * mod[0] / mod[1]);
2579
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2548
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2580
2549
  },
2581
2550
  .SpeedDown1, .SpeedDownChance => {
2582
2551
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2583
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2552
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2584
2553
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2585
2554
  if (stats.spe == 1) {
2586
2555
  boosts.spe += 1;
2587
2556
  if (showdown) {
2588
- try log.boost(foe_ident, .Speed, -1);
2589
- try log.boost(foe_ident, .Speed, 1);
2557
+ try log.boost(.{ foe_ident, .Speed, -1 });
2558
+ try log.boost(.{ foe_ident, .Speed, 1 });
2590
2559
  }
2591
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2592
2561
  }
2593
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2594
2563
  const stat = unmodifiedStats(battle, foe).spe;
2595
2564
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2596
- try log.boost(foe_ident, .Speed, -1);
2565
+ try log.boost(.{ foe_ident, .Speed, -1 });
2597
2566
  assert(boosts.spe >= -6);
2598
2567
  },
2599
2568
  .SpecialDownChance => {
2600
2569
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2601
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2570
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2571
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2603
2572
  if (stats.spc == 1) {
2604
2573
  boosts.spc += 1;
2605
2574
  if (showdown) {
2606
- try log.boost(foe_ident, .SpecialAttack, -1);
2607
- try log.boost(foe_ident, .SpecialAttack, 1);
2608
- try log.boost(foe_ident, .SpecialDefense, -1);
2609
- try log.boost(foe_ident, .SpecialDefense, 1);
2575
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2576
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2577
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2578
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2610
2579
  }
2611
- return try log.fail(foe_ident, .None);
2580
+ return log.fail(.{ foe_ident, .None });
2612
2581
  }
2613
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2582
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2614
2583
  const stat = unmodifiedStats(battle, foe).spc;
2615
2584
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2616
- try log.boost(foe_ident, .SpecialAttack, -1);
2617
- try log.boost(foe_ident, .SpecialDefense, -1);
2585
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2586
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2618
2587
  },
2619
2588
  .AccuracyDown1 => {
2620
2589
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2621
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2590
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2622
2591
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2623
- try log.boost(foe_ident, .Accuracy, -1);
2592
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2624
2593
  },
2625
2594
  else => unreachable,
2626
2595
  }
@@ -2649,35 +2618,35 @@ fn isForced(active: anytype) bool {
2649
2618
  active.volatiles.Thrashing or active.volatiles.Charging;
2650
2619
  }
2651
2620
 
2652
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2621
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2653
2622
  var log = options.log;
2654
2623
  var side = battle.side(who);
2655
2624
  var volatiles = &side.active.volatiles;
2656
2625
  const ident = battle.active(who);
2657
2626
 
2658
- options.chance.clearDurations(who, clear);
2659
-
2660
2627
  if (volatiles.disable_move != 0) {
2661
2628
  volatiles.disable_move = 0;
2662
2629
  volatiles.disable_duration = 0;
2663
- try log.end(ident, .DisableSilent);
2630
+ options.chance.durations(.disable, who, .None);
2631
+ try log.end(.{ ident, .DisableSilent });
2664
2632
  }
2665
2633
  if (volatiles.Confusion) {
2666
2634
  // volatiles.confusion is left unchanged
2667
2635
  volatiles.Confusion = false;
2668
- try log.end(ident, .ConfusionSilent);
2636
+ options.chance.durations(.confusion, who, .None);
2637
+ try log.end(.{ ident, .ConfusionSilent });
2669
2638
  }
2670
2639
  if (volatiles.Mist) {
2671
2640
  volatiles.Mist = false;
2672
- try log.end(ident, .MistSilent);
2641
+ try log.end(.{ ident, .MistSilent });
2673
2642
  }
2674
2643
  if (volatiles.FocusEnergy) {
2675
2644
  volatiles.FocusEnergy = false;
2676
- try log.end(ident, .FocusEnergySilent);
2645
+ try log.end(.{ ident, .FocusEnergySilent });
2677
2646
  }
2678
2647
  if (volatiles.LeechSeed) {
2679
2648
  volatiles.LeechSeed = false;
2680
- try log.end(ident, .LeechSeedSilent);
2649
+ try log.end(.{ ident, .LeechSeedSilent });
2681
2650
  }
2682
2651
  if (!showdown and volatiles.Toxic) {
2683
2652
  volatiles.Toxic = false;
@@ -2685,24 +2654,30 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2654
  }
2686
2655
  if (volatiles.LightScreen) {
2687
2656
  volatiles.LightScreen = false;
2688
- try log.end(ident, .LightScreenSilent);
2657
+ try log.end(.{ ident, .LightScreenSilent });
2689
2658
  }
2690
2659
  if (volatiles.Reflect) {
2691
2660
  volatiles.Reflect = false;
2692
- try log.end(ident, .ReflectSilent);
2661
+ try log.end(.{ ident, .ReflectSilent });
2693
2662
  }
2694
2663
  if (showdown and volatiles.Toxic) {
2695
2664
  volatiles.Toxic = false;
2696
2665
  // Pokémon Showdown erroneously clears the toxic counter
2697
2666
  volatiles.toxic = 0;
2698
- try log.end(ident, .ToxicSilent);
2667
+ try log.end(.{ ident, .ToxicSilent });
2699
2668
  }
2700
2669
  }
2701
2670
 
2671
+ fn observation(n: anytype) optional.Optional(chance.Observation) {
2672
+ return if (n == 0) .ended else .continuing;
2673
+ }
2674
+
2702
2675
  pub const Rolls = struct {
2703
2676
  fn speedTie(battle: anytype, options: anytype) !bool {
2704
2677
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2705
2678
  player == .P1
2679
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2680
+ battle.rng.speedTie()
2706
2681
  else if (showdown)
2707
2682
  battle.rng.range(u8, 0, 2) == 0
2708
2683
  else
@@ -2712,21 +2687,31 @@ pub const Rolls = struct {
2712
2687
  return p1;
2713
2688
  }
2714
2689
 
2715
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2690
+ fn criticalHit(
2691
+ battle: anytype,
2692
+ player: Player,
2693
+ rate: u8,
2694
+ spurious: bool,
2695
+ options: anytype,
2696
+ ) !bool {
2716
2697
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2717
2698
  val == .true
2699
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2700
+ battle.rng.criticalHit(player, rate)
2718
2701
  else if (showdown)
2719
2702
  battle.rng.chance(u8, rate, 256)
2720
2703
  else
2721
2704
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2722
2705
 
2723
- try options.chance.criticalHit(player, crit, rate);
2706
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2724
2707
  return crit;
2725
2708
  }
2726
2709
 
2727
2710
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2728
2711
  const roll = if (options.calc.overridden(player, .damage)) |val|
2729
2712
  val
2713
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2714
+ battle.rng.damage(player)
2730
2715
  else roll: {
2731
2716
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2732
2717
  while (true) {
@@ -2740,21 +2725,27 @@ pub const Rolls = struct {
2740
2725
  return roll;
2741
2726
  }
2742
2727
 
2743
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2728
+ const uN = if (pkmn.options.miss) u8 else u9;
2729
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2730
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2744
2731
  const ok = if (options.calc.overridden(player, .hit)) |val|
2745
2732
  val == .true
2733
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2734
+ battle.rng.hit(player, acc)
2746
2735
  else if (showdown)
2747
- battle.rng.chance(u8, accuracy, 256)
2736
+ battle.rng.chance(uN, acc, 256)
2748
2737
  else
2749
- battle.rng.next() < accuracy;
2738
+ battle.rng.next() < acc;
2750
2739
 
2751
- options.chance.hit(ok, accuracy);
2740
+ try options.chance.hit(player, ok, acc);
2752
2741
  return ok;
2753
2742
  }
2754
2743
 
2755
2744
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2756
2745
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2757
2746
  val == .true
2747
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2748
+ battle.rng.confused(player)
2758
2749
  else if (showdown)
2759
2750
  !battle.rng.chance(u8, 128, 256)
2760
2751
  else
@@ -2767,6 +2758,8 @@ pub const Rolls = struct {
2767
2758
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2768
2759
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2769
2760
  val == .true
2761
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2762
+ battle.rng.paralyzed(player)
2770
2763
  else if (showdown)
2771
2764
  battle.rng.chance(u8, 63, 256)
2772
2765
  else
@@ -2779,6 +2772,8 @@ pub const Rolls = struct {
2779
2772
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2780
2773
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2774
  val == .true
2775
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2776
+ battle.rng.secondaryChance(player, 25)
2782
2777
  else if (showdown)
2783
2778
  battle.rng.chance(u8, 25, 256)
2784
2779
  else
@@ -2793,6 +2788,8 @@ pub const Rolls = struct {
2793
2788
 
2794
2789
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2790
  val == .true
2791
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2792
+ battle.rng.secondaryChance(player, rate)
2796
2793
  else if (showdown)
2797
2794
  battle.rng.chance(u8, rate, 256)
2798
2795
  else
@@ -2807,6 +2804,8 @@ pub const Rolls = struct {
2807
2804
 
2808
2805
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2809
2806
  val == .true
2807
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2808
+ battle.rng.secondaryChance(player, rate)
2810
2809
  else if (showdown)
2811
2810
  battle.rng.chance(u8, rate, 256)
2812
2811
  else
@@ -2819,6 +2818,8 @@ pub const Rolls = struct {
2819
2818
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2820
2819
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2821
2820
  val == .true
2821
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2822
+ battle.rng.secondaryChance(player, 85)
2822
2823
  else if (showdown)
2823
2824
  battle.rng.chance(u8, 85, 256)
2824
2825
  else
@@ -2831,6 +2832,8 @@ pub const Rolls = struct {
2831
2832
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2832
2833
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2833
2834
  val
2835
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2836
+ battle.rng.metronome(player)
2834
2837
  else if (showdown)
2835
2838
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
2839
  else move: {
@@ -2849,6 +2852,8 @@ pub const Rolls = struct {
2849
2852
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2850
2853
  const power = if (options.calc.overridden(player, .psywave)) |val|
2851
2854
  val - 1
2855
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2856
+ battle.rng.psywave(player)
2852
2857
  else if (showdown)
2853
2858
  battle.rng.range(u8, 0, max)
2854
2859
  else power: {
@@ -2866,6 +2871,8 @@ pub const Rolls = struct {
2866
2871
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2867
2872
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2868
2873
  @intCast(val)
2874
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2875
+ battle.rng.sleepDuration(player)
2869
2876
  else if (showdown)
2870
2877
  battle.rng.range(u3, 1, 8)
2871
2878
  else duration: {
@@ -2876,46 +2883,52 @@ pub const Rolls = struct {
2876
2883
  };
2877
2884
 
2878
2885
  assert(duration >= 1 and duration <= 7);
2879
- options.chance.duration(.sleep, player, player.foe(), duration);
2886
+ options.chance.duration(player, duration);
2880
2887
  return duration;
2881
2888
  }
2882
2889
 
2883
2890
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2884
2891
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2885
2892
  val
2893
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2894
+ battle.rng.disableDuration(player)
2886
2895
  else if (showdown)
2887
2896
  battle.rng.range(u4, 1, 9)
2888
2897
  else
2889
2898
  @intCast((battle.rng.next() & 7) + 1);
2890
2899
 
2891
2900
  assert(duration >= 1 and duration <= 8);
2892
- options.chance.duration(.disable, player, player.foe(), duration);
2901
+ options.chance.duration(player, duration);
2893
2902
  return duration;
2894
2903
  }
2895
2904
 
2896
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2905
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2897
2906
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2898
2907
  @intCast(val)
2908
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2909
+ battle.rng.confusionDuration(player)
2899
2910
  else if (showdown)
2900
2911
  battle.rng.range(u3, 2, 6)
2901
2912
  else
2902
2913
  @intCast((battle.rng.next() & 3) + 2);
2903
2914
 
2904
2915
  assert(duration >= 2 and duration <= 5);
2905
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2916
+ options.chance.duration(player, duration);
2906
2917
  return duration;
2907
2918
  }
2908
2919
 
2909
2920
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2910
2921
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2911
2922
  @intCast(val)
2923
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2924
+ battle.rng.attackingDuration(player)
2912
2925
  else if (showdown)
2913
2926
  battle.rng.range(u3, 2, 4)
2914
2927
  else
2915
2928
  @intCast((battle.rng.next() & 1) + 2);
2916
2929
 
2917
2930
  assert(duration >= 2 and duration <= 3);
2918
- options.chance.duration(.attacking, player, player, duration);
2931
+ options.chance.duration(player, duration);
2919
2932
  return duration;
2920
2933
  }
2921
2934
 
@@ -2930,6 +2943,8 @@ pub const Rolls = struct {
2930
2943
  const roll = if (effect == .Binding) .duration else .multi_hit;
2931
2944
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2932
2945
  @intCast(val)
2946
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2947
+ battle.rng.distribution(player)
2933
2948
  else if (showdown)
2934
2949
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2935
2950
  else n: {
@@ -2939,7 +2954,7 @@ pub const Rolls = struct {
2939
2954
 
2940
2955
  assert(n >= 2 and n <= 5);
2941
2956
  if (effect == .Binding) {
2942
- options.chance.duration(.binding, player, player, n);
2957
+ options.chance.duration(player, n);
2943
2958
  } else {
2944
2959
  try options.chance.multiHit(player, n);
2945
2960
  }
@@ -2962,7 +2977,9 @@ pub const Rolls = struct {
2962
2977
  else
2963
2978
  null;
2964
2979
 
2965
- const slot: u4 = overridden orelse slot: {
2980
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2981
+ battle.rng.moveSlot(player, moves, check_pp)
2982
+ else slot: {
2966
2983
  if (showdown) {
2967
2984
  if (check_pp == 0) {
2968
2985
  var i: usize = moves.len;
@@ -3042,11 +3059,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3042
3059
  const side = battle.side(player);
3043
3060
  const foe = battle.foe(player);
3044
3061
 
3045
- var active = &side.active;
3062
+ const active = &side.active;
3046
3063
  const stored = side.stored();
3047
3064
 
3048
3065
  // While players are not given any input options on the cartridge in these cases,
3049
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3066
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3050
3067
  //
3051
3068
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3052
3069
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3054,7 +3071,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3054
3071
  // passing due to being forced into using a move. Instead of introducing another option
3055
3072
  // we simply repurpose Move with no move slot, even though pedantically this is not
3056
3073
  // strictly correct as the player would not have been presented the option to move or
3057
- // switch at all.
3074
+ // switch at all
3058
3075
  if (isForced(active)) {
3059
3076
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3060
3077
  n += 1;
@@ -3073,7 +3090,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3073
3090
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3074
3091
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3075
3092
  // either disables all other moves in the case of limited or requires you to select a
3076
- // move normally if sleeping/frozen/bound.
3093
+ // move normally if sleeping/frozen/bound
3077
3094
  if (!showdown and (limited or foe.active.volatiles.Binding or
3078
3095
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3079
3096
  {
@@ -3086,7 +3103,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3086
3103
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3087
3104
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3088
3105
  // Move will not result in forcing the subsequent use unless the user also had the
3089
- // proc-ed move in their moveset) and disabling all other moves.
3106
+ // proc-ed move in their moveset) and disabling all other moves
3090
3107
  if (limited) {
3091
3108
  assert(showdown);
3092
3109
  assert(side.last_selected_move != .None);