@pkmn/engine 0.1.0-dev.9f75c7ae → 0.1.0-dev.a0167a9d

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 +82 -90
  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 +11 -11
  26. package/src/lib/gen1/calc.zig +269 -97
  27. package/src/lib/gen1/chance.zig +126 -160
  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 +191 -206
  31. package/src/lib/gen1/mechanics.zig +308 -285
  32. package/src/lib/gen2/chance.zig +211 -121
  33. package/src/lib/gen2/data.zig +13 -11
  34. package/src/lib/gen2/generated.zig +15 -24
  35. package/src/lib/gen2/helpers.zig +37 -7
  36. package/src/lib/gen2/mechanics.zig +855 -413
  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
@@ -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);
@@ -1443,20 +1441,20 @@ fn checkFaint(
1443
1441
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
1442
  const more = player_out or foe_out;
1445
1443
 
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;
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;
1448
1446
 
1449
1447
  assert(!side.active.volatiles.MultiHit);
1450
1448
  assert(!foe.active.volatiles.MultiHit);
1451
1449
 
1452
1450
  if (player_out and foe_out) {
1453
- try options.log.tie();
1451
+ try options.log.tie(.{});
1454
1452
  return Result.Tie;
1455
1453
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1454
+ try options.log.win(.{player.foe()});
1457
1455
  return if (player == .P1) Result.Lose else Result.Win;
1458
1456
  } else if (foe_out) {
1459
- try options.log.win(player);
1457
+ try options.log.win(.{player});
1460
1458
  return if (player == .P1) Result.Win else Result.Lose;
1461
1459
  }
1462
1460
 
@@ -1465,7 +1463,7 @@ fn checkFaint(
1465
1463
  return .{ .p1 = foe_choice, .p2 = .Switch };
1466
1464
  }
1467
1465
 
1468
- 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 {
1469
1467
  var side = battle.side(player);
1470
1468
  var foe = battle.foe(player);
1471
1469
  assert(side.stored().hp == 0);
@@ -1498,8 +1496,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1496
  side.stored().stats.spe;
1499
1497
  }
1500
1498
 
1501
- try options.log.faint(battle.active(player), done);
1502
- 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);
1503
1503
  return null;
1504
1504
  }
1505
1505
 
@@ -1520,8 +1520,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1520
 
1521
1521
  if (!showdown or damage > 0) {
1522
1522
  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);
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 });
1525
1525
  }
1526
1526
  }
1527
1527
 
@@ -1546,14 +1546,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1546
  const amount = @min(damage, stored.hp);
1547
1547
  stored.hp -= amount;
1548
1548
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- 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 });
1551
1551
 
1552
1552
  const before = foe_stored.hp;
1553
1553
  // Uncapped damage is added back to the foe
1554
1554
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1555
  // 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);
1556
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1557
  }
1558
1558
  }
1559
1559
 
@@ -1564,18 +1564,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1564
  battle.turn += 1;
1565
1565
 
1566
1566
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1567
+ try options.log.tie(.{});
1568
1568
  return Result.Tie;
1569
1569
  }
1570
1570
 
1571
1571
  if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1572
+ try options.log.tie(.{});
1573
1573
  return Result.Tie;
1574
1574
  } else if (battle.turn >= 65535) {
1575
1575
  return Result.Error;
1576
1576
  }
1577
1577
 
1578
- try options.log.turn(battle.turn);
1578
+ try options.log.turn(.{battle.turn});
1579
1579
 
1580
1580
  return Result.Default;
1581
1581
  }
@@ -1645,7 +1645,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1645
  side.last_selected_move;
1646
1646
  if (m != .None and m == last) {
1647
1647
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1648
+ try options.log.cant(.{ ident, .Disable, last });
1649
1649
  return true;
1650
1650
  }
1651
1651
  }
