@pkmn/engine 0.1.0-dev.dc33f678 → 0.1.0-dev.deda8593

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 (49) 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 +33 -22
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +8 -8
  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 +397 -384
  24. package/src/lib/common/rational.zig +11 -8
  25. package/src/lib/common/rng.zig +20 -11
  26. package/src/lib/gen1/calc.zig +217 -47
  27. package/src/lib/gen1/chance.zig +91 -92
  28. package/src/lib/gen1/data/moves.zig +10 -10
  29. package/src/lib/gen1/data/species.zig +3 -3
  30. package/src/lib/gen1/data/types.zig +5 -5
  31. package/src/lib/gen1/data.zig +35 -35
  32. package/src/lib/gen1/helpers.zig +136 -55
  33. package/src/lib/gen1/mechanics.zig +333 -312
  34. package/src/lib/gen2/calc.zig +143 -0
  35. package/src/lib/gen2/chance.zig +1315 -0
  36. package/src/lib/gen2/data/items.zig +3 -3
  37. package/src/lib/gen2/data/moves.zig +5 -5
  38. package/src/lib/gen2/data/species.zig +1 -1
  39. package/src/lib/gen2/data/types.zig +10 -10
  40. package/src/lib/gen2/data.zig +27 -37
  41. package/src/lib/gen2/generated.zig +806 -0
  42. package/src/lib/gen2/helpers.zig +37 -7
  43. package/src/lib/gen2/mechanics.zig +1621 -456
  44. package/src/lib/{binding/node.zig → node.zig} +9 -12
  45. package/src/lib/pkmn.zig +27 -12
  46. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  47. package/src/test/display/pkmn.html.tmpl +5 -5
  48. package/src/lib/gen1/README.md +0 -894
  49. package/src/lib/gen2/README.md +0 -330
@@ -22,6 +22,7 @@ const Result = common.Result;
22
22
 
23
23
  const Boost = protocol.Boost;
24
24
  const Damage = protocol.Damage;
25
+ const Fail = protocol.Fail;
25
26
  const Heal = protocol.Heal;
26
27
 
27
28
  const Gen12 = rng.Gen12;
@@ -70,8 +71,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
70
71
  var p1 = battle.side(.P1);
71
72
  var p2 = battle.side(.P2);
72
73
 
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;
74
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
75
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
75
76
 
76
77
  if (try turnOrder(battle, c1, c2, options) == .P1) {
77
78
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -89,11 +90,11 @@ fn start(battle: anytype, options: anytype) !Result {
89
90
  const p1 = battle.side(.P1);
90
91
  const p2 = battle.side(.P2);
91
92
 
92
- var p1_slot = findFirstAlive(p1);
93
+ const p1_slot = findFirstAlive(p1);
93
94
  assert(!showdown or p1_slot == 1);
94
95
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
95
96
 
96
- var p2_slot = findFirstAlive(p2);
97
+ const p2_slot = findFirstAlive(p2);
97
98
  assert(!showdown or p2_slot == 1);
98
99
  if (p2_slot == 0) return Result.Win;
99
100
 
@@ -165,7 +166,7 @@ fn selectMove(
165
166
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
166
167
  // set the selected move will not actually be used, it will just be reported as having
167
168
  // been used (this differs from how Pokémon Showdown works, but its impossible to
168
- // replicate the incorrect behavior with the correct mechanisms).
169
+ // replicate the incorrect behavior with the correct mechanisms)
169
170
  saveMove(battle, player, choice);
170
171
  } else {
171
172
  assert(choice.data == 0);
@@ -255,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
255
256
 
256
257
  foe.active.volatiles.Binding = false;
257
258
 
258
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
259
260
  options.chance.switched(player, side.order[0], out);
260
261
 
261
262
  if (showdown and incoming.status == Status.TOX) {
262
263
  incoming.status = Status.init(.PSN);
263
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
264
265
  // rather not clutter up turnOrder just for this (incorrect) log message
265
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
266
267
  }
267
268
  }
268
269
 
@@ -346,10 +347,10 @@ fn doTurn(
346
347
  )) |r| return r;
347
348
  if (!replace) {
348
349
  if (player_choice.type != .Switch) {
349
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
350
351
  }
351
352
  if (residual) try handleResidual(battle, player, options);
352
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
353
354
  } else if (foe_choice.type == .Pass) return null;
354
355
 
355
356
  options.chance.clearPending();
@@ -368,10 +369,10 @@ fn doTurn(
368
369
  )) |r| return r;
369
370
  if (!replace) {
370
371
  if (!calc and foe_choice.type != .Switch) {
371
- if (try checkFaint(battle, player, options)) |r| return r;
372
+ if (try checkFaint(battle, player, true, options)) |r| return r;
372
373
  }
373
374
  if (residual) try handleResidual(battle, foe_player, options);
374
- if (try checkFaint(battle, foe_player, options)) |r| return r;
375
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
375
376
  }
376
377
 
