@pkmn/engine 0.1.0-dev.a93847b2 → 0.1.0-dev.abd68573

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 (44) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +39 -16
  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 +90 -76
  17. package/package.json +31 -23
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +8 -8
  20. package/src/lib/common/optional.zig +1 -1
  21. package/src/lib/common/protocol.zig +393 -396
  22. package/src/lib/common/rational.zig +11 -7
  23. package/src/lib/common/rng.zig +20 -11
  24. package/src/lib/gen1/calc.zig +7 -7
  25. package/src/lib/gen1/chance.zig +31 -30
  26. package/src/lib/gen1/data/moves.zig +9 -9
  27. package/src/lib/gen1/data/species.zig +2 -2
  28. package/src/lib/gen1/data/types.zig +4 -4
  29. package/src/lib/gen1/data.zig +32 -32
  30. package/src/lib/gen1/helpers.zig +48 -28
  31. package/src/lib/gen1/mechanics.zig +277 -255
  32. package/src/lib/gen2/calc.zig +1 -1
  33. package/src/lib/gen2/chance.zig +42 -41
  34. package/src/lib/gen2/data/items.zig +2 -2
  35. package/src/lib/gen2/data/moves.zig +2 -2
  36. package/src/lib/gen2/data/types.zig +9 -9
  37. package/src/lib/gen2/data.zig +23 -21
  38. package/src/lib/gen2/generated.zig +27 -78
  39. package/src/lib/gen2/mechanics.zig +1052 -459
  40. package/src/lib/{binding/node.zig → node.zig} +9 -12
  41. package/src/lib/pkmn.zig +27 -12
  42. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  43. package/src/lib/gen1/README.md +0 -894
  44. 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
 
@@ -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
 
@@ -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
  }
@@ -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()) {
@@ -901,7 +895,7 @@ 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
@@ -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
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
940
- try log.lastmiss();
941
- try log.miss(battle.active(player));
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
@@ -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;
@@ -1059,7 +1053,7 @@ 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;
@@ -1116,9 +1110,9 @@ fn calcDamage(
1116
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1117
1111
  }
1118
1112
 
1119
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1113
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1120
1114
 
1121
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1115
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1122
1116
 
1123
1117
  if (def == 0) return false;
1124
1118
 
@@ -1209,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1203
  const foe = battle.foe(player);
1210
1204
 
1211
1205
  if (battle.last_damage == 0) {
1212
- try options.log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1213
1207
  return null;
1214
1208
  }
1215
1209
 
@@ -1227,14 +1221,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1221
  foe_last_selected_move.type == .Fighting);
1228
1222
 
1229
1223
  if (!used and !selected) {
1230
- try options.log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1231
1225
  return null;
1232
1226
  }
1233
1227
 
1234
1228
  if (!used or !selected) {
1235
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1236
1230
  if (!showdown) return Result.Error;
1237
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1238
1232
  return null;
1239
1233
  }
1240
1234
 
@@ -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);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1274
1268
  options.calc.capped(target_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);
@@ -1344,12 +1338,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1344
1338
  if (mist) {
1345
1339
  assert(!showdown);
1346
1340
  const foe_ident = battle.active(player.foe());
1347
- try options.log.activate(foe_ident, .Mist);
1348
- try options.log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1349
1343
  } else {
1350
1344
  try options.chance.commit(player, .miss);
1351
- try options.log.lastmiss();
1352
- try options.log.miss(battle.active(player));
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1353
1347
  }
1354
1348
 
1355
1349
  return false;
@@ -1401,7 +1395,7 @@ fn moveHit(
1401
1395
  const overwritten = overwrite and state > 0;
1402
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1397
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1400
  accuracy = accuracy * boost[0] / boost[1];
1407
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,13 +1444,13 @@ fn checkFaint(
1450
1444
  assert(!foe.active.volatiles.MultiHit);
1451
1445
 
1452
1446
  if (player_out and foe_out) {
1453
- try options.log.tie();
1447
+ try options.log.tie(.{});
1454
1448
  return Result.Tie;
1455
1449
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1457
1451
  return if (player == .P1) Result.Lose else Result.Win;
1458
1452
  } else if (foe_out) {
1459
- try options.log.win(player);
1453
+ try options.log.win(.{player});
1460
1454
  return if (player == .P1) Result.Win else Result.Lose;
1461
1455
  }
1462
1456
 
@@ -1498,7 +1492,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1492
  side.stored().stats.spe;
1499
1493
  }
1500
1494
 
1501
- try options.log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1502
1496
  options.calc.capped(player);
1503
1497
  return null;
1504
1498
  }