@@ -1678,7 +1678,6 @@ fn onBegin(
1678
1678
  residual: *bool,
1679
1679
  options: anytype,
1680
1680
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1681
  return switch (move.effect) {
1683
1682
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1683
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,7 +1700,6 @@ fn onBegin(
1701
1700
  }
1702
1701
 
1703
1702
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1705
1703
  return switch (move.effect) {
1706
1704
  .Bide => Effects.bide(battle, player, options),
1707
1705
  // zig fmt: off
@@ -1723,7 +1721,6 @@ fn alwaysHappens(
1723
1721
  residual: *bool,
1724
1722
  options: anytype,
1725
1723
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1724
  return switch (move.effect) {
1728
1725
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1726
  .Explode => Effects.explode(battle, player),
@@ -1762,7 +1759,7 @@ pub const Effects = struct {
1762
1759
  side.active.volatiles.state = 0;
1763
1760
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1764
1761
 
1765
- try options.log.start(battle.active(player), .Bide);
1762
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1763
  }
1767
1764
 
1768
1765
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1773,11 @@ pub const Effects = struct {
1776
1773
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1774
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1775
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1776
+ try options.log.curestatus(.{
1780
1777
  battle.active(player.foe()),
1781
1778
  foe_stored.status,
1782
1779
  .Message,
1783
- );
1780
+ });
1784
1781
  foe_stored.status = 0;
1785
1782
  }
1786
1783
  return;
@@ -1794,7 +1791,7 @@ pub const Effects = struct {
1794
1791
  foe_stored.status = Status.init(.BRN);
1795
1792
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1793
 
1797
- 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 });
1798
1795
  }
1799
1796
 
1800
1797
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1801,8 @@ pub const Effects = struct {
1804
1801
  volatiles.Charging = true;
1805
1802
  const move = side.last_selected_move;
1806
1803
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1804
+ try options.log.laststill(.{});
1805
+ try options.log.prepare(.{ battle.active(player), move });
1809
1806
  }
1810
1807
 
1811
1808
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,42 +1813,36 @@ pub const Effects = struct {
1816
1813
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1814
  } else {
1818
1815
  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
- }
1816
+ if (!try checkHit(battle, player, move, options)) return;
1817
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1818
  } 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
- }
1819
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1820
+ if (!try checkHit(battle, player, move, options)) return;
1830
1821
  }
1831
- try options.chance.commit(player, .hit);
1832
1822
  }
1833
1823
 
1834
1824
  if (foe.active.volatiles.Confusion) return;
1835
1825
  foe.active.volatiles.Confusion = true;
1836
1826
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1827
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1828
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1829
  }
1840
1830
 
1841
1831
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1832
  const foe = battle.foe(player);
1843
1833
 
1844
1834
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1835
+ try options.log.lastmiss(.{});
1836
+ return options.log.miss(.{battle.active(player)});
1847
1837
  }
1848
1838
 
1849
1839
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1840
+ return options.log.start(.{
1851
1841
  battle.active(player),
1842
+ .TypeChange,
1852
1843
  foe.active.types,
1853
1844
  battle.active(player.foe()),
1854
- );
1845
+ });
1855
1846
  }
1856
1847
 
1857
1848
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1860,13 +1851,10 @@ pub const Effects = struct {
1860
1851
  const foe_ident = battle.active(player.foe());
1861
1852
 
1862
1853
  // 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
- }
1854
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1867
1855
 
1868
1856
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1857
+ try options.log.fail(.{ foe_ident, .None });
1870
1858
  return null;
1871
1859
  }
1872
1860
 
@@ -1882,7 +1870,7 @@ pub const Effects = struct {
1882
1870
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1871
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
1872
  if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1873
+ try options.log.fail(.{ foe_ident, .None });
1886
1874
  return null;
1887
1875
  } else if (err) {
1888
1876
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1895,12 +1883,13 @@ pub const Effects = struct {
1895
1883
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1896
1884
 
1897
1885
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1886
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1887
  return null;
1900
1888
  }
1901
1889
 
1902
1890
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1891
  var stored = battle.side(player).stored();
1892
+ const ident = battle.active(player);
1904
1893
 
1905
1894
  if (battle.last_damage == 0) {
1906
1895
  assert(showdown);
@@ -1913,7 +1902,7 @@ pub const Effects = struct {
1913
1902
  if (stored.hp == stored.stats.hp) return;
1914
1903
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1904
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1905
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1906
  }
1918
1907
 
1919
1908
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +1934,7 @@ pub const Effects = struct {
1945
1934
  if (side.active.volatiles.FocusEnergy) return;
1946
1935
  side.active.volatiles.FocusEnergy = true;
1947
1936
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
1937
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
1938
  }
1950
1939
 
1951
1940
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1967,7 +1956,7 @@ pub const Effects = struct {
1967
1956
  foe_stored.status = Status.init(.FRZ);
1968
1957
  // GLITCH: Hyper Beam recharging status is not cleared
1969
1958
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
1959
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
1960
  }
1972
1961
 
1973
1962
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1975,7 +1964,7 @@ pub const Effects = struct {
1975
1964
  var side = battle.side(player);
1976
1965
  var foe = battle.foe(player);
1977
1966
 
1978
- var side_stored = side.stored();
1967
+ const side_stored = side.stored();
1979
1968
  var foe_stored = foe.stored();
1980
1969
 
1981
1970
  const player_ident = battle.active(player);
@@ -1987,8 +1976,8 @@ pub const Effects = struct {
1987
1976
  side.active.stats = side_stored.stats;
1988
1977
  foe.active.stats = foe_stored.stats;
1989
1978
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
1979
+ try log.activate(.{ player_ident, .Haze });
1980
+ try log.clearallboost(.{});
1992
1981
 
1993
1982
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
1983
  if (showdown) {
@@ -1996,11 +1985,11 @@ pub const Effects = struct {
1996
1985
  const p: Player = @enumFromInt(i);
1997
1986
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
1987
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1988
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
1989
  s.stored().status = 0;
2001
1990
  } else if (showdown and s.stored().status == Status.TOX) {
2002
1991
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
1992
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
1993
  }
2005
1994
  try clearVolatiles(battle, p, p != player, options);
2006
1995
  }
@@ -2009,7 +1998,7 @@ pub const Effects = struct {
2009
1998
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
1999
  foe.last_selected_move = .SKIP_TURN;
2011
2000
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2001
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
2002
  foe_stored.status = 0;
2014
2003
  }
2015
2004
  try clearVolatiles(battle, player, false, options);
@@ -2025,7 +2014,7 @@ pub const Effects = struct {
2025
2014
  // GLITCH: HP recovery move failure glitches
2026
2015
  const delta = stored.stats.hp - stored.hp;
2027
2016
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return try options.log.fail(ident, .None);
2017
+ return options.log.fail(.{ ident, .None });
2029
2018
  }
2030
2019
 
2031
2020
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2022,17 @@ pub const Effects = struct {
2033
2022
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2023
  if (showdown) battle.rng.advance(1);
2035
2024
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2025
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2026
  stored.hp = stored.stats.hp;
2038
2027
  } else {
2039
2028
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2029
  }
2041
- 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 });
2042
2031
  }
2043
2032
 
2044
2033
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2034
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2035
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2036
  }