377
378
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -406,7 +407,7 @@ fn executeMove(
406
407
  if (side.last_selected_move == .SKIP_TURN) {
407
408
  assert(!showdown);
408
409
  if (battle.foe(player).active.volatiles.Binding) {
409
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
410
411
  }
411
412
  return null;
412
413
  }
@@ -430,7 +431,7 @@ fn executeMove(
430
431
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
431
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
432
433
  // 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.
434
+ // index 1 and the current Pokémon has the same move in its first slot
434
435
  if (!auto) {
435
436
  // side.active.moves(slot) is safe to check even though the slot in question might not
436
437
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -496,23 +497,23 @@ fn beforeMove(
496
497
  }
497
498
 
498
499
  if (duration == 0) {
499
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
500
501
  stored.status = 0; // clears EXT if present
501
502
  } else {
502
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
503
504
  }
504
505
  side.last_used_move = .None;
505
506
  return .done;
506
507
  }
507
508
 
508
509
  if (Status.is(stored.status, .FRZ)) {
509
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
510
511
  side.last_used_move = .None;
511
512
  return .done;
512
513
  }
513
514
 
514
515
  if (skip or foe.active.volatiles.Binding) {
515
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
516
517
  return .done;
517
518
  }
518
519
 
@@ -520,13 +521,13 @@ fn beforeMove(
520
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
521
522
  // Pokémon can sometimes flinch multiple times from the same original hit
522
523
  if (!showdown) volatiles.Flinch = false;
523
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
524
525
  return .done;
525
526
  }
526
527
 
527
528
  if (volatiles.Recharging) {
528
529
  volatiles.Recharging = false;
529
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
530
531
  return .done;
531
532
  }
532
533
 
@@ -540,7 +541,7 @@ fn beforeMove(
540
541
 
541
542
  if (volatiles.disable_duration == 0) {
542
543
  volatiles.disable_move = 0;
543
- try log.end(ident, .Disable);
544
+ try log.end(.{ ident, .Disable });
544
545
  }
545
546
  }
546
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -565,9 +566,9 @@ fn beforeMove(
565
566
 
566
567
  if (volatiles.confusion == 0) {
567
568
  volatiles.Confusion = false;
568
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
569
570
  } else {
570
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
571
572
 
572
573
  if (try Rolls.confused(battle, player, options)) {
573
574
  assert(!volatiles.MultiHit);
@@ -618,7 +619,7 @@ fn beforeMove(
618
619
  volatiles.Binding = false;
619
620
  options.chance.clearDurations(player, null);
620
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
621
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
622
623
  return .done;
623
624
  }
624
625
 
@@ -634,8 +635,6 @@ fn beforeMove(
634
635
  volatiles.state +%= battle.last_damage;
635
636
  }
636
637
 
637
- assert(volatiles.attacks > 0);
638
-
639
638
  if (options.calc.modify(player, .attacking)) |extend| {
640
639
  if (!extend) volatiles.attacks = 0;
641
640
  } else {
@@ -644,18 +643,18 @@ fn beforeMove(
644
643
  try options.chance.attacking(player, volatiles.attacks);
645
644
 
646
645
  if (volatiles.attacks != 0) {
647
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
648
647
  return .done;
649
648
  }
650
649
 
651
650
  volatiles.Bide = false;
652
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
653
652
 
654
653
  battle.last_damage = volatiles.state *% 2;
655
654
  volatiles.state = 0;
656
655
 
657
656
  if (battle.last_damage == 0) {
658
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
659
658
  return .done;
660
659
  }
661
660
 
@@ -675,8 +674,6 @@ fn beforeMove(
675
674
  }
676
675
 
677
676
  if (volatiles.Thrashing) {
678
- assert(volatiles.attacks > 0);
679
-
680
677
  if (options.calc.modify(player, .attacking)) |extend| {
681
678
  if (!extend) volatiles.attacks = 0;
682
679
  } else {
@@ -685,23 +682,21 @@ fn beforeMove(
685
682
  try options.chance.attacking(player, volatiles.attacks);
686
683
 
687
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
688
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
689
686
  }
690
- try log.move(ident, side.last_selected_move, battle.active(player.foe()));
687
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
691
688
  if (showdown) {
692
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
693
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
694
691
  side.last_used_move = side.last_selected_move;
695
692
  if (handleThrashing(battle, active, player, options)) {
696
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
697
694
  }
698
695
  }
699
696
  return .skip_can;
700
697
  }
701
698
 
702
699
  if (volatiles.Binding) {
703
- assert(volatiles.attacks > 0);
704
-
705
700
  if (options.calc.modify(player, .binding)) |extend| {
706
701
  if (!extend) volatiles.attacks = 0;
707
702
  } else {
@@ -709,7 +704,7 @@ fn beforeMove(
709
704
  }
710
705
  try options.chance.binding(player, volatiles.attacks);
711
706
 
712
- try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
713
708
  if (showdown or battle.last_damage != 0) {
714
709
  const sub = showdown and foe.active.volatiles.Substitute;
715
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -737,22 +732,21 @@ fn canMove(
737
732
  const player_ident = battle.active(player);
738
733
  const move = Move.get(side.last_selected_move);
739
734
 
740
- var skip = skip_pp;
741
735
  if (side.active.volatiles.Charging) {
742
736
  side.active.volatiles.Charging = false;
743
737
  side.active.volatiles.Invulnerable = false;
744
738
  } else if (move.effect == .Charge) {
745
- try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
746
740
  setCounterable(battle, player, side, move);
747
741
  try Effects.charge(battle, player, options);
748
742
  return false;
749
743
  }
750
744
 
751
745
  side.last_used_move = side.last_selected_move;
752
- if (!skip) decrementPP(side, mslot, auto);
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
753
747
 
754
748
  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);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
756
750
  setCounterable(battle, player, side, move);
757
751
 
758
752
  if (move.effect.onBegin()) {
@@ -858,7 +852,7 @@ fn doMove(
858
852
  break :miss true;
859
853
  }
860
854
  break :miss (move.target != .Self and
861
- !moveHit(battle, player, move, &immune, &mist, options));
855
+ !try moveHit(battle, player, move, &immune, &mist, options));
862
856
  };
863
857
  assert(!immune or miss or (showdown and move.effect == .Binding));
864
858
 
@@ -901,11 +895,11 @@ fn doMove(
901
895
  }
902
896
  }
903
897
 
904
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
905
899
  miss = if (showdown or skip)
906
900
  miss
907
901
  else
908
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
902
+ (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
909
903
 
910
904
  assert(showdown or miss or battle.last_damage > 0 or skip);
911
905
  assert((!showdown and miss) or !(ohko and immune));
@@ -929,16 +923,16 @@ fn doMove(
929
923
  ohko = (!showdown or (!immune and !invulnerable)) and
930
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
925
  if (ohko) {
932
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
933
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
934
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
935
929
  } else if (mist) {
936
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
937
- try log.fail(foe_ident, .None);
930
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
+ try log.fail(.{ foe_ident, .None });
938
932
  } else {
939
- if (showdown or !zero) try options.chance.commit(player, .miss);
940
- try log.lastmiss();
941
- try log.miss(battle.active(player));
933
+ if (!showdown and !zero) try options.chance.commit(player, .miss);
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
942
936
  }
943
937
  if (move.effect == .JumpKick) {
944
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -955,13 +949,13 @@ fn doMove(
955
949
  try options.chance.commit(player, .binding);
956
950
  }
957
951
  return null;
958
- } else {
952
+ } else if (!showdown) {
959
953
  try options.chance.commit(player, .hit);
960
954
  }
961
955
 
962
956
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
963
957
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
964
- // it up above before damage calculation).
958
+ // it up above before damage calculation)
965
959
  if (!showdown and move.effect.isMulti()) {
966
960
  try Effects.multiHit(battle, player, move, options);
967
961
  hits = side.active.volatiles.attacks;
@@ -974,15 +968,15 @@ fn doMove(
974
968
  var hit: u4 = 0;
975
969
  while (hit < hits) {
976
970
  if (hit == 0) {
977
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
978
972
  if (effectiveness > Effectiveness.neutral) {
979
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
980
974
  } else if (effectiveness < Effectiveness.neutral) {
981
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
982
976
  }
983
977
  }
984
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
985
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
986
980
  hit += 1;
987
981
  if (foe.stored().hp == 0) break;
988
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -999,7 +993,7 @@ fn doMove(
999
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1000
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1001
995
  }
1002
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1003
997
  } else if (showdown) {
1004
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1005
999
  if (move.effect == .Binding) {
@@ -1009,9 +1003,9 @@ fn doMove(
1009
1003
  assert(foe.stored().hp > 0);
1010
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1011
1005
  if (sub) {
1012
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1013
1007
  } else {
1014
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1015
1009
  try buildRage(battle, player.foe(), options);
1016
1010
  }
1017
1011
  return null;
@@ -1020,12 +1014,12 @@ fn doMove(
1020
1014
  }
1021
1015
 
1022
1016
  // 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.
1017
+ // if an effect was intended to "always happen" it will still get skipped
1024
1018
  if (nullified) return null;
1025
1019
 
1026
1020
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1027
1021
  // 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.
1022
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1029
1023
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1030
1024
 
1031
1025
  if (foe.stored().hp == 0) return null;
@@ -1040,7 +1034,7 @@ fn doMove(
1040
1034
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1041
1035
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1042
1036
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1043
- // puts the |-status| message before |-hitcount| instead of after.
1037
+ // puts the |-status| message before |-hitcount| instead of after
1044
1038
  if (move.effect == .Twineedle) {
1045
1039
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1046
1040
  } else if (move.effect == .Disable) {
@@ -1059,12 +1053,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1059
1053
  const side = battle.side(player);
1060
1054
 
1061
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1062
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1063
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1064
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1065
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1066
1060
 
1067
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1061
+ const spurious =
1062
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1063
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1068
1064
  }
1069
1065
 
1070
1066
  fn calcDamage(
@@ -1116,9 +1112,9 @@ fn calcDamage(
1116
1112
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1117
1113
  }
1118
1114
 
1119
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1115
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1120
1116
 
1121
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1117
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1122
1118
 
1123
1119
  if (def == 0) return false;
1124
1120
 
@@ -1179,7 +1175,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1179
1175
  const foe = battle.foe(player);
1180
1176
 
1181
1177
  if (!try checkHit(battle, player, move, options)) return null;
1182
- try options.chance.commit(player, .hit);
1183
1178
 
1184
1179
  battle.last_damage = switch (side.last_selected_move) {
1185
1180
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1209,7 +1204,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1204
  const foe = battle.foe(player);
1210
1205
 
1211
1206
  if (battle.last_damage == 0) {
1212
- try options.log.fail(battle.active(player), .None);
1207
+ try options.log.fail(.{ battle.active(player), .None });
1213
1208
  return null;
1214
1209
  }
1215
1210
 
@@ -1227,22 +1222,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1222
  foe_last_selected_move.type == .Fighting);
1228
1223
 
1229
1224
  if (!used and !selected) {
1230
- try options.log.fail(battle.active(player), .None);
1225
+ try options.log.fail(.{ battle.active(player), .None });
1231
1226
  return null;
1232
1227
  }
1233
1228
 
1234
1229
  if (!used or !selected) {
1235
1230
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1236
1231
  if (!showdown) return Result.Error;
1237
- try options.log.fail(battle.active(player), .None);
1232
+ try options.log.fail(.{ battle.active(player), .None });
1238
1233
  return null;
1239
1234
  }
1240
1235
 
1241
1236
  battle.last_damage *|= 2;
1242
1237
 
1243
- // Pokémon Showdown calls checkHit before Counter
1238
+ // Pokémon Showdown calls moveHit before Counter
1244
1239
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1245
- try options.chance.commit(player, .hit);
1246
1240
 
1247
1241
  const sub = showdown and foe.active.volatiles.Substitute;
1248
1242
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1270,20 +1264,20 @@ fn applyDamage(
1270
1264
  subbed.active.volatiles.substitute = 0;
1271
1265
  subbed.active.volatiles.Substitute = false;
1272
1266
  // 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);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
+ options.calc.capped(sub_player);
1275
1269
  return true;
1276
1270
  } else {
1277
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1278
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1279
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1280
1274
  return false;
1281
1275
  }
1282
1276
  }
1283
1277
 
1284
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1285
1279
  target.stored().hp -= battle.last_damage;
1286
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1287
1281
  return false;
1288
1282
  }
1289
1283
 
@@ -1302,7 +1296,7 @@ fn mirrorMove(
1302
1296
  side.last_selected_move = foe.last_used_move;
1303
1297
 
1304
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1305
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1306
1300
  return null;
1307
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1308
1302
  incrementPP(side, mslot);
@@ -1338,20 +1332,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1338
1332
  var immune = false;
1339
1333
  var mist = false;
1340
1334
 
1341
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1335
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1336
+ if (!showdown) try options.chance.commit(player, .hit);
1337
+ return true;
1338
+ }
1342
1339
 
1343
1340
  assert(!immune);
1344
1341
  if (mist) {
1345
1342
  assert(!showdown);
1346
1343
  const foe_ident = battle.active(player.foe());
1347
- try options.log.activate(foe_ident, .Mist);
1348
- try options.log.fail(foe_ident, .None);
1344
+ try options.log.activate(.{ foe_ident, .Mist });
1345
+ try options.log.fail(.{ foe_ident, .None });
1349
1346
  } else {
1350
- try options.chance.commit(player, .miss);
1351
- try options.log.lastmiss();
1352
- try options.log.miss(battle.active(player));
1347
+ try options.log.lastmiss(.{});
1348
+ try options.log.miss(.{battle.active(player)});
1353
1349
  }
1354
1350
 
1351
+ if (!showdown) try options.chance.commit(player, .miss);
1355
1352
  return false;
1356
1353
  }
1357
1354
 
@@ -1362,7 +1359,7 @@ fn moveHit(
1362
1359
  immune: *bool,
1363
1360
  mist: *bool,
1364
1361
  options: anytype,
1365
- ) bool {
1362
+ ) !bool {
1366
1363
  var side = battle.side(player);
1367
1364
  const foe = battle.foe(player);
1368
1365
 
@@ -1401,7 +1398,7 @@ fn moveHit(
1401
1398
  const overwritten = overwrite and state > 0;
1402
1399
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1400
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1401
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1402
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1403
  accuracy = accuracy * boost[0] / boost[1];
1407
1404
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1413,7 +1410,7 @@ fn moveHit(
1413
1410
  if (overwrite) side.active.volatiles.state = accuracy;
1414
1411
 
1415
1412
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1416
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1413
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1417
1414
  };
1418
1415
 
1419
1416
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1431,6 +1428,7 @@ fn moveHit(
1431
1428
  fn checkFaint(
1432
1429
  battle: anytype,
1433
1430
  player: Player,
1431
+ cap: bool,
1434
1432
  options: anytype,
1435
1433
  ) @TypeOf(options.log).Error!?Result {
1436
1434
  const side = battle.side(player);
@@ -1440,24 +1438,23 @@ fn checkFaint(
1440
1438
  const foe_fainted = foe.stored().hp == 0;
1441
1439
 
1442
1440
  const player_out = findFirstAlive(side) == 0;
1443
- const foe_out = findFirstAlive(foe) == 0;
1444
- const tie = player_out and foe_out;
1445
- const more = tie or player_out or foe_out;
1441
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1442
+ const more = player_out or foe_out;
1446
1443
 
1447
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1448
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1444
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1445
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1449
1446
 
1450
1447
  assert(!side.active.volatiles.MultiHit);
1451
1448
  assert(!foe.active.volatiles.MultiHit);
1452
1449
 
1453
- if (tie) {
1454
- try options.log.tie();
1450
+ if (player_out and foe_out) {
1451
+ try options.log.tie(.{});
1455
1452
  return Result.Tie;
1456
1453
  } else if (player_out) {
1457
- try options.log.win(player.foe());
1454
+ try options.log.win(.{player.foe()});
1458
1455
  return if (player == .P1) Result.Lose else Result.Win;
1459
1456
  } else if (foe_out) {
1460
- try options.log.win(player);
1457
+ try options.log.win(.{player});
1461
1458
  return if (player == .P1) Result.Win else Result.Lose;
1462
1459
  }
1463
1460
 
@@ -1466,7 +1463,7 @@ fn checkFaint(
1466
1463
  return .{ .p1 = foe_choice, .p2 = .Switch };
1467
1464
  }
1468
1465
 
1469
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1466
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1470
1467
  var side = battle.side(player);
1471
1468
  var foe = battle.foe(player);
1472
1469
  assert(side.stored().hp == 0);
@@ -1499,8 +1496,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1499
1496
  side.stored().stats.spe;
1500
1497
  }
1501
1498
 
1502
- try options.log.faint(battle.active(player), done);
1503
- options.calc.capped(player);
1499
+ try options.log.faint(.{ battle.active(player), done });
1500
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1501
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1502
+ if (cap) options.calc.capped(player);
1504
1503
  return null;
1505
1504
  }
1506
1505
 
@@ -1521,8 +1520,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1521
1520
 
1522
1521
  if (!showdown or damage > 0) {
1523
1522
  stored.hp -= damage;
1524
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1525
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1523
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1524
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1526
1525
  }
1527
1526
  }
1528
1527
 
@@ -1547,14 +1546,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1547
1546
  const amount = @min(damage, stored.hp);
1548
1547
  stored.hp -= amount;
1549
1548
 
1550
- // As above, Pokémon Showdown uses damageOf but its not relevant
1551
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1549
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1550
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1552
1551
 
1553
1552
  const before = foe_stored.hp;
1554
1553
  // Uncapped damage is added back to the foe
1555
1554
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1556
1555
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1557
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1556
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1558
1557
  }
1559
1558
  }
1560
1559
 
@@ -1565,18 +1564,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1565
1564
  battle.turn += 1;
1566
1565
 
1567
1566
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1568
- try options.log.tie();
1567
+ try options.log.tie(.{});
1569
1568
  return Result.Tie;
1570
1569
  }
1571
1570
 
1572
1571
  if (showdown and battle.turn >= 1000) {
1573
- try options.log.tie();
1572
+ try options.log.tie(.{});
1574
1573
  return Result.Tie;
1575
1574
  } else if (battle.turn >= 65535) {
1576
1575
  return Result.Error;
1577
1576
  }
1578
1577
 
1579
- try options.log.turn(battle.turn);
1578
+ try options.log.turn(.{battle.turn});
1580
1579
 
1581
1580
  return Result.Default;
1582
1581
  }
@@ -1646,14 +1645,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1646
1645
  side.last_selected_move;
1647
1646
  if (m != .None and m == last) {
1648
1647
  side.active.volatiles.Charging = false;
1649
- try options.log.disabled(ident, last);
1648
+ try options.log.cant(.{ ident, .Disable, last });
1650
1649
  return true;
1651
1650
  }
1652
1651
  }
1653
1652
  return false;
1654
1653
  }
1655
1654
 
1656
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1655
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1657
1656
  const side = battle.side(who);
1658
1657
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1659
1658
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1671,7 +1670,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1671
1670
  return true;
1672
1671
  }
1673
1672
 
1674
- inline fn onBegin(
1673
+ fn onBegin(
1675
1674
  battle: anytype,
1676
1675
  player: Player,
1677
1676
  move: Move.Data,
@@ -1679,7 +1678,6 @@ inline fn onBegin(
1679
1678
  residual: *bool,
1680
1679
  options: anytype,
1681
1680
  ) !void {
1682
- assert(move.effect.onBegin());
1683
1681
  return switch (move.effect) {
1684
1682
  .Confusion => Effects.confusion(battle, player, move, options),
1685
1683
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,8 +1699,7 @@ inline fn onBegin(
1701
1699
  };
1702
1700
  }
1703
1701
 
1704
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
- assert(move.effect.onEnd());
1702
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1706
1703
  return switch (move.effect) {
1707
1704
  .Bide => Effects.bide(battle, player, options),
1708
1705
  // zig fmt: off
@@ -1717,14 +1714,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1717
1714
  };
1718
1715
  }
1719
1716
 
1720
- inline fn alwaysHappens(
1717
+ fn alwaysHappens(
1721
1718
  battle: anytype,
1722
1719
  player: Player,
1723
1720
  move: Move.Data,
1724
1721
  residual: *bool,
1725
1722
  options: anytype,
1726
1723
  ) !void {
1727
- assert(move.effect.alwaysHappens());
1728
1724
  return switch (move.effect) {
1729
1725
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1730
1726
  .Explode => Effects.explode(battle, player),
@@ -1736,7 +1732,7 @@ inline fn alwaysHappens(
1736
1732
  };
1737
1733
  }
1738
1734
 
1739
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1735
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1740
1736
  return switch (move.effect) {
1741
1737
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1742
1738
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1763,7 +1759,7 @@ pub const Effects = struct {
1763
1759
  side.active.volatiles.state = 0;
1764
1760
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1765
1761
 
1766
- try options.log.start(battle.active(player), .Bide);
1762
+ try options.log.start(.{ battle.active(player), .Bide });
1767
1763
  }
1768
1764
 
1769
1765
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1777,11 +1773,11 @@ pub const Effects = struct {
1777
1773
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1778
1774
  if (Status.is(foe_stored.status, .FRZ)) {
1779
1775
  assert(move.type == .Fire);
1780
- try options.log.curestatus(
1776
+ try options.log.curestatus(.{
1781
1777
  battle.active(player.foe()),
1782
1778
  foe_stored.status,
1783
1779
  .Message,
1784
- );
1780
+ });
1785
1781
  foe_stored.status = 0;
1786
1782
  }
1787
1783
  return;
@@ -1795,7 +1791,7 @@ pub const Effects = struct {
1795
1791
  foe_stored.status = Status.init(.BRN);
1796
1792
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1797
1793
 
1798
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1794
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1799
1795
  }
1800
1796
 
1801
1797
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1805,8 +1801,8 @@ pub const Effects = struct {
1805
1801
  volatiles.Charging = true;
1806
1802
  const move = side.last_selected_move;
1807
1803
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1808
- try options.log.laststill();
1809
- try options.log.prepare(battle.active(player), move);
1804
+ try options.log.laststill(.{});
1805
+ try options.log.prepare(.{ battle.active(player), move });
1810
1806
  }
1811
1807
 
1812
1808
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1817,42 +1813,36 @@ pub const Effects = struct {
1817
1813
  if (!try Rolls.confusionChance(battle, player, options)) return;
1818
1814
  } else {
1819
1815
  if (showdown) {
1820
- if (!try checkHit(battle, player, move, options)) {
1821
- return;
1822
- } else if (sub) {
1823
- return options.log.fail(battle.active(player.foe()), .None);
1824
- }
1816
+ if (!try checkHit(battle, player, move, options)) return;
1817
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1825
1818
  } else {
1826
- if (sub) {
1827
- return options.log.fail(battle.active(player.foe()), .None);
1828
- } else if (!try checkHit(battle, player, move, options)) {
1829
- return;
1830
- }
1819
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1820
+ if (!try checkHit(battle, player, move, options)) return;
1831
1821
  }
1832
- try options.chance.commit(player, .hit);
1833
1822
  }
1834
1823
 
1835
1824
  if (foe.active.volatiles.Confusion) return;
1836
1825
  foe.active.volatiles.Confusion = true;
1837
1826
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1838
1827
 
1839
- try options.log.start(battle.active(player.foe()), .Confusion);
1828
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1840
1829
  }
1841
1830
 
1842
1831
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1843
1832
  const foe = battle.foe(player);
1844
1833
 
1845
1834
  if (foe.active.volatiles.Invulnerable) {
1846
- try options.log.lastmiss();
1847
- return options.log.miss(battle.active(player));
1835
+ try options.log.lastmiss(.{});
1836
+ return options.log.miss(.{battle.active(player)});
1848
1837
  }
1849
1838
 
1850
1839
  battle.side(player).active.types = foe.active.types;
1851
- return options.log.typechange(
1840
+ return options.log.start(.{
1852
1841
  battle.active(player),
1842
+ .TypeChange,
1853
1843
  foe.active.types,
1854
1844
  battle.active(player.foe()),
1855
- );
1845
+ });
1856
1846
  }
1857
1847
 
1858
1848
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1861,13 +1851,10 @@ pub const Effects = struct {
1861
1851
  const foe_ident = battle.active(player.foe());
1862
1852
 
1863
1853
  // Pokémon Showdown handles hit/miss earlier in doMove
1864
- if (!showdown) {
1865
- if (!try checkHit(battle, player, move, options)) return null;
1866
- try options.chance.commit(player, .hit);
1867
- }
1854
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1868
1855
 
1869
1856
  if (volatiles.disable_move != 0) {
1870
- try options.log.fail(foe_ident, .None);
1857
+ try options.log.fail(.{ foe_ident, .None });
1871
1858
  return null;
1872
1859
  }
1873
1860
 
@@ -1883,7 +1870,7 @@ pub const Effects = struct {
1883
1870
  // Technically this is still considered simply a "miss" on the cartridge,
1884
1871
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1885
1872
  if (n == 0) {
1886
- try options.log.fail(foe_ident, .None);
1873
+ try options.log.fail(.{ foe_ident, .None });
1887
1874
  return null;
1888
1875
  } else if (err) {
1889
1876
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1896,13 +1883,13 @@ pub const Effects = struct {
1896
1883
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1897
1884
 
1898
1885
  const id = foe.active.move(volatiles.disable_move).id;
1899
- try options.log.startEffect(foe_ident, .Disable, id);
1886
+ try options.log.start(.{ foe_ident, .Disable, id });
1900
1887
  return null;
1901
1888
  }
1902
1889
 
1903
1890
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1904
- var side = battle.side(player);
1905
- var stored = side.stored();
1891
+ var stored = battle.side(player).stored();
1892
+ const ident = battle.active(player);
1906
1893
 
1907
1894
  if (battle.last_damage == 0) {
1908
1895
  assert(showdown);
@@ -1915,7 +1902,7 @@ pub const Effects = struct {
1915
1902
  if (stored.hp == stored.stats.hp) return;
1916
1903
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1917
1904
 
1918
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1905
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1919
1906
  }
1920
1907
 
1921
1908
  fn explode(battle: anytype, player: Player) !void {
@@ -1947,7 +1934,7 @@ pub const Effects = struct {
1947
1934
  if (side.active.volatiles.FocusEnergy) return;
1948
1935
  side.active.volatiles.FocusEnergy = true;
1949
1936
 
1950
- try options.log.start(battle.active(player), .FocusEnergy);
1937
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1951
1938
  }
1952
1939
 
1953
1940
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1969,7 +1956,7 @@ pub const Effects = struct {
1969
1956
  foe_stored.status = Status.init(.FRZ);
1970
1957
  // GLITCH: Hyper Beam recharging status is not cleared
1971
1958
 
1972
- try options.log.status(foe_ident, foe_stored.status, .None);
1959
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1973
1960
  }
1974
1961
 
1975
1962
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1977,7 +1964,7 @@ pub const Effects = struct {
1977
1964
  var side = battle.side(player);
1978
1965
  var foe = battle.foe(player);
1979
1966
 
1980
- var side_stored = side.stored();
1967
+ const side_stored = side.stored();
1981
1968
  var foe_stored = foe.stored();
1982
1969
 
1983
1970
  const player_ident = battle.active(player);
@@ -1989,8 +1976,8 @@ pub const Effects = struct {
1989
1976
  side.active.stats = side_stored.stats;
1990
1977
  foe.active.stats = foe_stored.stats;
1991
1978
 
1992
- try log.activate(player_ident, .Haze);
1993
- try log.clearallboost();
1979
+ try log.activate(.{ player_ident, .Haze });
1980
+ try log.clearallboost(.{});
1994
1981
 
1995
1982
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1996
1983
  if (showdown) {
@@ -1998,11 +1985,11 @@ pub const Effects = struct {
1998
1985
  const p: Player = @enumFromInt(i);
1999
1986
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2000
1987
  if (p != player and Status.any(s.stored().status)) {
2001
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1988
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2002
1989
  s.stored().status = 0;
2003
1990
  } else if (showdown and s.stored().status == Status.TOX) {
2004
1991
  s.stored().status = Status.init(.PSN);
2005
- try log.status(battle.active(p), s.stored().status, .None);
1992
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2006
1993
  }
2007
1994
  try clearVolatiles(battle, p, p != player, options);
2008
1995
  }
@@ -2011,7 +1998,7 @@ pub const Effects = struct {
2011
1998
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2012
1999
  foe.last_selected_move = .SKIP_TURN;
2013
2000
  }
2014
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2001
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2015
2002
  foe_stored.status = 0;
2016
2003
  }
2017
2004
  try clearVolatiles(battle, player, false, options);
@@ -2027,7 +2014,7 @@ pub const Effects = struct {
2027
2014
  // GLITCH: HP recovery move failure glitches
2028
2015
  const delta = stored.stats.hp - stored.hp;
2029
2016
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2030
- return try options.log.fail(ident, .None);
2017
+ return options.log.fail(.{ ident, .None });
2031
2018
  }
2032
2019
 
2033
2020
  const rest = side.last_selected_move == .Rest;
@@ -2035,17 +2022,17 @@ pub const Effects = struct {
2035
2022
  // Adding the sleep status runs the sleep condition handler to roll duration
2036
2023
  if (showdown) battle.rng.advance(1);
2037
2024
  stored.status = Status.slf(2);
2038
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2025
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2039
2026
  stored.hp = stored.stats.hp;
2040
2027
  } else {
2041
2028
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2042
2029
  }
2043
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2030
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2044
2031
  }
2045
2032
 
2046
2033
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2047
2034
  battle.side(player).active.volatiles.Recharging = true;
2048
- try options.log.mustrecharge(battle.active(player));
2035
+ try options.log.mustrecharge(.{battle.active(player)});
2049
2036
  }
2050
2037
 
2051
2038
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2054,33 +2041,31 @@ pub const Effects = struct {
2054
2041
  if (showdown) {
2055
2042
  // Invulnerability trumps type immunity on Pokémon Showdown
2056
2043
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2057
- return options.log.immune(battle.active(player.foe()), .None);
2044
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2058
2045
  }
2059
2046
  if (!try checkHit(battle, player, move, options)) return;
2060
2047
  if (foe.active.volatiles.LeechSeed) return;
2061
2048
  } else {
2062
2049
  if (!try checkHit(battle, player, move, options)) return;
2063
2050
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2064
- try options.log.lastmiss();
2065
- return options.log.miss(battle.active(player));
2051
+ try options.log.lastmiss(.{});
2052
+ return options.log.miss(.{battle.active(player)});
2066
2053
  }
2067
2054
  }
2068
2055
 
2069
- try options.chance.commit(player, .hit);
2070
2056
  foe.active.volatiles.LeechSeed = true;
2071
2057
 
2072
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2058
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2073
2059
  }
2074
2060
 
2075
2061
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2076
2062
  var side = battle.side(player);
2063
+ const ident = battle.active(player);
2077
2064
 
2078
- if (side.active.volatiles.LightScreen) {
2079
- return options.log.fail(battle.active(player), .None);
2080
- }
2065
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2081
2066
  side.active.volatiles.LightScreen = true;
2082
2067
 
2083
- try options.log.start(battle.active(player), .LightScreen);
2068
+ try options.log.start(.{ ident, .LightScreen });
2084
2069
  }
2085
2070
 
2086
2071
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2108,17 +2093,15 @@ pub const Effects = struct {
2108
2093
  if (!has_mimic) {
2109
2094
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2110
2095
  if (!try checkHit(battle, player, move, options)) return;
2111
- try options.chance.commit(player, .hit);
2112
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2113
2097
  }
2114
2098
  }
2115
2099
  if (!try checkHit(battle, player, move, options)) return;
2116
- try options.chance.commit(player, .hit);
2117
2100
 
2118
2101
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2119
2102
  side.active.move(oslot).id = foe.active.move(rslot).id;
2120
2103
 
2121
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2104
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2122
2105
  }
2123
2106
 
2124
2107
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2127,7 +2110,7 @@ pub const Effects = struct {
2127
2110
  if (side.active.volatiles.Mist) return;
2128
2111
  side.active.volatiles.Mist = true;
2129
2112
 
2130
- try options.log.start(battle.active(player), .Mist);
2113
+ try options.log.start(.{ battle.active(player), .Mist });
2131
2114
  }
2132
2115
 
2133
2116
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2153,25 +2136,25 @@ pub const Effects = struct {
2153
2136
 
2154
2137
  if (showdown) {
2155
2138
  // Invulnerability trumps type immunity on Pokémon Showdown
2156
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2157
- if (!try checkHit(battle, player, move, options)) return;
2139
+ if (immune and !foe.active.volatiles.Invulnerable) {
2140
+ return log.immune(.{ foe_ident, .None });
2141
+ } else if (!try checkHit(battle, player, move, options)) return;
2158
2142
  }
2159
2143
  if (Status.any(foe_stored.status)) {
2160
- return log.fail(
2144
+ return log.fail(.{
2161
2145
  foe_ident,
2162
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2163
- );
2146
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2147
+ });
2164
2148
  }
2165
2149
  if (!showdown) {
2166
- if (immune) return log.immune(foe_ident, .None);
2150
+ if (immune) return log.immune(.{ foe_ident, .None });
2167
2151
  if (!try checkHit(battle, player, move, options)) return;
2168
2152
  }
2169
- try options.chance.commit(player, .hit);
2170
2153
 
2171
2154
  foe_stored.status = Status.init(.PAR);
2172
2155
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2173
2156
 
2174
- try log.status(foe_ident, foe_stored.status, .None);
2157
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2175
2158
  }
2176
2159
 
2177
2160
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2193,12 +2176,12 @@ pub const Effects = struct {
2193
2176
  foe_stored.status = Status.init(.PAR);
2194
2177
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2195
2178
 
2196
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2179
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2197
2180
  }
2198
2181
 
2199
2182
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2200
2183
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2201
- try options.log.fieldactivate();
2184
+ try options.log.fieldactivate(.{});
2202
2185
  }
2203
2186
  }
2204
2187
 
@@ -2213,28 +2196,27 @@ pub const Effects = struct {
2213
2196
  return;
2214
2197
  } else if (foe.active.volatiles.Substitute) {
2215
2198
  if (move.effect != .Poison) return;
2216
- return log.fail(foe_ident, .None);
2199
+ return log.fail(.{ foe_ident, .None });
2217
2200
  } else if (Status.any(foe_stored.status)) {
2218
2201
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2219
2202
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2220
2203
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2221
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2204
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2222
2205
  if (!showdown)
2223
- .Poison
2206
+ Fail.Poison
2224
2207
  else if (toxic == (foe_stored.status == Status.TOX))
2225
- if (toxic) .Toxic else .Poison
2208
+ if (toxic) Fail.Toxic else Fail.Poison
2226
2209
  else
2227
- .None
2210
+ Fail.None
2228
2211
  else
2229
- .None);
2212
+ Fail.None });
2230
2213
  } else if (foe.active.types.includes(.Poison)) {
2231
2214
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2232
- return log.immune(foe_ident, .None);
2215
+ return log.immune(.{ foe_ident, .None });
2233
2216
  }
2234
2217
 
2235
2218
  if (move.effect == .Poison) {
2236
2219
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2237
- try options.chance.commit(player, .hit);
2238
2220
  } else {
2239
2221
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2240
2222
  return;
@@ -2248,7 +2230,7 @@ pub const Effects = struct {
2248
2230
  foe.active.volatiles.toxic = 0;
2249
2231
  }
2250
2232
 
2251
- try log.status(foe_ident, foe_stored.status, .None);
2233
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2252
2234
  }
2253
2235
 
2254
2236
  fn rage(battle: anytype, player: Player) !void {
@@ -2268,22 +2250,23 @@ pub const Effects = struct {
2268
2250
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2269
2251
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2270
2252
 
2271
- try options.log.damageOf(
2253
+ try options.log.damage(.{
2272
2254
  battle.active(player),
2273
2255
  stored,
2274
2256
  .RecoilOf,
2275
2257
  battle.active(player.foe()),
2276
- );
2258
+ });
2277
2259
  if (showdown and stored.hp == 0) residual.* = false;
2278
2260
  }
2279
2261
 
2280
2262
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2281
2263
  var side = battle.side(player);
2264
+ const ident = battle.active(player);
2282
2265
 
2283
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2266
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2284
2267
  side.active.volatiles.Reflect = true;
2285
2268
 
2286
- try options.log.start(battle.active(player), .Reflect);
2269
+ try options.log.start(.{ ident, .Reflect });
2287
2270
  }
2288
2271
 
2289
2272
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2297,14 +2280,13 @@ pub const Effects = struct {
2297
2280
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2298
2281
  } else {
2299
2282
  if (Status.any(foe_stored.status)) {
2300
- return options.log.fail(
2283
+ return options.log.fail(.{
2301
2284
  foe_ident,
2302
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2303
- );
2285
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2286
+ });
2304
2287
  }
2305
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2288
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2306
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2307
- try options.chance.commit(player, .hit);
2308
2290
  }
2309
2291
 
2310
2292
  // Sleep Clause Mod
@@ -2317,18 +2299,17 @@ pub const Effects = struct {
2317
2299
 
2318
2300
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2319
2301
  const last = battle.side(player).last_selected_move;
2320
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2302
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2321
2303
  }
2322
2304
 
2323
2305
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2324
- try options.log.activate(battle.active(player), .Splash);
2306
+ try options.log.activate(.{ battle.active(player), .Splash });
2325
2307
  }
2326
2308
 
2327
2309
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2328
2310
  var side = battle.side(player);
2329
2311
  if (side.active.volatiles.Substitute) {
2330
- try options.log.fail(battle.active(player), .Substitute);
2331
- return;
2312
+ return options.log.fail(.{ battle.active(player), .Substitute });
2332
2313
  }
2333
2314
 
2334
2315
  assert(side.stored().stats.hp <= 1023);
@@ -2340,17 +2321,16 @@ pub const Effects = struct {
2340
2321
  // avoid having to convert to floating point)
2341
2322
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2342
2323
  if (side.stored().hp < required_hp) {
2343
- try options.log.fail(battle.active(player), .Weak);
2344
- return;
2324
+ return options.log.fail(.{ battle.active(player), .Weak });
2345
2325
  }
2346
2326
 
2347
2327
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2348
2328
  side.stored().hp -= hp;
2349
2329
  side.active.volatiles.substitute = hp + 1;
2350
2330
  side.active.volatiles.Substitute = true;
2351
- try options.log.start(battle.active(player), .Substitute);
2331
+ try options.log.start(.{ battle.active(player), .Substitute });
2352
2332
  if (hp > 0) {
2353
- try options.log.damage(battle.active(player), side.stored(), .None);
2333
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2354
2334
  if (showdown and side.stored().hp == 0) residual.* = false;
2355
2335
  }
2356
2336
  }
@@ -2359,13 +2339,11 @@ pub const Effects = struct {
2359
2339
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2360
2340
  if (showdown) {
2361
2341
  if (battle.side(player).last_selected_move == .Teleport) return;
2362
- if (try checkHit(battle, player, move, options)) {
2363
- try options.chance.commit(player, .hit);
2364
- }
2342
+ _ = try checkHit(battle, player, move, options);
2365
2343
  battle.last_damage = 0;
2366
2344
  } else {
2367
- try options.log.fail(battle.active(player), .None);
2368
- try options.log.laststill();
2345
+ try options.log.fail(.{ battle.active(player), .None });
2346
+ try options.log.laststill(.{});
2369
2347
  }
2370
2348
  }
2371
2349
 
@@ -2404,7 +2382,7 @@ pub const Effects = struct {
2404
2382
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2405
2383
  }
2406
2384
 
2407
- try options.log.transform(battle.active(player), foe_ident);
2385
+ try options.log.transform(.{ battle.active(player), foe_ident });
2408
2386
  }
2409
2387
 
2410
2388
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2434,87 +2412,87 @@ pub const Effects = struct {
2434
2412
  switch (move.effect) {
2435
2413
  .AttackUp1, .AttackUp2, .Rage => {
2436
2414
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2437
- if (boosts.atk == 6) return try log.fail(ident, .None);
2415
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2438
2416
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2439
2417
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2440
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2418
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2441
2419
  if (stats.atk == MAX_STAT_VALUE) {
2442
2420
  boosts.atk -= 1;
2443
2421
  if (showdown) {
2444
- try log.boost(ident, reason, n);
2445
- try log.boost(ident, Boost.Attack, -1);
2422
+ try log.boost(.{ ident, reason, n });
2423
+ try log.boost(.{ ident, .Attack, -1 });
2446
2424
  if (move.effect == .Rage) return;
2447
2425
  }
2448
- return try log.fail(ident, .None);
2426
+ return log.fail(.{ ident, .None });
2449
2427
  }
2450
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2428
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2451
2429
  const stat = unmodifiedStats(battle, side).atk;
2452
2430
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2453
- try log.boost(ident, reason, n);
2431
+ try log.boost(.{ ident, reason, n });
2454
2432
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2455
2433
  if (showdown and move.effect == .Rage) return;
2456
2434
  },
2457
2435
  .DefenseUp1, .DefenseUp2 => {
2458
2436
  assert(boosts.def >= -6 and boosts.def <= 6);
2459
- if (boosts.def == 6) return try log.fail(ident, .None);
2437
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2460
2438
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2461
2439
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2462
2440
  if (stats.def == MAX_STAT_VALUE) {
2463
2441
  boosts.def -= 1;
2464
2442
  if (showdown) {
2465
- try log.boost(ident, .Defense, n);
2466
- try log.boost(ident, .Defense, -1);
2443
+ try log.boost(.{ ident, .Defense, n });
2444
+ try log.boost(.{ ident, .Defense, -1 });
2467
2445
  }
2468
- return try log.fail(ident, .None);
2446
+ return log.fail(.{ ident, .None });
2469
2447
  }
2470
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2448
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2471
2449
  const stat = unmodifiedStats(battle, side).def;
2472
2450
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2473
- try log.boost(ident, .Defense, n);
2451
+ try log.boost(.{ ident, .Defense, n });
2474
2452
  },
2475
2453
  .SpeedUp2 => {
2476
2454
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2477
- if (boosts.spe == 6) return try log.fail(ident, .None);
2455
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2478
2456
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2479
2457
  if (stats.spe == MAX_STAT_VALUE) {
2480
2458
  boosts.spe -= 1;
2481
2459
  if (showdown) {
2482
- try log.boost(ident, .Speed, 2);
2483
- try log.boost(ident, .Speed, -1);
2460
+ try log.boost(.{ ident, .Speed, 2 });
2461
+ try log.boost(.{ ident, .Speed, -1 });
2484
2462
  }
2485
- return try log.fail(ident, .None);
2463
+ return log.fail(.{ ident, .None });
2486
2464
  }
2487
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2465
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2488
2466
  const stat = unmodifiedStats(battle, side).spe;
2489
2467
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2490
- try log.boost(ident, .Speed, 2);
2468
+ try log.boost(.{ ident, .Speed, 2 });
2491
2469
  },
2492
2470
  .SpecialUp1, .SpecialUp2 => {
2493
2471
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2494
- if (boosts.spc == 6) return try log.fail(ident, .None);
2472
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2495
2473
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2496
2474
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2497
2475
  if (stats.spc == MAX_STAT_VALUE) {
2498
2476
  boosts.spc -= 1;
2499
2477
  if (showdown) {
2500
- try log.boost(ident, .SpecialAttack, n);
2501
- try log.boost(ident, .SpecialAttack, -1);
2502
- try log.boost(ident, .SpecialDefense, n);
2503
- try log.boost(ident, .SpecialDefense, -1);
2478
+ try log.boost(.{ ident, .SpecialAttack, n });
2479
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2480
+ try log.boost(.{ ident, .SpecialDefense, n });
2481
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2504
2482
  }
2505
- return try log.fail(ident, .None);
2483
+ return log.fail(.{ ident, .None });
2506
2484
  }
2507
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2485
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2508
2486
  const stat = unmodifiedStats(battle, side).spc;
2509
2487
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2510
- try log.boost(ident, .SpecialAttack, n);
2511
- try log.boost(ident, .SpecialDefense, n);
2488
+ try log.boost(.{ ident, .SpecialAttack, n });
2489
+ try log.boost(.{ ident, .SpecialDefense, n });
2512
2490
  },
2513
2491
  .EvasionUp1 => {
2514
2492
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2515
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2493
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2516
2494
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2517
- try log.boost(ident, .Evasion, 1);
2495
+ try log.boost(.{ ident, .Evasion, 1 });
2518
2496
  },
2519
2497
  else => unreachable,
2520
2498
  }
@@ -2529,7 +2507,7 @@ pub const Effects = struct {
2529
2507
  const foe_ident = battle.active(player.foe());
2530
2508
  const secondary = move.effect.isStatDownChance();
2531
2509
 
2532
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2510
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2533
2511
 
2534
2512
  if (secondary) {
2535
2513
  const proc = try Rolls.unboost(battle, player, options);
@@ -2537,7 +2515,6 @@ pub const Effects = struct {
2537
2515
  } else {
2538
2516
  // checkHit already checks for Invulnerable
2539
2517
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2540
- try options.chance.commit(player, .hit);
2541
2518
  }
2542
2519
 
2543
2520
  var stats = &foe.active.stats;
@@ -2547,82 +2524,82 @@ pub const Effects = struct {
2547
2524
  switch (move.effect) {
2548
2525
  .AttackDown1, .AttackDownChance => {
2549
2526
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2550
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2527
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2551
2528
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2552
2529
  if (stats.atk == 1) {
2553
2530
  boosts.atk += 1;
2554
2531
  if (showdown) {
2555
- try log.boost(foe_ident, .Attack, -1);
2556
- try log.boost(foe_ident, .Attack, 1);
2532
+ try log.boost(.{ foe_ident, .Attack, -1 });
2533
+ try log.boost(.{ foe_ident, .Attack, 1 });
2557
2534
  }
2558
- return try log.fail(foe_ident, .None);
2535
+ return log.fail(.{ foe_ident, .None });
2559
2536
  }
2560
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2537
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2561
2538
  const stat = unmodifiedStats(battle, foe).atk;
2562
2539
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2563
- try log.boost(foe_ident, .Attack, -1);
2540
+ try log.boost(.{ foe_ident, .Attack, -1 });
2564
2541
  },
2565
2542
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2566
2543
  assert(boosts.def >= -6 and boosts.def <= 6);
2567
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2544
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2568
2545
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2569
2546
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2570
2547
  if (stats.def == 1) {
2571
2548
  boosts.def += 1;
2572
2549
  if (showdown) {
2573
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2574
- try log.boost(foe_ident, .Defense, 1);
2550
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2551
+ try log.boost(.{ foe_ident, .Defense, 1 });
2575
2552
  }
2576
- return try log.fail(foe_ident, .None);
2553
+ return log.fail(.{ foe_ident, .None });
2577
2554
  }
2578
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2555
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2579
2556
  const stat = unmodifiedStats(battle, foe).def;
2580
2557
  stats.def = @max(1, stat * mod[0] / mod[1]);
2581
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2558
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2582
2559
  },
2583
2560
  .SpeedDown1, .SpeedDownChance => {
2584
2561
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2585
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2562
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2586
2563
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2587
2564
  if (stats.spe == 1) {
2588
2565
  boosts.spe += 1;
2589
2566
  if (showdown) {
2590
- try log.boost(foe_ident, .Speed, -1);
2591
- try log.boost(foe_ident, .Speed, 1);
2567
+ try log.boost(.{ foe_ident, .Speed, -1 });
2568
+ try log.boost(.{ foe_ident, .Speed, 1 });
2592
2569
  }
2593
- return try log.fail(foe_ident, .None);
2570
+ return log.fail(.{ foe_ident, .None });
2594
2571
  }
2595
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2572
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2596
2573
  const stat = unmodifiedStats(battle, foe).spe;
2597
2574
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2598
- try log.boost(foe_ident, .Speed, -1);
2575
+ try log.boost(.{ foe_ident, .Speed, -1 });
2599
2576
  assert(boosts.spe >= -6);
2600
2577
  },
2601
2578
  .SpecialDownChance => {
2602
2579
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2603
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2580
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2604
2581
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2605
2582
  if (stats.spc == 1) {
2606
2583
  boosts.spc += 1;
2607
2584
  if (showdown) {
2608
- try log.boost(foe_ident, .SpecialAttack, -1);
2609
- try log.boost(foe_ident, .SpecialAttack, 1);
2610
- try log.boost(foe_ident, .SpecialDefense, -1);
2611
- try log.boost(foe_ident, .SpecialDefense, 1);
2585
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2586
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2587
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2588
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2612
2589
  }
2613
- return try log.fail(foe_ident, .None);
2590
+ return log.fail(.{ foe_ident, .None });
2614
2591
  }
2615
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2592
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2616
2593
  const stat = unmodifiedStats(battle, foe).spc;
2617
2594
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2618
- try log.boost(foe_ident, .SpecialAttack, -1);
2619
- try log.boost(foe_ident, .SpecialDefense, -1);
2595
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2596
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2620
2597
  },
2621
2598
  .AccuracyDown1 => {
2622
2599
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2623
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2600
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2624
2601
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2625
- try log.boost(foe_ident, .Accuracy, -1);
2602
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2626
2603
  },
2627
2604
  else => unreachable,
2628
2605
  }
@@ -2646,7 +2623,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2646
2623
  }
2647
2624
  }
2648
2625
 
2649
- inline fn isForced(active: anytype) bool {
2626
+ fn isForced(active: anytype) bool {
2650
2627
  return active.volatiles.Recharging or active.volatiles.Rage or
2651
2628
  active.volatiles.Thrashing or active.volatiles.Charging;
2652
2629
  }
@@ -2662,24 +2639,24 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2662
2639
  if (volatiles.disable_move != 0) {
2663
2640
  volatiles.disable_move = 0;
2664
2641
  volatiles.disable_duration = 0;
2665
- try log.end(ident, .DisableSilent);
2642
+ try log.end(.{ ident, .DisableSilent });
2666
2643
  }
2667
2644
  if (volatiles.Confusion) {
2668
2645
  // volatiles.confusion is left unchanged
2669
2646
  volatiles.Confusion = false;
2670
- try log.end(ident, .ConfusionSilent);
2647
+ try log.end(.{ ident, .ConfusionSilent });
2671
2648
  }
2672
2649
  if (volatiles.Mist) {
2673
2650
  volatiles.Mist = false;
2674
- try log.end(ident, .MistSilent);
2651
+ try log.end(.{ ident, .MistSilent });
2675
2652
  }
2676
2653
  if (volatiles.FocusEnergy) {
2677
2654
  volatiles.FocusEnergy = false;
2678
- try log.end(ident, .FocusEnergySilent);
2655
+ try log.end(.{ ident, .FocusEnergySilent });
2679
2656
  }
2680
2657
  if (volatiles.LeechSeed) {
2681
2658
  volatiles.LeechSeed = false;
2682
- try log.end(ident, .LeechSeedSilent);
2659
+ try log.end(.{ ident, .LeechSeedSilent });
2683
2660
  }
2684
2661
  if (!showdown and volatiles.Toxic) {
2685
2662
  volatiles.Toxic = false;
@@ -2687,24 +2664,26 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2687
2664
  }
2688
2665
  if (volatiles.LightScreen) {
2689
2666
  volatiles.LightScreen = false;
2690
- try log.end(ident, .LightScreenSilent);
2667
+ try log.end(.{ ident, .LightScreenSilent });
2691
2668
  }
2692
2669
  if (volatiles.Reflect) {
2693
2670
  volatiles.Reflect = false;
2694
- try log.end(ident, .ReflectSilent);
2671
+ try log.end(.{ ident, .ReflectSilent });
2695
2672
  }
2696
2673
  if (showdown and volatiles.Toxic) {
2697
2674
  volatiles.Toxic = false;
2698
2675
  // Pokémon Showdown erroneously clears the toxic counter
2699
2676
  volatiles.toxic = 0;
2700
- try log.end(ident, .ToxicSilent);
2677
+ try log.end(.{ ident, .ToxicSilent });
2701
2678
  }
2702
2679
  }
2703
2680
 
2704
2681
  pub const Rolls = struct {
2705
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2682
+ fn speedTie(battle: anytype, options: anytype) !bool {
2706
2683
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2707
2684
  player == .P1
2685
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2686
+ battle.rng.speedTie()
2708
2687
  else if (showdown)
2709
2688
  battle.rng.range(u8, 0, 2) == 0
2710
2689
  else
@@ -2714,21 +2693,31 @@ pub const Rolls = struct {
2714
2693
  return p1;
2715
2694
  }
2716
2695
 
2717
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2696
+ fn criticalHit(
2697
+ battle: anytype,
2698
+ player: Player,
2699
+ rate: u8,
2700
+ spurious: bool,
2701
+ options: anytype,
2702
+ ) !bool {
2718
2703
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2719
2704
  val == .true
2705
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2706
+ battle.rng.criticalHit(player, rate)
2720
2707
  else if (showdown)
2721
2708
  battle.rng.chance(u8, rate, 256)
2722
2709
  else
2723
2710
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2724
2711
 
2725
- try options.chance.criticalHit(player, crit, rate);
2712
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2726
2713
  return crit;
2727
2714
  }
2728
2715
 
2729
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2716
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2730
2717
  const roll = if (options.calc.overridden(player, .damage)) |val|
2731
2718
  val
2719
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2720
+ battle.rng.damage(player)
2732
2721
  else roll: {
2733
2722
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2734
2723
  while (true) {
@@ -2742,21 +2731,27 @@ pub const Rolls = struct {
2742
2731
  return roll;
2743
2732
  }
2744
2733
 
2745
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2734
+ const uN = if (pkmn.options.miss) u8 else u9;
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2736
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2746
2737
  const ok = if (options.calc.overridden(player, .hit)) |val|
2747
2738
  val == .true
2739
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2740
+ battle.rng.hit(player, acc)
2748
2741
  else if (showdown)
2749
- battle.rng.chance(u8, accuracy, 256)
2742
+ battle.rng.chance(uN, acc, 256)
2750
2743
  else
2751
- battle.rng.next() < accuracy;
2744
+ battle.rng.next() < acc;
2752
2745
 
2753
- options.chance.hit(ok, accuracy);
2746
+ try options.chance.hit(player, ok, acc);
2754
2747
  return ok;
2755
2748
  }
2756
2749
 
2757
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2750
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2758
2751
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2759
2752
  val == .true
2753
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2754
+ battle.rng.confused(player)
2760
2755
  else if (showdown)
2761
2756
  !battle.rng.chance(u8, 128, 256)
2762
2757
  else
@@ -2766,9 +2761,11 @@ pub const Rolls = struct {
2766
2761
  return cfz;
2767
2762
  }
2768
2763
 
2769
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2770
2765
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2771
2766
  val == .true
2767
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2768
+ battle.rng.paralyzed(player)
2772
2769
  else if (showdown)
2773
2770
  battle.rng.chance(u8, 63, 256)
2774
2771
  else
@@ -2778,9 +2775,11 @@ pub const Rolls = struct {
2778
2775
  return par;
2779
2776
  }
2780
2777
 
2781
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2778
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2782
2779
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2783
2780
  val == .true
2781
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2782
+ battle.rng.secondaryChance(player, 25)
2784
2783
  else if (showdown)
2785
2784
  battle.rng.chance(u8, 25, 256)
2786
2785
  else
@@ -2790,11 +2789,13 @@ pub const Rolls = struct {
2790
2789
  return proc;
2791
2790
  }
2792
2791
 
2793
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2792
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2794
2793
  const rate: u8 = if (low) 26 else 77;
2795
2794
 
2796
2795
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2797
2796
  val == .true
2797
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2798
+ battle.rng.secondaryChance(player, rate)
2798
2799
  else if (showdown)
2799
2800
  battle.rng.chance(u8, rate, 256)
2800
2801
  else
@@ -2804,11 +2805,13 @@ pub const Rolls = struct {
2804
2805
  return proc;
2805
2806
  }
2806
2807
 
2807
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2808
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2808
2809
  const rate: u8 = if (low) 52 else 103;
2809
2810
 
2810
2811
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2811
2812
  val == .true
2813
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2814
+ battle.rng.secondaryChance(player, rate)
2812
2815
  else if (showdown)
2813
2816
  battle.rng.chance(u8, rate, 256)
2814
2817
  else
@@ -2818,9 +2821,11 @@ pub const Rolls = struct {
2818
2821
  return proc;
2819
2822
  }
2820
2823
 
2821
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2824
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2822
2825
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2823
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2828
+ battle.rng.secondaryChance(player, 85)
2824
2829
  else if (showdown)
2825
2830
  battle.rng.chance(u8, 85, 256)
2826
2831
  else
@@ -2830,9 +2835,11 @@ pub const Rolls = struct {
2830
2835
  return proc;
2831
2836
  }
2832
2837
 
2833
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2838
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2834
2839
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2835
2840
  val
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2842
+ battle.rng.metronome(player)
2836
2843
  else if (showdown)
2837
2844
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2838
2845
  else move: {
@@ -2848,9 +2855,11 @@ pub const Rolls = struct {
2848
2855
  return move;
2849
2856
  }
2850
2857
 
2851
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2858
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2852
2859
  const power = if (options.calc.overridden(player, .psywave)) |val|
2853
2860
  val - 1
2861
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2862
+ battle.rng.psywave(player)
2854
2863
  else if (showdown)
2855
2864
  battle.rng.range(u8, 0, max)
2856
2865
  else power: {
@@ -2865,9 +2874,11 @@ pub const Rolls = struct {
2865
2874
  return power;
2866
2875
  }
2867
2876
 
2868
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2877
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2869
2878
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2870
2879
  @intCast(val)
2880
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
+ battle.rng.sleepDuration(player)
2871
2882
  else if (showdown)
2872
2883
  battle.rng.range(u3, 1, 8)
2873
2884
  else duration: {
@@ -2882,9 +2893,11 @@ pub const Rolls = struct {
2882
2893
  return duration;
2883
2894
  }
2884
2895
 
2885
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2896
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2886
2897
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2887
2898
  val
2899
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
+ battle.rng.disableDuration(player)
2888
2901
  else if (showdown)
2889
2902
  battle.rng.range(u4, 1, 9)
2890
2903
  else
@@ -2895,9 +2908,11 @@ pub const Rolls = struct {
2895
2908
  return duration;
2896
2909
  }
2897
2910
 
2898
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2911
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2899
2912
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2900
2913
  @intCast(val)
2914
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2915
+ battle.rng.confusionDuration(player, self)
2901
2916
  else if (showdown)
2902
2917
  battle.rng.range(u3, 2, 6)
2903
2918
  else
@@ -2908,9 +2923,11 @@ pub const Rolls = struct {
2908
2923
  return duration;
2909
2924
  }
2910
2925
 
2911
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2926
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2912
2927
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2913
2928
  @intCast(val)
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
+ battle.rng.attackingDuration(player)
2914
2931
  else if (showdown)
2915
2932
  battle.rng.range(u3, 2, 4)
2916
2933
  else
@@ -2923,7 +2940,7 @@ pub const Rolls = struct {
2923
2940
 
2924
2941
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2925
2942
 
2926
- inline fn distribution(
2943
+ fn distribution(
2927
2944
  battle: anytype,
2928
2945
  comptime effect: Move.Effect,
2929
2946
  player: Player,
@@ -2932,6 +2949,8 @@ pub const Rolls = struct {
2932
2949
  const roll = if (effect == .Binding) .duration else .multi_hit;
2933
2950
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2934
2951
  @intCast(val)
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
+ battle.rng.distribution(player)
2935
2954
  else if (showdown)
2936
2955
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2937
2956
  else n: {
@@ -2964,7 +2983,9 @@ pub const Rolls = struct {
2964
2983
  else
2965
2984
  null;
2966
2985
 
2967
- const slot: u4 = overridden orelse slot: {
2986
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2987
+ battle.rng.moveSlot(player, moves, check_pp)
2988
+ else slot: {
2968
2989
  if (showdown) {
2969
2990
  if (check_pp == 0) {
2970
2991
  var i: usize = moves.len;
@@ -3044,11 +3065,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3044
3065
  const side = battle.side(player);
3045
3066
  const foe = battle.foe(player);
3046
3067
 
3047
- var active = &side.active;
3068
+ const active = &side.active;
3048
3069
  const stored = side.stored();
3049
3070
 
3050
3071
  // While players are not given any input options on the cartridge in these cases,
3051
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3072
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3052
3073
  //
3053
3074
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3054
3075
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3056,7 +3077,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3056
3077
  // passing due to being forced into using a move. Instead of introducing another option
3057
3078
  // we simply repurpose Move with no move slot, even though pedantically this is not
3058
3079
  // strictly correct as the player would not have been presented the option to move or
3059
- // switch at all.
3080
+ // switch at all
3060
3081
  if (isForced(active)) {
3061
3082
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3062
3083
  n += 1;
@@ -3075,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3075
3096
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3076
3097
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3077
3098
  // either disables all other moves in the case of limited or requires you to select a
3078
- // move normally if sleeping/frozen/bound.
3099
+ // move normally if sleeping/frozen/bound
3079
3100
  if (!showdown and (limited or foe.active.volatiles.Binding or
3080
3101
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3081
3102
  {
@@ -3088,7 +3109,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3088
3109
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3089
3110
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3090
3111
  // Move will not result in forcing the subsequent use unless the user also had the
3091
- // proc-ed move in their moveset) and disabling all other moves.
3112
+ // proc-ed move in their moveset) and disabling all other moves
3092
3113
  if (limited) {
3093
3114
  assert(showdown);
3094
3115
  assert(side.last_selected_move != .None);