@@ -1520,8 +1514,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1514
 
1521
1515
  if (!showdown or damage > 0) {
1522
1516
  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);
1517
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1518
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1525
1519
  }
1526
1520
  }
1527
1521
 
@@ -1546,14 +1540,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1540
  const amount = @min(damage, stored.hp);
1547
1541
  stored.hp -= amount;
1548
1542
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1543
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1544
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1551
1545
 
1552
1546
  const before = foe_stored.hp;
1553
1547
  // Uncapped damage is added back to the foe
1554
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1549
  // 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);
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1551
  }
1558
1552
  }
1559
1553
 
@@ -1564,18 +1558,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1558
  battle.turn += 1;
1565
1559
 
1566
1560
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1561
+ try options.log.tie(.{});
1568
1562
  return Result.Tie;
1569
1563
  }
1570
1564
 
1571
1565
  if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1566
+ try options.log.tie(.{});
1573
1567
  return Result.Tie;
1574
1568
  } else if (battle.turn >= 65535) {
1575
1569
  return Result.Error;
1576
1570
  }
1577
1571
 
1578
- try options.log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1579
1573
 
1580
1574
  return Result.Default;
1581
1575
  }
@@ -1645,14 +1639,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1639
  side.last_selected_move;
1646
1640
  if (m != .None and m == last) {
1647
1641
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1642
+ try options.log.cant(.{ ident, .Disable, last });
1649
1643
  return true;
1650
1644
  }
1651
1645
  }
1652
1646
  return false;
1653
1647
  }
1654
1648
 
1655
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1656
1650
  const side = battle.side(who);
1657
1651
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1658
1652
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1670,7 +1664,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1670
1664
  return true;
1671
1665
  }
1672
1666
 
1673
- inline fn onBegin(
1667
+ fn onBegin(
1674
1668
  battle: anytype,
1675
1669
  player: Player,
1676
1670
  move: Move.Data,
@@ -1678,7 +1672,6 @@ inline fn onBegin(
1678
1672
  residual: *bool,
1679
1673
  options: anytype,
1680
1674
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1675
  return switch (move.effect) {
1683
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1700,8 +1693,7 @@ inline fn onBegin(
1700
1693
  };
1701
1694
  }
1702
1695
 
1703
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
1697
  return switch (move.effect) {
1706
1698
  .Bide => Effects.bide(battle, player, options),
1707
1699
  // zig fmt: off
@@ -1716,14 +1708,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1716
1708
  };
1717
1709
  }
1718
1710
 
1719
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1720
1712
  battle: anytype,
1721
1713
  player: Player,
1722
1714
  move: Move.Data,
1723
1715
  residual: *bool,
1724
1716
  options: anytype,
1725
1717
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1718
  return switch (move.effect) {
1728
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1720
  .Explode => Effects.explode(battle, player),
@@ -1735,7 +1726,7 @@ inline fn alwaysHappens(
1735
1726
  };
1736
1727
  }
1737
1728
 
1738
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1729
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1739
1730
  return switch (move.effect) {
1740
1731
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1741
1732
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1762,7 +1753,7 @@ pub const Effects = struct {
1762
1753
  side.active.volatiles.state = 0;
1763
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1764
1755
 
1765
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1757
  }
1767
1758
 
1768
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1767,11 @@ pub const Effects = struct {
1776
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1769
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1770
+ try options.log.curestatus(.{
1780
1771
  battle.active(player.foe()),
1781
1772
  foe_stored.status,
1782
1773
  .Message,
1783
- );
1774
+ });
1784
1775
  foe_stored.status = 0;
1785
1776
  }
1786
1777
  return;
@@ -1794,7 +1785,7 @@ pub const Effects = struct {
1794
1785
  foe_stored.status = Status.init(.BRN);
1795
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1787
 
1797
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1788
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1798
1789
  }
1799
1790
 
1800
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1795,8 @@ pub const Effects = struct {
1804
1795
  volatiles.Charging = true;
1805
1796
  const move = side.last_selected_move;
1806
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1809
1800
  }
1810
1801
 
1811
1802
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,17 +1807,11 @@ pub const Effects = struct {
1816
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1808
  } else {
1818
1809
  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
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1812
  } 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
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1830
1815
  }
1831
1816
  try options.chance.commit(player, .hit);
1832
1817
  }
@@ -1835,23 +1820,24 @@ pub const Effects = struct {
1835
1820
  foe.active.volatiles.Confusion = true;
1836
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1822
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1824
  }
1840
1825
 
1841
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1827
  const foe = battle.foe(player);
1843
1828
 