2048
2037
 
2049
2038
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2041,31 @@ pub const Effects = struct {
2052
2041
  if (showdown) {
2053
2042
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2043
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2044
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2045
  }
2057
2046
  if (!try checkHit(battle, player, move, options)) return;
2058
2047
  if (foe.active.volatiles.LeechSeed) return;
2059
2048
  } else {
2060
2049
  if (!try checkHit(battle, player, move, options)) return;
2061
2050
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2051
+ try options.log.lastmiss(.{});
2052
+ return options.log.miss(.{battle.active(player)});
2064
2053
  }
2065
2054
  }
2066
2055
 
2067
- try options.chance.commit(player, .hit);
2068
2056
  foe.active.volatiles.LeechSeed = true;
2069
2057
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2058
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2059
  }
2072
2060
 
2073
2061
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2062
  var side = battle.side(player);
2063
+ const ident = battle.active(player);
2075
2064
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2065
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2066
  side.active.volatiles.LightScreen = true;
2080
2067
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2068
+ try options.log.start(.{ ident, .LightScreen });
2082
2069
  }
2083
2070
 
2084
2071
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2106,17 +2093,15 @@ pub const Effects = struct {
2106
2093
  if (!has_mimic) {
2107
2094
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2095
  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);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2111
2097
  }
2112
2098
  }
2113
2099
  if (!try checkHit(battle, player, move, options)) return;
2114
- try options.chance.commit(player, .hit);
2115
2100
 
2116
2101
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2102
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2103
 
2119
- 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 });
2120
2105
  }
2121
2106
 
2122
2107
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2110,7 @@ pub const Effects = struct {
2125
2110
  if (side.active.volatiles.Mist) return;
2126
2111
  side.active.volatiles.Mist = true;
2127
2112
 
2128
- try options.log.start(battle.active(player), .Mist);
2113
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2114
  }
2130
2115
 
2131
2116
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,25 +2136,25 @@ pub const Effects = struct {
2151
2136
 
2152
2137
  if (showdown) {
2153
2138
  // 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;
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;
2156
2142
  }
2157
2143
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2144
+ return log.fail(.{
2159
2145
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2146
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2147
+ });
2162
2148
  }
2163
2149
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2150
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2151
  if (!try checkHit(battle, player, move, options)) return;
2166
2152
  }
2167
- try options.chance.commit(player, .hit);
2168
2153
 
2169
2154
  foe_stored.status = Status.init(.PAR);
2170
2155
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2156
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2157
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2158
  }
2174
2159
 
2175
2160
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2176,12 @@ pub const Effects = struct {
2191
2176
  foe_stored.status = Status.init(.PAR);
2192
2177
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2178
 
2194
- 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 });
2195
2180
  }
2196
2181
 
2197
2182
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2183
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2184
+ try options.log.fieldactivate(.{});
2200
2185
  }
2201
2186
  }
2202
2187
 