1844
1829
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1847
1832
  }
1848
1833
 
1849
1834
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1835
+ return options.log.start(.{
1851
1836
  battle.active(player),
1837
+ .TypeChange,
1852
1838
  foe.active.types,
1853
1839
  battle.active(player.foe()),
1854
- );
1840
+ });
1855
1841
  }
1856
1842
 
1857
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1866,7 +1852,7 @@ pub const Effects = struct {
1866
1852
  }
1867
1853
 
1868
1854
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1855
+ try options.log.fail(.{ foe_ident, .None });
1870
1856
  return null;
1871
1857
  }
1872
1858
 
@@ -1882,7 +1868,7 @@ pub const Effects = struct {
1882
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
1870
  if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1886
1872
  return null;
1887
1873
  } else if (err) {
1888
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1895,12 +1881,13 @@ pub const Effects = struct {
1895
1881
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1896
1882
 
1897
1883
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1884
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1885
  return null;
1900
1886
  }
1901
1887
 
1902
1888
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1889
  var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1904
1891
 
1905
1892
  if (battle.last_damage == 0) {
1906
1893
  assert(showdown);
@@ -1913,7 +1900,7 @@ pub const Effects = struct {
1913
1900
  if (stored.hp == stored.stats.hp) return;
1914
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1902
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1904
  }
1918
1905
 
1919
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +1932,7 @@ pub const Effects = struct {
1945
1932
  if (side.active.volatiles.FocusEnergy) return;
1946
1933
  side.active.volatiles.FocusEnergy = true;
1947
1934
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
1936
  }
1950
1937
 
1951
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1967,7 +1954,7 @@ pub const Effects = struct {
1967
1954
  foe_stored.status = Status.init(.FRZ);
1968
1955
  // GLITCH: Hyper Beam recharging status is not cleared
1969
1956
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
1958
  }
1972
1959
 
1973
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1975,7 +1962,7 @@ pub const Effects = struct {
1975
1962
  var side = battle.side(player);
1976
1963
  var foe = battle.foe(player);
1977
1964
 
1978
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
1979
1966
  var foe_stored = foe.stored();
1980
1967
 
1981
1968
  const player_ident = battle.active(player);
@@ -1987,8 +1974,8 @@ pub const Effects = struct {
1987
1974
  side.active.stats = side_stored.stats;
1988
1975
  foe.active.stats = foe_stored.stats;
1989
1976
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
1992
1979
 
1993
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
1981
  if (showdown) {
@@ -1996,11 +1983,11 @@ pub const Effects = struct {
1996
1983
  const p: Player = @enumFromInt(i);
1997
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
1985
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
1987
  s.stored().status = 0;
2001
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2002
1989
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
1991
  }
2005
1992
  try clearVolatiles(battle, p, p != player, options);
2006
1993
  }
@@ -2009,7 +1996,7 @@ pub const Effects = struct {
2009
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
1997
  foe.last_selected_move = .SKIP_TURN;
2011
1998
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
2000
  foe_stored.status = 0;
2014
2001
  }
2015
2002
  try clearVolatiles(battle, player, false, options);
@@ -2025,7 +2012,7 @@ pub const Effects = struct {
2025
2012
  // GLITCH: HP recovery move failure glitches
2026
2013
  const delta = stored.stats.hp - stored.hp;
2027
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return try options.log.fail(ident, .None);
2015
+ return options.log.fail(.{ ident, .None });
2029
2016
  }
2030
2017
 
2031
2018
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2020,17 @@ pub const Effects = struct {
2033
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2021
  if (showdown) battle.rng.advance(1);
2035
2022
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2024
  stored.hp = stored.stats.hp;
2038
2025
  } else {
2039
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2027
  }
2041
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2028
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2042
2029
  }
2043
2030
 
2044
2031
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2032
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2034
  }
2048
2035
 
2049
2036
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2039,32 @@ pub const Effects = struct {
2052
2039
  if (showdown) {
2053
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2043
  }
2057
2044
  if (!try checkHit(battle, player, move, options)) return;
2058
2045
  if (foe.active.volatiles.LeechSeed) return;
2059
2046
  } else {
2060
2047
  if (!try checkHit(battle, player, move, options)) return;
2061
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
2064
2051
  }
2065
2052
  }
2066
2053
 
2067
2054
  try options.chance.commit(player, .hit);
2068
2055
  foe.active.volatiles.LeechSeed = true;
2069
2056
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2058
  }
2072
2059
 
2073
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2075
2063
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2065
  side.active.volatiles.LightScreen = true;
2080
2066
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2082
2068
  }
2083
2069
 