@@ -2211,28 +2196,27 @@ pub const Effects = struct {
2211
2196
  return;
2212
2197
  } else if (foe.active.volatiles.Substitute) {
2213
2198
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2199
+ return log.fail(.{ foe_ident, .None });
2215
2200
  } else if (Status.any(foe_stored.status)) {
2216
2201
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2202
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2203
  // 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))
2204
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2205
  if (!showdown)
2221
- .Poison
2206
+ Fail.Poison
2222
2207
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2208
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2209
  else
2225
- .None
2210
+ Fail.None
2226
2211
  else
2227
- .None);
2212
+ Fail.None });
2228
2213
  } else if (foe.active.types.includes(.Poison)) {
2229
2214
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2215
+ return log.immune(.{ foe_ident, .None });
2231
2216
  }
2232
2217
 
2233
2218
  if (move.effect == .Poison) {
2234
2219
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2235
- try options.chance.commit(player, .hit);
2236
2220
  } else {
2237
2221
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2238
2222
  return;
@@ -2246,7 +2230,7 @@ pub const Effects = struct {
2246
2230
  foe.active.volatiles.toxic = 0;
2247
2231
  }
2248
2232
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2233
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2234
  }
2251
2235
 
2252
2236
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2250,23 @@ pub const Effects = struct {
2266
2250
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2251
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2252
 
2269
- try options.log.damageOf(
2253
+ try options.log.damage(.{
2270
2254
  battle.active(player),
2271
2255
  stored,
2272
2256
  .RecoilOf,
2273
2257
  battle.active(player.foe()),
2274
- );
2258
+ });
2275
2259
  if (showdown and stored.hp == 0) residual.* = false;
2276
2260
  }
2277
2261
 
2278
2262
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2263
  var side = battle.side(player);
2264
+ const ident = battle.active(player);
2280
2265
 
2281
- 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 });
2282
2267
  side.active.volatiles.Reflect = true;
2283
2268
 
2284
- try options.log.start(battle.active(player), .Reflect);
2269
+ try options.log.start(.{ ident, .Reflect });
2285
2270
  }
2286
2271
 
2287
2272
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,14 +2280,13 @@ pub const Effects = struct {
2295
2280
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2281
  } else {
2297
2282
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2283
+ return options.log.fail(.{
2299
2284
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2285
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2286
+ });
2302
2287
  }
2303
- // 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
2304
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2305
- try options.chance.commit(player, .hit);
2306
2290
  }
2307
2291
 
2308
2292
  // Sleep Clause Mod
@@ -2315,18 +2299,17 @@ pub const Effects = struct {
2315
2299
 
2316
2300
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2317
2301
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2302
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2303
  }
2320
2304
 
2321
2305
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2306
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2307
  }
2324
2308
 
2325
2309
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2310
  var side = battle.side(player);
2327
2311
  if (side.active.volatiles.Substitute) {
2328
- try options.log.fail(battle.active(player), .Substitute);
2329
- return;
2312
+ return options.log.fail(.{ battle.active(player), .Substitute });
2330
2313
  }
2331
2314
 
2332
2315
  assert(side.stored().stats.hp <= 1023);
@@ -2338,17 +2321,16 @@ pub const Effects = struct {
2338
2321
  // avoid having to convert to floating point)
2339
2322
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2340
2323
  if (side.stored().hp < required_hp) {
2341
- try options.log.fail(battle.active(player), .Weak);
2342
- return;
2324
+ return options.log.fail(.{ battle.active(player), .Weak });
2343
2325
  }
2344
2326
 
2345
2327
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2346
2328
  side.stored().hp -= hp;
2347
2329
  side.active.volatiles.substitute = hp + 1;
2348
2330
  side.active.volatiles.Substitute = true;
2349
- try options.log.start(battle.active(player), .Substitute);
2331
+ try options.log.start(.{ battle.active(player), .Substitute });
2350
2332
  if (hp > 0) {
2351
- try options.log.damage(battle.active(player), side.stored(), .None);
2333
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2352
2334
  if (showdown and side.stored().hp == 0) residual.* = false;
2353
2335
  }
2354
2336
  }
@@ -2357,13 +2339,11 @@ pub const Effects = struct {
2357
2339
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2358
2340
  if (showdown) {
2359
2341
  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
- }
2342
+ _ = try checkHit(battle, player, move, options);
2363
2343
  battle.last_damage = 0;
2364
2344
  } else {
2365
- try options.log.fail(battle.active(player), .None);
2366
- try options.log.laststill();
2345
+ try options.log.fail(.{ battle.active(player), .None });
2346
+ try options.log.laststill(.{});
2367
2347
  }
2368
2348
  }
2369
2349
 
@@ -2402,7 +2382,7 @@ pub const Effects = struct {
2402
2382
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2403
2383
  }
2404
2384
 
2405
- try options.log.transform(battle.active(player), foe_ident);
2385
+ try options.log.transform(.{ battle.active(player), foe_ident });
2406
2386
  }
2407
2387
 
2408
2388
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2432,87 +2412,87 @@ pub const Effects = struct {
2432
2412
  switch (move.effect) {
2433
2413
  .AttackUp1, .AttackUp2, .Rage => {
2434
2414
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2435
- if (boosts.atk == 6) return try log.fail(ident, .None);
2415
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2436
2416
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2437
2417
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2438
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2418
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2439
2419
  if (stats.atk == MAX_STAT_VALUE) {
2440
2420
  boosts.atk -= 1;
2441
2421
  if (showdown) {
2442
- try log.boost(ident, reason, n);
2443
- try log.boost(ident, Boost.Attack, -1);
2422
+ try log.boost(.{ ident, reason, n });
2423
+ try log.boost(.{ ident, .Attack, -1 });
2444
2424
  if (move.effect == .Rage) return;
2445
2425
  }
2446
- return try log.fail(ident, .None);
2426
+ return log.fail(.{ ident, .None });
2447
2427
  }
2448
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2428
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2449
2429
  const stat = unmodifiedStats(battle, side).atk;
2450
2430
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2451
- try log.boost(ident, reason, n);
2431
+ try log.boost(.{ ident, reason, n });
2452
2432
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2453
2433
  if (showdown and move.effect == .Rage) return;
2454
2434
  },
2455
2435
  .DefenseUp1, .DefenseUp2 => {
2456
2436
  assert(boosts.def >= -6 and boosts.def <= 6);
2457
- if (boosts.def == 6) return try log.fail(ident, .None);
2437
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2458
2438
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2459
2439
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2460
2440
  if (stats.def == MAX_STAT_VALUE) {
2461
2441
  boosts.def -= 1;
2462
2442
  if (showdown) {
2463
- try log.boost(ident, .Defense, n);
2464
- try log.boost(ident, .Defense, -1);
2443
+ try log.boost(.{ ident, .Defense, n });
2444
+ try log.boost(.{ ident, .Defense, -1 });
2465
2445
  }
2466
- return try log.fail(ident, .None);
2446
+ return log.fail(.{ ident, .None });
2467
2447
  }
2468
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2448
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2469
2449
  const stat = unmodifiedStats(battle, side).def;
2470
2450
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2471
- try log.boost(ident, .Defense, n);
2451
+ try log.boost(.{ ident, .Defense, n });
2472
2452
  },
2473
2453
  .SpeedUp2 => {
2474
2454
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2475
- if (boosts.spe == 6) return try log.fail(ident, .None);
2455
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2476
2456
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2477
2457
  if (stats.spe == MAX_STAT_VALUE) {
2478
2458
  boosts.spe -= 1;
2479
2459
  if (showdown) {
2480
- try log.boost(ident, .Speed, 2);
2481
- try log.boost(ident, .Speed, -1);
2460
+ try log.boost(.{ ident, .Speed, 2 });
2461
+ try log.boost(.{ ident, .Speed, -1 });
2482
2462
  }
2483
- return try log.fail(ident, .None);
2463
+ return log.fail(.{ ident, .None });
2484
2464
  }
2485
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2465
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2486
2466
  const stat = unmodifiedStats(battle, side).spe;
2487
2467
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2488
- try log.boost(ident, .Speed, 2);
2468
+ try log.boost(.{ ident, .Speed, 2 });
2489
2469
  },
2490
2470
  .SpecialUp1, .SpecialUp2 => {
2491
2471
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2492
- if (boosts.spc == 6) return try log.fail(ident, .None);
2472
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2493
2473
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2494
2474
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2495
2475
  if (stats.spc == MAX_STAT_VALUE) {
2496
2476
  boosts.spc -= 1;
2497
2477
  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);
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 });
2502
2482
  }
2503
- return try log.fail(ident, .None);
2483
+ return log.fail(.{ ident, .None });
2504
2484
  }
2505
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2485
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2506
2486
  const stat = unmodifiedStats(battle, side).spc;
2507
2487
  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);
2488
+ try log.boost(.{ ident, .SpecialAttack, n });
2489
+ try log.boost(.{ ident, .SpecialDefense, n });
2510
2490
  },
2511
2491
  .EvasionUp1 => {
2512
2492
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2513
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2493
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2514
2494
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2515
- try log.boost(ident, .Evasion, 1);
2495
+ try log.boost(.{ ident, .Evasion, 1 });
2516
2496
  },
2517
2497
  else => unreachable,
2518
2498
  }
@@ -2527,7 +2507,7 @@ pub const Effects = struct {
2527
2507
  const foe_ident = battle.active(player.foe());
2528
2508
  const secondary = move.effect.isStatDownChance();
2529
2509
 
2530
- 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 });
2531
2511
 