2084
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2107,7 +2093,7 @@ pub const Effects = struct {
2107
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2094
  if (!try checkHit(battle, player, move, options)) return;
2109
2095
  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;
@@ -2116,7 +2102,7 @@ pub const Effects = struct {
2116
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2104
 
2119
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2105
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2120
2106
  }
2121
2107
 
2122
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2111,7 @@ pub const Effects = struct {
2125
2111
  if (side.active.volatiles.Mist) return;
2126
2112
  side.active.volatiles.Mist = true;
2127
2113
 
2128
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2115
  }
2130
2116
 
2131
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,17 +2137,18 @@ pub const Effects = struct {
2151
2137
 
2152
2138
  if (showdown) {
2153
2139
  // 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;
2140
+ if (immune and !foe.active.volatiles.Invulnerable) {
2141
+ return log.immune(.{ foe_ident, .None });
2142
+ } else if (!try checkHit(battle, player, move, options)) return;
2156
2143
  }
2157
2144
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2145
+ return log.fail(.{
2159
2146
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2162
2149
  }
2163
2150
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2152
  if (!try checkHit(battle, player, move, options)) return;
2166
2153
  }
2167
2154
  try options.chance.commit(player, .hit);
@@ -2169,7 +2156,7 @@ pub const Effects = struct {
2169
2156
  foe_stored.status = Status.init(.PAR);
2170
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2158
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2160
  }
2174
2161
 
2175
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2178,12 @@ pub const Effects = struct {
2191
2178
  foe_stored.status = Status.init(.PAR);
2192
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2180
 
2194
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2181
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2195
2182
  }
2196
2183
 
2197
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2185
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2186
+ try options.log.fieldactivate(.{});
2200
2187
  }
2201
2188
  }
2202
2189
 
@@ -2211,23 +2198,23 @@ pub const Effects = struct {
2211
2198
  return;
2212
2199
  } else if (foe.active.volatiles.Substitute) {
2213
2200
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2215
2202
  } else if (Status.any(foe_stored.status)) {
2216
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2205
  // 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))
2206
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2207
  if (!showdown)
2221
- .Poison
2208
+ Fail.Poison
2222
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2211
  else
2225
- .None
2212
+ Fail.None
2226
2213
  else
2227
- .None);
2214
+ Fail.None });
2228
2215
  } else if (foe.active.types.includes(.Poison)) {
2229
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2231
2218
  }
2232
2219
 
2233
2220
  if (move.effect == .Poison) {
@@ -2246,7 +2233,7 @@ pub const Effects = struct {
2246
2233
  foe.active.volatiles.toxic = 0;
2247
2234
  }
2248
2235
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2237
  }
2251
2238
 
2252
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2253,23 @@ pub const Effects = struct {
2266
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2254
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2255
 
2269
- try options.log.damageOf(
2256
+ try options.log.damage(.{
2270
2257
  battle.active(player),
2271
2258
  stored,
2272
2259
  .RecoilOf,
2273
2260
  battle.active(player.foe()),
2274
- );
2261
+ });
2275
2262
  if (showdown and stored.hp == 0) residual.* = false;
2276
2263
  }
2277
2264
 
2278
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2280
2268
 
2281
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2269
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2282
2270
  side.active.volatiles.Reflect = true;
2283
2271
 
2284
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2285
2273
  }
2286
2274
 
2287
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,10 +2283,10 @@ pub const Effects = struct {
2295
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2284
  } else {
2297
2285
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2286
+ return options.log.fail(.{
2299
2287
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2302
2290
  }
2303
2291
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2304
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2315,18 +2303,17 @@ pub const Effects = struct {
2315
2303
 
2316
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2317
2305
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2307
  }
2320
2308
 
2321
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2311
  }
2324
2312
 
2325
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2314
  var side = battle.side(player);
2327
2315
  if (side.active.volatiles.Substitute) {
2328
- try options.log.fail(battle.active(player), .Substitute);
2329
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2330
2317
  }
2331
2318
 
2332
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2338,17 +2325,16 @@ pub const Effects = struct {
2338
2325
  // avoid having to convert to floating point)
2339
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2340
2327
  if (side.stored().hp < required_hp) {
2341
- try options.log.fail(battle.active(player), .Weak);
2342
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2343
2329
  }
2344
2330
 
2345
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2346
2332
  side.stored().hp -= hp;
2347
2333
  side.active.volatiles.substitute = hp + 1;
2348
2334
  side.active.volatiles.Substitute = true;
2349
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2350
2336
  if (hp > 0) {
2351
- try options.log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2352
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2353
2339
  }
2354
2340
  }
@@ -2362,8 +2348,8 @@ pub const Effects = struct {
2362
2348
  }
2363
2349
  battle.last_damage = 0;
2364
2350
  } else {
2365
- try options.log.fail(battle.active(player), .None);
2366
- try options.log.laststill();
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2367
2353
  }
2368
2354
  }
2369
2355
 
@@ -2402,7 +2388,7 @@ pub const Effects = struct {
2402
2388
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2403
2389
  }
2404
2390
 
2405
- try options.log.transform(battle.active(player), foe_ident);
2391
+ try options.log.transform(.{ battle.active(player), foe_ident });
2406
2392
  }
2407
2393
 
2408
2394
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2432,87 +2418,87 @@ pub const Effects = struct {
2432
2418
  switch (move.effect) {
2433
2419
  .AttackUp1, .AttackUp2, .Rage => {
2434
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2435
- if (boosts.atk == 6) return try log.fail(ident, .None);
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2436
2422
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2437
2423
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2438
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2439
2425
  if (stats.atk == MAX_STAT_VALUE) {
2440
2426
  boosts.atk -= 1;
2441
2427
  if (showdown) {
2442
- try log.boost(ident, reason, n);
2443
- try log.boost(ident, Boost.Attack, -1);
2428
+ try log.boost(.{ ident, reason, n });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2444
2430
  if (move.effect == .Rage) return;
2445
2431
  }
2446
- return try log.fail(ident, .None);
2432
+ return log.fail(.{ ident, .None });
2447
2433
  }
2448
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2449
2435
  const stat = unmodifiedStats(battle, side).atk;
2450
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2451
- try log.boost(ident, reason, n);
2437
+ try log.boost(.{ ident, reason, n });
2452
2438
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2453
2439
  if (showdown and move.effect == .Rage) return;
2454
2440
  },
2455
2441
  .DefenseUp1, .DefenseUp2 => {
2456
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2457
- if (boosts.def == 6) return try log.fail(ident, .None);
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2458
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2459
2445
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2460
2446
  if (stats.def == MAX_STAT_VALUE) {
2461
2447
  boosts.def -= 1;
2462
2448
  if (showdown) {
2463
- try log.boost(ident, .Defense, n);
2464
- try log.boost(ident, .Defense, -1);
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2465
2451
  }
2466
- return try log.fail(ident, .None);
2452
+ return log.fail(.{ ident, .None });
2467
2453
  }
2468
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2469
2455
  const stat = unmodifiedStats(battle, side).def;
2470
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2471
- try log.boost(ident, .Defense, n);
2457
+ try log.boost(.{ ident, .Defense, n });
2472
2458
  },
2473
2459
  .SpeedUp2 => {
2474
2460
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2475
- if (boosts.spe == 6) return try log.fail(ident, .None);
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2476
2462
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2477
2463
  if (stats.spe == MAX_STAT_VALUE) {
2478
2464
  boosts.spe -= 1;
2479
2465
  if (showdown) {
2480
- try log.boost(ident, .Speed, 2);
2481
- try log.boost(ident, .Speed, -1);
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2482
2468
  }
2483
- return try log.fail(ident, .None);
2469
+ return log.fail(.{ ident, .None });
2484
2470
  }
2485
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2486
2472
  const stat = unmodifiedStats(battle, side).spe;
2487
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2488
- try log.boost(ident, .Speed, 2);
2474
+ try log.boost(.{ ident, .Speed, 2 });
2489
2475
  },
2490
2476
  .SpecialUp1, .SpecialUp2 => {
2491
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2492
- if (boosts.spc == 6) return try log.fail(ident, .None);
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2493
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2494
2480
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2495
2481
  if (stats.spc == MAX_STAT_VALUE) {
2496
2482
  boosts.spc -= 1;
2497
2483
  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);
2484
+ try log.boost(.{ ident, .SpecialAttack, n });
2485
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2486
+ try log.boost(.{ ident, .SpecialDefense, n });
2487
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2502
2488
  }
2503
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2504
2490
  }
2505
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2506
2492
  const stat = unmodifiedStats(battle, side).spc;
2507
2493
  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);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2510
2496
  },
2511
2497
  .EvasionUp1 => {
2512
2498
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2513
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2514
2500
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2515
- try log.boost(ident, .Evasion, 1);
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2516
2502
  },
2517
2503
  else => unreachable,
2518
2504
  }