2532
2512
  if (secondary) {
2533
2513
  const proc = try Rolls.unboost(battle, player, options);
@@ -2535,7 +2515,6 @@ pub const Effects = struct {
2535
2515
  } else {
2536
2516
  // checkHit already checks for Invulnerable
2537
2517
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2538
- try options.chance.commit(player, .hit);
2539
2518
  }
2540
2519
 
2541
2520
  var stats = &foe.active.stats;
@@ -2545,82 +2524,82 @@ pub const Effects = struct {
2545
2524
  switch (move.effect) {
2546
2525
  .AttackDown1, .AttackDownChance => {
2547
2526
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2548
- 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 });
2549
2528
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2550
2529
  if (stats.atk == 1) {
2551
2530
  boosts.atk += 1;
2552
2531
  if (showdown) {
2553
- try log.boost(foe_ident, .Attack, -1);
2554
- try log.boost(foe_ident, .Attack, 1);
2532
+ try log.boost(.{ foe_ident, .Attack, -1 });
2533
+ try log.boost(.{ foe_ident, .Attack, 1 });
2555
2534
  }
2556
- return try log.fail(foe_ident, .None);
2535
+ return log.fail(.{ foe_ident, .None });
2557
2536
  }
2558
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2537
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2559
2538
  const stat = unmodifiedStats(battle, foe).atk;
2560
2539
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2561
- try log.boost(foe_ident, .Attack, -1);
2540
+ try log.boost(.{ foe_ident, .Attack, -1 });
2562
2541
  },
2563
2542
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2564
2543
  assert(boosts.def >= -6 and boosts.def <= 6);
2565
- 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 });
2566
2545
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2567
2546
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2568
2547
  if (stats.def == 1) {
2569
2548
  boosts.def += 1;
2570
2549
  if (showdown) {
2571
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2572
- 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 });
2573
2552
  }
2574
- return try log.fail(foe_ident, .None);
2553
+ return log.fail(.{ foe_ident, .None });
2575
2554
  }
2576
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2555
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2577
2556
  const stat = unmodifiedStats(battle, foe).def;
2578
2557
  stats.def = @max(1, stat * mod[0] / mod[1]);
2579
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2558
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2580
2559
  },
2581
2560
  .SpeedDown1, .SpeedDownChance => {
2582
2561
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2583
- 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 });
2584
2563
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2585
2564
  if (stats.spe == 1) {
2586
2565
  boosts.spe += 1;
2587
2566
  if (showdown) {
2588
- try log.boost(foe_ident, .Speed, -1);
2589
- try log.boost(foe_ident, .Speed, 1);
2567
+ try log.boost(.{ foe_ident, .Speed, -1 });
2568
+ try log.boost(.{ foe_ident, .Speed, 1 });
2590
2569
  }
2591
- return try log.fail(foe_ident, .None);
2570
+ return log.fail(.{ foe_ident, .None });
2592
2571
  }
2593
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2572
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2594
2573
  const stat = unmodifiedStats(battle, foe).spe;
2595
2574
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2596
- try log.boost(foe_ident, .Speed, -1);
2575
+ try log.boost(.{ foe_ident, .Speed, -1 });
2597
2576
  assert(boosts.spe >= -6);
2598
2577
  },
2599
2578
  .SpecialDownChance => {
2600
2579
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2601
- 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 });
2602
2581
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2603
2582
  if (stats.spc == 1) {
2604
2583
  boosts.spc += 1;
2605
2584
  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);
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 });
2610
2589
  }
2611
- return try log.fail(foe_ident, .None);
2590
+ return log.fail(.{ foe_ident, .None });
2612
2591
  }
2613
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2592
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2614
2593
  const stat = unmodifiedStats(battle, foe).spc;
2615
2594
  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);
2595
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2596
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2618
2597
  },
2619
2598
  .AccuracyDown1 => {
2620
2599
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2621
- 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 });
2622
2601
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2623
- try log.boost(foe_ident, .Accuracy, -1);
2602
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2624
2603
  },
2625
2604
  else => unreachable,
2626
2605
  }
@@ -2660,24 +2639,24 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2660
2639
  if (volatiles.disable_move != 0) {
2661
2640
  volatiles.disable_move = 0;
2662
2641
  volatiles.disable_duration = 0;
2663
- try log.end(ident, .DisableSilent);
2642
+ try log.end(.{ ident, .DisableSilent });
2664
2643
  }
2665
2644
  if (volatiles.Confusion) {
2666
2645
  // volatiles.confusion is left unchanged
2667
2646
  volatiles.Confusion = false;
2668
- try log.end(ident, .ConfusionSilent);
2647
+ try log.end(.{ ident, .ConfusionSilent });
2669
2648
  }
2670
2649
  if (volatiles.Mist) {
2671
2650
  volatiles.Mist = false;
2672
- try log.end(ident, .MistSilent);
2651
+ try log.end(.{ ident, .MistSilent });
2673
2652
  }