@@ -2527,7 +2513,7 @@ pub const Effects = struct {
2527
2513
  const foe_ident = battle.active(player.foe());
2528
2514
  const secondary = move.effect.isStatDownChance();
2529
2515
 
2530
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2516
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2531
2517
 
2532
2518
  if (secondary) {
2533
2519
  const proc = try Rolls.unboost(battle, player, options);
@@ -2545,82 +2531,82 @@ pub const Effects = struct {
2545
2531
  switch (move.effect) {
2546
2532
  .AttackDown1, .AttackDownChance => {
2547
2533
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2548
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2534
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2549
2535
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2550
2536
  if (stats.atk == 1) {
2551
2537
  boosts.atk += 1;
2552
2538
  if (showdown) {
2553
- try log.boost(foe_ident, .Attack, -1);
2554
- try log.boost(foe_ident, .Attack, 1);
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2555
2541
  }
2556
- return try log.fail(foe_ident, .None);
2542
+ return log.fail(.{ foe_ident, .None });
2557
2543
  }
2558
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2559
2545
  const stat = unmodifiedStats(battle, foe).atk;
2560
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2561
- try log.boost(foe_ident, .Attack, -1);
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2562
2548
  },
2563
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2564
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2565
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2551
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2566
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2567
2553
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2568
2554
  if (stats.def == 1) {
2569
2555
  boosts.def += 1;
2570
2556
  if (showdown) {
2571
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2572
- try log.boost(foe_ident, .Defense, 1);
2557
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2558
+ try log.boost(.{ foe_ident, .Defense, 1 });
2573
2559
  }
2574
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2575
2561
  }
2576
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2577
2563
  const stat = unmodifiedStats(battle, foe).def;
2578
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2579
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2580
2566
  },
2581
2567
  .SpeedDown1, .SpeedDownChance => {
2582
2568
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2583
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2569
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2584
2570
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2585
2571
  if (stats.spe == 1) {
2586
2572
  boosts.spe += 1;
2587
2573
  if (showdown) {
2588
- try log.boost(foe_ident, .Speed, -1);
2589
- try log.boost(foe_ident, .Speed, 1);
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2590
2576
  }
2591
- return try log.fail(foe_ident, .None);
2577
+ return log.fail(.{ foe_ident, .None });
2592
2578
  }
2593
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2594
2580
  const stat = unmodifiedStats(battle, foe).spe;
2595
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2596
- try log.boost(foe_ident, .Speed, -1);
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2597
2583
  assert(boosts.spe >= -6);
2598
2584
  },
2599
2585
  .SpecialDownChance => {
2600
2586
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2601
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2587
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2588
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2603
2589
  if (stats.spc == 1) {
2604
2590
  boosts.spc += 1;
2605
2591
  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);
2592
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2593
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2594
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2595
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2610
2596
  }
2611
- return try log.fail(foe_ident, .None);
2597
+ return log.fail(.{ foe_ident, .None });
2612
2598
  }
2613
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2614
2600
  const stat = unmodifiedStats(battle, foe).spc;
2615
2601
  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);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2618
2604
  },
2619
2605
  .AccuracyDown1 => {
2620
2606
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2621
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2607
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2622
2608
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2623
- try log.boost(foe_ident, .Accuracy, -1);
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2624
2610
  },
2625
2611
  else => unreachable,
2626
2612
  }
@@ -2644,7 +2630,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2644
2630
  }
2645
2631
  }
2646
2632
 