2674
2653
  if (volatiles.FocusEnergy) {
2675
2654
  volatiles.FocusEnergy = false;
2676
- try log.end(ident, .FocusEnergySilent);
2655
+ try log.end(.{ ident, .FocusEnergySilent });
2677
2656
  }
2678
2657
  if (volatiles.LeechSeed) {
2679
2658
  volatiles.LeechSeed = false;
2680
- try log.end(ident, .LeechSeedSilent);
2659
+ try log.end(.{ ident, .LeechSeedSilent });
2681
2660
  }
2682
2661
  if (!showdown and volatiles.Toxic) {
2683
2662
  volatiles.Toxic = false;
@@ -2685,17 +2664,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2664
  }
2686
2665
  if (volatiles.LightScreen) {
2687
2666
  volatiles.LightScreen = false;
2688
- try log.end(ident, .LightScreenSilent);
2667
+ try log.end(.{ ident, .LightScreenSilent });
2689
2668
  }
2690
2669
  if (volatiles.Reflect) {
2691
2670
  volatiles.Reflect = false;
2692
- try log.end(ident, .ReflectSilent);
2671
+ try log.end(.{ ident, .ReflectSilent });
2693
2672
  }
2694
2673
  if (showdown and volatiles.Toxic) {
2695
2674
  volatiles.Toxic = false;
2696
2675
  // Pokémon Showdown erroneously clears the toxic counter
2697
2676
  volatiles.toxic = 0;
2698
- try log.end(ident, .ToxicSilent);
2677
+ try log.end(.{ ident, .ToxicSilent });
2699
2678
  }
2700
2679
  }
2701
2680
 