2647
- inline fn isForced(active: anytype) bool {
2633
+ fn isForced(active: anytype) bool {
2648
2634
  return active.volatiles.Recharging or active.volatiles.Rage or
2649
2635
  active.volatiles.Thrashing or active.volatiles.Charging;
2650
2636
  }
@@ -2660,24 +2646,24 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2660
2646
  if (volatiles.disable_move != 0) {
2661
2647
  volatiles.disable_move = 0;
2662
2648
  volatiles.disable_duration = 0;
2663
- try log.end(ident, .DisableSilent);
2649
+ try log.end(.{ ident, .DisableSilent });
2664
2650
  }
2665
2651
  if (volatiles.Confusion) {
2666
2652
  // volatiles.confusion is left unchanged
2667
2653
  volatiles.Confusion = false;
2668
- try log.end(ident, .ConfusionSilent);
2654
+ try log.end(.{ ident, .ConfusionSilent });
2669
2655
  }
2670
2656
  if (volatiles.Mist) {
2671
2657
  volatiles.Mist = false;
2672
- try log.end(ident, .MistSilent);
2658
+ try log.end(.{ ident, .MistSilent });
2673
2659
  }
2674
2660
  if (volatiles.FocusEnergy) {
2675
2661
  volatiles.FocusEnergy = false;
2676
- try log.end(ident, .FocusEnergySilent);
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2677
2663
  }
2678
2664
  if (volatiles.LeechSeed) {
2679
2665
  volatiles.LeechSeed = false;
2680
- try log.end(ident, .LeechSeedSilent);
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2681
2667
  }
2682
2668
  if (!showdown and volatiles.Toxic) {
2683
2669
  volatiles.Toxic = false;
@@ -2685,24 +2671,26 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2671
  }
2686
2672
  if (volatiles.LightScreen) {
2687
2673
  volatiles.LightScreen = false;
2688
- try log.end(ident, .LightScreenSilent);
2674
+ try log.end(.{ ident, .LightScreenSilent });
2689
2675
  }
2690
2676
  if (volatiles.Reflect) {
2691
2677
  volatiles.Reflect = false;
2692
- try log.end(ident, .ReflectSilent);
2678
+ try log.end(.{ ident, .ReflectSilent });
2693
2679
  }
2694
2680
  if (showdown and volatiles.Toxic) {
2695
2681
  volatiles.Toxic = false;
2696
2682
  // Pokémon Showdown erroneously clears the toxic counter
2697
2683
  volatiles.toxic = 0;
2698
- try log.end(ident, .ToxicSilent);
2684
+ try log.end(.{ ident, .ToxicSilent });
2699
2685
  }
2700
2686
  }
2701
2687
 
2702
2688
  pub const Rolls = struct {
2703
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2689
+ fn speedTie(battle: anytype, options: anytype) !bool {
2704
2690
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2705
2691
  player == .P1
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2706
2694
  else if (showdown)
2707
2695
  battle.rng.range(u8, 0, 2) == 0
2708
2696
  else
@@ -2712,9 +2700,11 @@ pub const Rolls = struct {
2712
2700
  return p1;
2713
2701
  }
2714
2702
 
2715
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2703
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2716
2704
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2717
2705
  val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2718
2708
  else if (showdown)
2719
2709
  battle.rng.chance(u8, rate, 256)
2720
2710
  else
@@ -2724,9 +2714,11 @@ pub const Rolls = struct {
2724
2714
  return crit;
2725
2715
  }
2726
2716
 
2727
- inline fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2717
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2728
2718
  const roll = if (options.calc.overridden(player, .damage)) |val|
2729
2719
  val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2730
2722
  else roll: {
2731
2723
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2732
2724
  while (true) {
@@ -2740,9 +2732,11 @@ pub const Rolls = struct {
2740
2732
  return roll;
2741
2733
  }
2742
2734
 
2743
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2744
2736
  const ok = if (options.calc.overridden(player, .hit)) |val|
2745
2737
  val == .true
2738
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
+ battle.rng.hit(player, accuracy)
2746
2740
  else if (showdown)
2747
2741
  battle.rng.chance(u8, accuracy, 256)
2748
2742
  else
@@ -2752,9 +2746,11 @@ pub const Rolls = struct {
2752
2746
  return ok;
2753
2747
  }
2754
2748
 
2755
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2749
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2756
2750
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2757
2751
  val == .true
2752
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2753
+ battle.rng.confused(player)
2758
2754
  else if (showdown)
2759
2755
  !battle.rng.chance(u8, 128, 256)
2760
2756
  else
@@ -2764,9 +2760,11 @@ pub const Rolls = struct {
2764
2760
  return cfz;
2765
2761
  }
2766
2762
 
2767
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2768
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2769
2765
  val == .true
2766
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2767
+ battle.rng.paralyzed(player)
2770
2768
  else if (showdown)
2771
2769
  battle.rng.chance(u8, 63, 256)
2772
2770
  else
@@ -2776,9 +2774,11 @@ pub const Rolls = struct {
2776
2774
  return par;
2777
2775
  }
2778
2776
 
2779
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2777
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2780
2778
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2779
  val == .true
2780
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2781
+ battle.rng.secondaryChance(player, 25)
2782
2782
  else if (showdown)
2783
2783
  battle.rng.chance(u8, 25, 256)
2784
2784
  else
@@ -2788,11 +2788,13 @@ pub const Rolls = struct {
2788
2788
  return proc;
2789
2789
  }
2790
2790
 
2791
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2791
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2792
2792
  const rate: u8 = if (low) 26 else 77;
2793
2793
 
2794
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2797
+ battle.rng.secondaryChance(player, rate)
2796
2798
  else if (showdown)
2797
2799
  battle.rng.chance(u8, rate, 256)
2798
2800
  else
@@ -2802,11 +2804,13 @@ pub const Rolls = struct {
2802
2804
  return proc;
2803
2805
  }
2804
2806
 
2805
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2807
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2806
2808
  const rate: u8 = if (low) 52 else 103;
2807
2809
 
2808
2810
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2809
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2813
+ battle.rng.secondaryChance(player, rate)
2810
2814
  else if (showdown)
2811
2815
  battle.rng.chance(u8, rate, 256)
2812
2816
  else
@@ -2816,9 +2820,11 @@ pub const Rolls = struct {
2816
2820
  return proc;
2817
2821
  }
2818
2822
 
2819
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2823
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2820
2824
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2821
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2827
+ battle.rng.secondaryChance(player, 85)
2822
2828
  else if (showdown)
2823
2829
  battle.rng.chance(u8, 85, 256)
2824
2830
  else
@@ -2828,9 +2834,11 @@ pub const Rolls = struct {
2828
2834
  return proc;
2829
2835
  }
2830
2836
 
2831
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2837
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2832
2838
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2833
2839
  val
2840
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2841
+ battle.rng.metronome(player)
2834
2842
  else if (showdown)
2835
2843
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
2844
  else move: {
@@ -2846,9 +2854,11 @@ pub const Rolls = struct {
2846
2854
  return move;
2847
2855
  }
2848
2856
 
2849
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2857
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2850
2858
  const power = if (options.calc.overridden(player, .psywave)) |val|
2851
2859
  val - 1
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2861
+ battle.rng.psywave(player)
2852
2862
  else if (showdown)
2853
2863
  battle.rng.range(u8, 0, max)
2854
2864
  else power: {
@@ -2863,9 +2873,11 @@ pub const Rolls = struct {
2863
2873
  return power;
2864
2874
  }
2865
2875
 
2866
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2876
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2867
2877
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2868
2878
  @intCast(val)
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
+ battle.rng.sleepDuration(player)
2869
2881
  else if (showdown)
2870
2882
  battle.rng.range(u3, 1, 8)
2871
2883
  else duration: {
@@ -2880,9 +2892,11 @@ pub const Rolls = struct {
2880
2892
  return duration;
2881
2893
  }
2882
2894
 
2883
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2884
2896
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2885
2897
  val
2898
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
+ battle.rng.disableDuration(player)
2886
2900
  else if (showdown)
2887
2901
  battle.rng.range(u4, 1, 9)
2888
2902
  else
@@ -2893,9 +2907,11 @@ pub const Rolls = struct {
2893
2907
  return duration;
2894
2908
  }
2895
2909
 
2896
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2910
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2897
2911
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2898
2912
  @intCast(val)
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
+ battle.rng.confusionDuration(player, self)
2899
2915
  else if (showdown)
2900
2916
  battle.rng.range(u3, 2, 6)
2901
2917
  else
@@ -2906,9 +2922,11 @@ pub const Rolls = struct {
2906
2922
  return duration;
2907
2923
  }
2908
2924
 
2909
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2925
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2910
2926
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2911
2927
  @intCast(val)
2928
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
+ battle.rng.attackingDuration(player)
2912
2930
  else if (showdown)
2913
2931
  battle.rng.range(u3, 2, 4)
2914
2932
  else
@@ -2921,7 +2939,7 @@ pub const Rolls = struct {
2921
2939
 
2922
2940
  const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2923
2941
 
2924
- inline fn distribution(
2942
+ fn distribution(
2925
2943
  battle: anytype,
2926
2944
  comptime effect: Move.Effect,
2927
2945
  player: Player,
@@ -2930,6 +2948,8 @@ pub const Rolls = struct {
2930
2948
  const roll = if (effect == .Binding) .duration else .multi_hit;
2931
2949
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2932
2950
  @intCast(val)
2951
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
+ battle.rng.distribution(player)
2933
2953
  else if (showdown)
2934
2954
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2935
2955
  else n: {
@@ -2962,7 +2982,9 @@ pub const Rolls = struct {
2962
2982
  else
2963
2983
  null;
2964
2984
 
2965
- const slot: u4 = overridden orelse slot: {
2985
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
+ battle.rng.moveSlot(player, moves, check_pp)
2987
+ else slot: {
2966
2988
  if (showdown) {
2967
2989
  if (check_pp == 0) {
2968
2990
  var i: usize = moves.len;
@@ -3042,7 +3064,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3042
3064
  const side = battle.side(player);
3043
3065
  const foe = battle.foe(player);
3044
3066
 
3045
- var active = &side.active;
3067
+ const active = &side.active;
3046
3068
  const stored = side.stored();
3047
3069
 
3048
3070
  // While players are not given any input options on the cartridge in these cases,