@@ -2703,6 +2682,8 @@ pub const Rolls = struct {
2703
2682
  fn speedTie(battle: anytype, options: anytype) !bool {
2704
2683
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2705
2684
  player == .P1
2685
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2686
+ battle.rng.speedTie()
2706
2687
  else if (showdown)
2707
2688
  battle.rng.range(u8, 0, 2) == 0
2708
2689
  else
@@ -2712,21 +2693,31 @@ pub const Rolls = struct {
2712
2693
  return p1;
2713
2694
  }
2714
2695
 
2715
- 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 {
2716
2703
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2717
2704
  val == .true
2705
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2706
+ battle.rng.criticalHit(player, rate)
2718
2707
  else if (showdown)
2719
2708
  battle.rng.chance(u8, rate, 256)
2720
2709
  else
2721
2710
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2722
2711
 
2723
- try options.chance.criticalHit(player, crit, rate);
2712
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2724
2713
  return crit;
2725
2714
  }
2726
2715
 
2727
2716
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2728
2717
  const roll = if (options.calc.overridden(player, .damage)) |val|
2729
2718
  val
2719
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2720
+ battle.rng.damage(player)
2730
2721
  else roll: {
2731
2722
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2732
2723
  while (true) {
@@ -2740,21 +2731,27 @@ pub const Rolls = struct {
2740
2731
  return roll;
2741
2732
  }
2742
2733
 
2743
- 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;
2744
2737
  const ok = if (options.calc.overridden(player, .hit)) |val|
2745
2738
  val == .true
2739
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2740
+ battle.rng.hit(player, acc)
2746
2741
  else if (showdown)
2747
- battle.rng.chance(u8, accuracy, 256)
2742
+ battle.rng.chance(uN, acc, 256)
2748
2743
  else
2749
- battle.rng.next() < accuracy;
2744
+ battle.rng.next() < acc;
2750
2745
 
2751
- options.chance.hit(ok, accuracy);
2746
+ try options.chance.hit(player, ok, acc);
2752
2747
  return ok;
2753
2748
  }
2754
2749
 
2755
2750
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2756
2751
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2757
2752
  val == .true
2753
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2754
+ battle.rng.confused(player)
2758
2755
  else if (showdown)
2759
2756
  !battle.rng.chance(u8, 128, 256)
2760
2757
  else
@@ -2767,6 +2764,8 @@ pub const Rolls = struct {
2767
2764
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2768
2765
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2769
2766
  val == .true
2767
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2768
+ battle.rng.paralyzed(player)
2770
2769
  else if (showdown)
2771
2770
  battle.rng.chance(u8, 63, 256)
2772
2771
  else
@@ -2779,6 +2778,8 @@ pub const Rolls = struct {
2779
2778
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2780
2779
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2780
  val == .true
2781
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2782
+ battle.rng.secondaryChance(player, 25)
2782
2783
  else if (showdown)
2783
2784
  battle.rng.chance(u8, 25, 256)
2784
2785
  else
@@ -2793,6 +2794,8 @@ pub const Rolls = struct {
2793
2794
 
2794
2795
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2796
  val == .true
2797
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2798
+ battle.rng.secondaryChance(player, rate)
2796
2799
  else if (showdown)
2797
2800
  battle.rng.chance(u8, rate, 256)
2798
2801
  else
@@ -2807,6 +2810,8 @@ pub const Rolls = struct {
2807
2810
 
2808
2811
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2809
2812
  val == .true
2813
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2814
+ battle.rng.secondaryChance(player, rate)
2810
2815
  else if (showdown)
2811
2816
  battle.rng.chance(u8, rate, 256)
2812
2817
  else
@@ -2819,6 +2824,8 @@ pub const Rolls = struct {
2819
2824
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2820
2825
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2821
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2828
+ battle.rng.secondaryChance(player, 85)
2822
2829
  else if (showdown)
2823
2830
  battle.rng.chance(u8, 85, 256)
2824
2831
  else
@@ -2831,6 +2838,8 @@ pub const Rolls = struct {
2831
2838
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2832
2839
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2833
2840
  val
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2842
+ battle.rng.metronome(player)
2834
2843
  else if (showdown)
2835
2844
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
2845
  else move: {
@@ -2849,6 +2858,8 @@ pub const Rolls = struct {
2849
2858
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2850
2859
  const power = if (options.calc.overridden(player, .psywave)) |val|
2851
2860
  val - 1
2861
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2862
+ battle.rng.psywave(player)
2852
2863
  else if (showdown)
2853
2864
  battle.rng.range(u8, 0, max)
2854
2865
  else power: {
@@ -2866,6 +2877,8 @@ pub const Rolls = struct {
2866
2877
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2867
2878
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2868
2879
  @intCast(val)
2880
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
+ battle.rng.sleepDuration(player)
2869
2882
  else if (showdown)
2870
2883
  battle.rng.range(u3, 1, 8)
2871
2884
  else duration: {
@@ -2883,6 +2896,8 @@ pub const Rolls = struct {
2883
2896
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2884
2897
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2885
2898
  val
2899
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
+ battle.rng.disableDuration(player)
2886
2901
  else if (showdown)
2887
2902
  battle.rng.range(u4, 1, 9)
2888
2903
  else
@@ -2896,6 +2911,8 @@ pub const Rolls = struct {
2896
2911
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2897
2912
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2898
2913
  @intCast(val)
2914
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2915
+ battle.rng.confusionDuration(player, self)
2899
2916
  else if (showdown)
2900
2917
  battle.rng.range(u3, 2, 6)
2901
2918
  else
@@ -2909,6 +2926,8 @@ pub const Rolls = struct {
2909
2926
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2910
2927
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2911
2928
  @intCast(val)
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
+ battle.rng.attackingDuration(player)
2912
2931
  else if (showdown)
2913
2932
  battle.rng.range(u3, 2, 4)
2914
2933
  else
@@ -2930,6 +2949,8 @@ pub const Rolls = struct {
2930
2949
  const roll = if (effect == .Binding) .duration else .multi_hit;
2931
2950
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2932
2951
  @intCast(val)
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
+ battle.rng.distribution(player)
2933
2954
  else if (showdown)
2934
2955
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2935
2956
  else n: {
@@ -2962,7 +2983,9 @@ pub const Rolls = struct {
2962
2983
  else
2963
2984
  null;
2964
2985
 
2965
- 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: {
2966
2989
  if (showdown) {
2967
2990
  if (check_pp == 0) {
2968
2991
  var i: usize = moves.len;
@@ -3042,11 +3065,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3042
3065
  const side = battle.side(player);
3043
3066
  const foe = battle.foe(player);
3044
3067
 
3045
- var active = &side.active;
3068
+ const active = &side.active;
3046
3069
  const stored = side.stored();
3047
3070
 
3048
3071
  // 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.
3072
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3050
3073
  //
3051
3074
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3052
3075
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3054,7 +3077,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3054
3077
  // passing due to being forced into using a move. Instead of introducing another option
3055
3078
  // we simply repurpose Move with no move slot, even though pedantically this is not
3056
3079
  // strictly correct as the player would not have been presented the option to move or
3057
- // switch at all.
3080
+ // switch at all
3058
3081
  if (isForced(active)) {
3059
3082
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3060
3083
  n += 1;
@@ -3073,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3073
3096
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3074
3097
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3075
3098
  // either disables all other moves in the case of limited or requires you to select a
3076
- // move normally if sleeping/frozen/bound.
3099
+ // move normally if sleeping/frozen/bound
3077
3100
  if (!showdown and (limited or foe.active.volatiles.Binding or
3078
3101
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3079
3102
  {
@@ -3086,7 +3109,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3086
3109
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3087
3110
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3088
3111
  // 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.
3112
+ // proc-ed move in their moveset) and disabling all other moves
3090
3113
  if (limited) {
3091
3114
  assert(showdown);
3092
3115
  assert(side.last_selected_move != .None);