@pkmn/engine 0.1.0-dev.30c63015 → 0.1.0-dev.32adfe77

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.
@@ -21,13 +21,9 @@ const Player = common.Player;
21
21
  const Result = common.Result;
22
22
 
23
23
  const Boost = protocol.Boost;
24
- const Cant = protocol.Cant;
25
- const CureStatus = protocol.CureStatus;
26
24
  const Damage = protocol.Damage;
27
- const End = protocol.End;
28
25
  const Fail = protocol.Fail;
29
26
  const Heal = protocol.Heal;
30
- const Immune = protocol.Immune;
31
27
 
32
28
  const Gen12 = rng.Gen12;
33
29
 
@@ -75,8 +71,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
75
71
  var p1 = battle.side(.P1);
76
72
  var p2 = battle.side(.P2);
77
73
 
78
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
79
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
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;
80
76
 
81
77
  if (try turnOrder(battle, c1, c2, options) == .P1) {
82
78
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -94,11 +90,11 @@ fn start(battle: anytype, options: anytype) !Result {
94
90
  const p1 = battle.side(.P1);
95
91
  const p2 = battle.side(.P2);
96
92
 
97
- var p1_slot = findFirstAlive(p1);
93
+ const p1_slot = findFirstAlive(p1);
98
94
  assert(!showdown or p1_slot == 1);
99
95
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
100
96
 
101
- var p2_slot = findFirstAlive(p2);
97
+ const p2_slot = findFirstAlive(p2);
102
98
  assert(!showdown or p2_slot == 1);
103
99
  if (p2_slot == 0) return Result.Win;
104
100
 
@@ -267,7 +263,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
267
263
  incoming.status = Status.init(.PSN);
268
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
269
265
  // rather not clutter up turnOrder just for this (incorrect) log message
270
- try options.log.status(.{ battle.active(player), incoming.status, protocol.Status.Silent });
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
271
267
  }
272
268
  }
273
269
 
@@ -411,7 +407,7 @@ fn executeMove(
411
407
  if (side.last_selected_move == .SKIP_TURN) {
412
408
  assert(!showdown);
413
409
  if (battle.foe(player).active.volatiles.Binding) {
414
- try options.log.cant(.{ battle.active(player), Cant.Bound });
410
+ try options.log.cant(.{ battle.active(player), .Bound });
415
411
  }
416
412
  return null;
417
413
  }
@@ -501,23 +497,23 @@ fn beforeMove(
501
497
  }
502
498
 
503
499
  if (duration == 0) {
504
- try log.curestatus(.{ ident, before, CureStatus.Message });
500
+ try log.curestatus(.{ ident, before, .Message });
505
501
  stored.status = 0; // clears EXT if present
506
502
  } else {
507
- try log.cant(.{ ident, Cant.Sleep });
503
+ try log.cant(.{ ident, .Sleep });
508
504
  }
509
505
  side.last_used_move = .None;
510
506
  return .done;
511
507
  }
512
508
 
513
509
  if (Status.is(stored.status, .FRZ)) {
514
- try log.cant(.{ ident, Cant.Freeze });
510
+ try log.cant(.{ ident, .Freeze });
515
511
  side.last_used_move = .None;
516
512
  return .done;
517
513
  }
518
514
 
519
515
  if (skip or foe.active.volatiles.Binding) {
520
- try log.cant(.{ ident, Cant.Bound });
516
+ try log.cant(.{ ident, .Bound });
521
517
  return .done;
522
518
  }
523
519
 
@@ -525,13 +521,13 @@ fn beforeMove(
525
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
526
522
  // Pokémon can sometimes flinch multiple times from the same original hit
527
523
  if (!showdown) volatiles.Flinch = false;
528
- try log.cant(.{ ident, Cant.Flinch });
524
+ try log.cant(.{ ident, .Flinch });
529
525
  return .done;
530
526
  }
531
527
 
532
528
  if (volatiles.Recharging) {
533
529
  volatiles.Recharging = false;
534
- try log.cant(.{ ident, Cant.Recharge });
530
+ try log.cant(.{ ident, .Recharge });
535
531
  return .done;
536
532
  }
537
533
 
@@ -545,7 +541,7 @@ fn beforeMove(
545
541
 
546
542
  if (volatiles.disable_duration == 0) {
547
543
  volatiles.disable_move = 0;
548
- try log.end(.{ ident, End.Disable });
544
+ try log.end(.{ ident, .Disable });
549
545
  }
550
546
  }
551
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -570,9 +566,9 @@ fn beforeMove(
570
566
 
571
567
  if (volatiles.confusion == 0) {
572
568
  volatiles.Confusion = false;
573
- try log.end(.{ ident, End.Confusion });
569
+ try log.end(.{ ident, .Confusion });
574
570
  } else {
575
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
576
572
 
577
573
  if (try Rolls.confused(battle, player, options)) {
578
574
  assert(!volatiles.MultiHit);
@@ -623,7 +619,7 @@ fn beforeMove(
623
619
  volatiles.Binding = false;
624
620
  options.chance.clearDurations(player, null);
625
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
626
- try log.cant(.{ ident, Cant.Paralysis });
622
+ try log.cant(.{ ident, .Paralysis });
627
623
  return .done;
628
624
  }
629
625
 
@@ -639,8 +635,6 @@ fn beforeMove(
639
635
  volatiles.state +%= battle.last_damage;
640
636
  }
641
637
 
642
- assert(volatiles.attacks > 0);
643
-
644
638
  if (options.calc.modify(player, .attacking)) |extend| {
645
639
  if (!extend) volatiles.attacks = 0;
646
640
  } else {
@@ -649,18 +643,18 @@ fn beforeMove(
649
643
  try options.chance.attacking(player, volatiles.attacks);
650
644
 
651
645
  if (volatiles.attacks != 0) {
652
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
653
647
  return .done;
654
648
  }
655
649
 
656
650
  volatiles.Bide = false;
657
- try log.end(.{ ident, End.Bide });
651
+ try log.end(.{ ident, .Bide });
658
652
 
659
653
  battle.last_damage = volatiles.state *% 2;
660
654
  volatiles.state = 0;
661
655
 
662
656
  if (battle.last_damage == 0) {
663
- try log.fail(.{ ident, Fail.None });
657
+ try log.fail(.{ ident, .None });
664
658
  return .done;
665
659
  }
666
660
 
@@ -680,8 +674,6 @@ fn beforeMove(
680
674
  }
681
675
 
682
676
  if (volatiles.Thrashing) {
683
- assert(volatiles.attacks > 0);
684
-
685
677
  if (options.calc.modify(player, .attacking)) |extend| {
686
678
  if (!extend) volatiles.attacks = 0;
687
679
  } else {
@@ -690,7 +682,7 @@ fn beforeMove(
690
682
  try options.chance.attacking(player, volatiles.attacks);
691
683
 
692
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
693
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
694
686
  }
695
687
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
696
688
  if (showdown) {
@@ -698,15 +690,13 @@ fn beforeMove(
698
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
699
691
  side.last_used_move = side.last_selected_move;
700
692
  if (handleThrashing(battle, active, player, options)) {
701
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
702
694
  }
703
695
  }
704
696
  return .skip_can;
705
697
  }
706
698
 
707
699
  if (volatiles.Binding) {
708
- assert(volatiles.attacks > 0);
709
-
710
700
  if (options.calc.modify(player, .binding)) |extend| {
711
701
  if (!extend) volatiles.attacks = 0;
712
702
  } else {
@@ -742,7 +732,6 @@ fn canMove(
742
732
  const player_ident = battle.active(player);
743
733
  const move = Move.get(side.last_selected_move);
744
734
 
745
- var skip = skip_pp;
746
735
  if (side.active.volatiles.Charging) {
747
736
  side.active.volatiles.Charging = false;
748
737
  side.active.volatiles.Invulnerable = false;
@@ -754,7 +743,7 @@ fn canMove(
754
743
  }
755
744
 
756
745
  side.last_used_move = side.last_selected_move;
757
- if (!skip) decrementPP(side, mslot, auto);
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
758
747
 
759
748
  const target = if (move.target == .Self) player else player.foe();
760
749
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -906,7 +895,7 @@ fn doMove(
906
895
  }
907
896
  }
908
897
 
909
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
910
899
  miss = if (showdown or skip)
911
900
  miss
912
901
  else
@@ -934,12 +923,12 @@ fn doMove(
934
923
  ohko = (!showdown or (!immune and !invulnerable)) and
935
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
936
925
  if (ohko) {
937
- try log.immune(.{ foe_ident, Immune.OHKO });
926
+ try log.immune(.{ foe_ident, .OHKO });
938
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
939
- try log.immune(.{ foe_ident, Immune.None });
928
+ try log.immune(.{ foe_ident, .None });
940
929
  } else if (mist) {
941
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
942
- try log.fail(.{ foe_ident, Fail.None });
930
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
+ try log.fail(.{ foe_ident, .None });
943
932
  } else {
944
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
945
934
  try log.lastmiss(.{});
@@ -1014,9 +1003,9 @@ fn doMove(
1014
1003
  assert(foe.stored().hp > 0);
1015
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1016
1005
  if (sub) {
1017
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1018
1007
  } else {
1019
- try log.damage(.{ battle.active(player.foe()), foe.stored(), Damage.None });
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1020
1009
  try buildRage(battle, player.foe(), options);
1021
1010
  }
1022
1011
  return null;
@@ -1064,7 +1053,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1064
1053
  const side = battle.side(player);
1065
1054
 
1066
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1067
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1068
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1069
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1070
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
@@ -1121,9 +1110,9 @@ fn calcDamage(
1121
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1122
1111
  }
1123
1112
 
1124
- 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);
1125
1114
 
1126
- 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;
1127
1116
 
1128
1117
  if (def == 0) return false;
1129
1118
 
@@ -1214,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1214
1203
  const foe = battle.foe(player);
1215
1204
 
1216
1205
  if (battle.last_damage == 0) {
1217
- try options.log.fail(.{ battle.active(player), Fail.None });
1206
+ try options.log.fail(.{ battle.active(player), .None });
1218
1207
  return null;
1219
1208
  }
1220
1209
 
@@ -1232,14 +1221,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1232
1221
  foe_last_selected_move.type == .Fighting);
1233
1222
 
1234
1223
  if (!used and !selected) {
1235
- try options.log.fail(.{ battle.active(player), Fail.None });
1224
+ try options.log.fail(.{ battle.active(player), .None });
1236
1225
  return null;
1237
1226
  }
1238
1227
 
1239
1228
  if (!used or !selected) {
1240
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1241
1230
  if (!showdown) return Result.Error;
1242
- try options.log.fail(.{ battle.active(player), Fail.None });
1231
+ try options.log.fail(.{ battle.active(player), .None });
1243
1232
  return null;
1244
1233
  }
1245
1234
 
@@ -1275,13 +1264,13 @@ fn applyDamage(
1275
1264
  subbed.active.volatiles.substitute = 0;
1276
1265
  subbed.active.volatiles.Substitute = false;
1277
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1278
- try options.log.end(.{ battle.active(sub_player), End.Substitute });
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1279
1268
  options.calc.capped(target_player);
1280
1269
  return true;
1281
1270
  } else {
1282
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1283
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1284
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1285
1274
  return false;
1286
1275
  }
1287
1276
  }
@@ -1307,7 +1296,7 @@ fn mirrorMove(
1307
1296
  side.last_selected_move = foe.last_used_move;
1308
1297
 
1309
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1310
- try options.log.fail(.{ battle.active(player), Fail.None });
1299
+ try options.log.fail(.{ battle.active(player), .None });
1311
1300
  return null;
1312
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1313
1302
  incrementPP(side, mslot);
@@ -1349,8 +1338,8 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1349
1338
  if (mist) {
1350
1339
  assert(!showdown);
1351
1340
  const foe_ident = battle.active(player.foe());
1352
- try options.log.activate(foe_ident, .Mist);
1353
- try options.log.fail(.{ foe_ident, Fail.None });
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1354
1343
  } else {
1355
1344
  try options.chance.commit(player, .miss);
1356
1345
  try options.log.lastmiss(.{});
@@ -1406,7 +1395,7 @@ fn moveHit(
1406
1395
  const overwritten = overwrite and state > 0;
1407
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1408
1397
 
1409
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1410
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1411
1400
  accuracy = accuracy * boost[0] / boost[1];
1412
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1552,13 +1541,13 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1552
1541
  stored.hp -= amount;
1553
1542
 
1554
1543
  // As above, Pokémon Showdown uses damage [of] but its not relevant
1555
- if (amount > 0) try options.log.damage(.{ ident, stored, Damage.LeechSeed });
1544
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1556
1545
 
1557
1546
  const before = foe_stored.hp;
1558
1547
  // Uncapped damage is added back to the foe
1559
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1560
1549
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1561
- if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1562
1551
  }
1563
1552
  }
1564
1553
 
@@ -1650,7 +1639,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1650
1639
  side.last_selected_move;
1651
1640
  if (m != .None and m == last) {
1652
1641
  side.active.volatiles.Charging = false;
1653
- try options.log.cant(.{ ident, Cant.Disable, last });
1642
+ try options.log.cant(.{ ident, .Disable, last });
1654
1643
  return true;
1655
1644
  }
1656
1645
  }
@@ -1683,7 +1672,6 @@ fn onBegin(
1683
1672
  residual: *bool,
1684
1673
  options: anytype,
1685
1674
  ) !void {
1686
- assert(move.effect.onBegin());
1687
1675
  return switch (move.effect) {
1688
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1689
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1706,7 +1694,6 @@ fn onBegin(
1706
1694
  }
1707
1695
 
1708
1696
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1709
- assert(move.effect.onEnd());
1710
1697
  return switch (move.effect) {
1711
1698
  .Bide => Effects.bide(battle, player, options),
1712
1699
  // zig fmt: off
@@ -1728,7 +1715,6 @@ fn alwaysHappens(
1728
1715
  residual: *bool,
1729
1716
  options: anytype,
1730
1717
  ) !void {
1731
- assert(move.effect.alwaysHappens());
1732
1718
  return switch (move.effect) {
1733
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1734
1720
  .Explode => Effects.explode(battle, player),
@@ -1767,7 +1753,7 @@ pub const Effects = struct {
1767
1753
  side.active.volatiles.state = 0;
1768
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1769
1755
 
1770
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1771
1757
  }
1772
1758
 
1773
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1784,7 +1770,7 @@ pub const Effects = struct {
1784
1770
  try options.log.curestatus(.{
1785
1771
  battle.active(player.foe()),
1786
1772
  foe_stored.status,
1787
- CureStatus.Message,
1773
+ .Message,
1788
1774
  });
1789
1775
  foe_stored.status = 0;
1790
1776
  }
@@ -1799,8 +1785,7 @@ pub const Effects = struct {
1799
1785
  foe_stored.status = Status.init(.BRN);
1800
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1801
1787
 
1802
- const reason: protocol.Status = .None;
1803
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1788
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1804
1789
  }
1805
1790
 
1806
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1822,17 +1807,11 @@ pub const Effects = struct {
1822
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1823
1808
  } else {
1824
1809
  if (showdown) {
1825
- if (!try checkHit(battle, player, move, options)) {
1826
- return;
1827
- } else if (sub) {
1828
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1829
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1830
1812
  } else {
1831
- if (sub) {
1832
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1833
- } else if (!try checkHit(battle, player, move, options)) {
1834
- return;
1835
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1836
1815
  }
1837
1816
  try options.chance.commit(player, .hit);
1838
1817
  }
@@ -1841,7 +1820,7 @@ pub const Effects = struct {
1841
1820
  foe.active.volatiles.Confusion = true;
1842
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1843
1822
 
1844
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1845
1824
  }
1846
1825
 
1847
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
@@ -1853,11 +1832,12 @@ pub const Effects = struct {
1853
1832
  }
1854
1833
 
1855
1834
  battle.side(player).active.types = foe.active.types;
1856
- return options.log.typechange(
1835
+ return options.log.start(.{
1857
1836
  battle.active(player),
1837
+ .TypeChange,
1858
1838
  foe.active.types,
1859
1839
  battle.active(player.foe()),
1860
- );
1840
+ });
1861
1841
  }
1862
1842
 
1863
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1872,7 +1852,7 @@ pub const Effects = struct {
1872
1852
  }
1873
1853
 
1874
1854
  if (volatiles.disable_move != 0) {
1875
- try options.log.fail(.{ foe_ident, Fail.None });
1855
+ try options.log.fail(.{ foe_ident, .None });
1876
1856
  return null;
1877
1857
  }
1878
1858
 
@@ -1888,7 +1868,7 @@ pub const Effects = struct {
1888
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1889
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1890
1870
  if (n == 0) {
1891
- try options.log.fail(.{ foe_ident, Fail.None });
1871
+ try options.log.fail(.{ foe_ident, .None });
1892
1872
  return null;
1893
1873
  } else if (err) {
1894
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1901,7 +1881,7 @@ pub const Effects = struct {
1901
1881
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1902
1882
 
1903
1883
  const id = foe.active.move(volatiles.disable_move).id;
1904
- try options.log.startEffect(foe_ident, .Disable, id);
1884
+ try options.log.start(.{ foe_ident, .Disable, id });
1905
1885
  return null;
1906
1886
  }
1907
1887
 
@@ -1920,7 +1900,7 @@ pub const Effects = struct {
1920
1900
  if (stored.hp == stored.stats.hp) return;
1921
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1922
1902
 
1923
- try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1924
1904
  }
1925
1905
 
1926
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1952,7 +1932,7 @@ pub const Effects = struct {
1952
1932
  if (side.active.volatiles.FocusEnergy) return;
1953
1933
  side.active.volatiles.FocusEnergy = true;
1954
1934
 
1955
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1956
1936
  }
1957
1937
 
1958
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1974,7 +1954,7 @@ pub const Effects = struct {
1974
1954
  foe_stored.status = Status.init(.FRZ);
1975
1955
  // GLITCH: Hyper Beam recharging status is not cleared
1976
1956
 
1977
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1978
1958
  }
1979
1959
 
1980
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1982,7 +1962,7 @@ pub const Effects = struct {
1982
1962
  var side = battle.side(player);
1983
1963
  var foe = battle.foe(player);
1984
1964
 
1985
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
1986
1966
  var foe_stored = foe.stored();
1987
1967
 
1988
1968
  const player_ident = battle.active(player);
@@ -1994,7 +1974,7 @@ pub const Effects = struct {
1994
1974
  side.active.stats = side_stored.stats;
1995
1975
  foe.active.stats = foe_stored.stats;
1996
1976
 
1997
- try log.activate(player_ident, .Haze);
1977
+ try log.activate(.{ player_ident, .Haze });
1998
1978
  try log.clearallboost(.{});
1999
1979
 
2000
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
@@ -2003,11 +1983,11 @@ pub const Effects = struct {
2003
1983
  const p: Player = @enumFromInt(i);
2004
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2005
1985
  if (p != player and Status.any(s.stored().status)) {
2006
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2007
1987
  s.stored().status = 0;
2008
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2009
1989
  s.stored().status = Status.init(.PSN);
2010
- try log.status(.{ battle.active(p), s.stored().status, protocol.Status.None });
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2011
1991
  }
2012
1992
  try clearVolatiles(battle, p, p != player, options);
2013
1993
  }
@@ -2016,7 +1996,7 @@ pub const Effects = struct {
2016
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2017
1997
  foe.last_selected_move = .SKIP_TURN;
2018
1998
  }
2019
- try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2020
2000
  foe_stored.status = 0;
2021
2001
  }
2022
2002
  try clearVolatiles(battle, player, false, options);
@@ -2032,7 +2012,7 @@ pub const Effects = struct {
2032
2012
  // GLITCH: HP recovery move failure glitches
2033
2013
  const delta = stored.stats.hp - stored.hp;
2034
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2035
- return options.log.fail(.{ ident, Fail.None });
2015
+ return options.log.fail(.{ ident, .None });
2036
2016
  }
2037
2017
 
2038
2018
  const rest = side.last_selected_move == .Rest;
@@ -2040,7 +2020,7 @@ pub const Effects = struct {
2040
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2041
2021
  if (showdown) battle.rng.advance(1);
2042
2022
  stored.status = Status.slf(2);
2043
- try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2044
2024
  stored.hp = stored.stats.hp;
2045
2025
  } else {
2046
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
@@ -2059,7 +2039,7 @@ pub const Effects = struct {
2059
2039
  if (showdown) {
2060
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2061
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2062
- return options.log.immune(.{ battle.active(player.foe()), Immune.None });
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2063
2043
  }
2064
2044
  if (!try checkHit(battle, player, move, options)) return;
2065
2045
  if (foe.active.volatiles.LeechSeed) return;
@@ -2074,18 +2054,17 @@ pub const Effects = struct {
2074
2054
  try options.chance.commit(player, .hit);
2075
2055
  foe.active.volatiles.LeechSeed = true;
2076
2056
 
2077
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2078
2058
  }
2079
2059
 
2080
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2081
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2082
2063
 
2083
- if (side.active.volatiles.LightScreen) {
2084
- return options.log.fail(.{ battle.active(player), Fail.None });
2085
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2086
2065
  side.active.volatiles.LightScreen = true;
2087
2066
 
2088
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2089
2068
  }
2090
2069
 
2091
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2114,7 +2093,7 @@ pub const Effects = struct {
2114
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2115
2094
  if (!try checkHit(battle, player, move, options)) return;
2116
2095
  try options.chance.commit(player, .hit);
2117
- return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2118
2097
  }
2119
2098
  }
2120
2099
  if (!try checkHit(battle, player, move, options)) return;
@@ -2123,7 +2102,7 @@ pub const Effects = struct {
2123
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2124
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2125
2104
 
2126
- 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 });
2127
2106
  }
2128
2107
 
2129
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2132,7 +2111,7 @@ pub const Effects = struct {
2132
2111
  if (side.active.volatiles.Mist) return;
2133
2112
  side.active.volatiles.Mist = true;
2134
2113
 
2135
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2136
2115
  }
2137
2116
 
2138
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2159,7 +2138,7 @@ pub const Effects = struct {
2159
2138
  if (showdown) {
2160
2139
  // Invulnerability trumps type immunity on Pokémon Showdown
2161
2140
  if (immune and !foe.active.volatiles.Invulnerable) {
2162
- return log.immune(.{ foe_ident, Immune.None });
2141
+ return log.immune(.{ foe_ident, .None });
2163
2142
  } else if (!try checkHit(battle, player, move, options)) return;
2164
2143
  }
2165
2144
  if (Status.any(foe_stored.status)) {
@@ -2169,7 +2148,7 @@ pub const Effects = struct {
2169
2148
  });
2170
2149
  }
2171
2150
  if (!showdown) {
2172
- if (immune) return log.immune(.{ foe_ident, Immune.None });
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2173
2152
  if (!try checkHit(battle, player, move, options)) return;
2174
2153
  }
2175
2154
  try options.chance.commit(player, .hit);
@@ -2177,7 +2156,7 @@ pub const Effects = struct {
2177
2156
  foe_stored.status = Status.init(.PAR);
2178
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2179
2158
 
2180
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2181
2160
  }
2182
2161
 
2183
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2199,8 +2178,7 @@ pub const Effects = struct {
2199
2178
  foe_stored.status = Status.init(.PAR);
2200
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2201
2180
 
2202
- const reason: protocol.Status = .None;
2203
- try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2181
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2204
2182
  }
2205
2183
 
2206
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
@@ -2220,7 +2198,7 @@ pub const Effects = struct {
2220
2198
  return;
2221
2199
  } else if (foe.active.volatiles.Substitute) {
2222
2200
  if (move.effect != .Poison) return;
2223
- return log.fail(.{ foe_ident, Fail.None });
2201
+ return log.fail(.{ foe_ident, .None });
2224
2202
  } else if (Status.any(foe_stored.status)) {
2225
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2226
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
@@ -2236,7 +2214,7 @@ pub const Effects = struct {
2236
2214
  Fail.None });
2237
2215
  } else if (foe.active.types.includes(.Poison)) {
2238
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2239
- return log.immune(.{ foe_ident, Immune.None });
2217
+ return log.immune(.{ foe_ident, .None });
2240
2218
  }
2241
2219
 
2242
2220
  if (move.effect == .Poison) {
@@ -2255,7 +2233,7 @@ pub const Effects = struct {
2255
2233
  foe.active.volatiles.toxic = 0;
2256
2234
  }
2257
2235
 
2258
- try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2259
2237
  }
2260
2238
 
2261
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2278,7 +2256,7 @@ pub const Effects = struct {
2278
2256
  try options.log.damage(.{
2279
2257
  battle.active(player),
2280
2258
  stored,
2281
- Damage.RecoilOf,
2259
+ .RecoilOf,
2282
2260
  battle.active(player.foe()),
2283
2261
  });
2284
2262
  if (showdown and stored.hp == 0) residual.* = false;
@@ -2286,13 +2264,12 @@ pub const Effects = struct {
2286
2264
 
2287
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2288
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2289
2268
 
2290
- if (side.active.volatiles.Reflect) {
2291
- return options.log.fail(.{ battle.active(player), Fail.None });
2292
- }
2269
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2293
2270
  side.active.volatiles.Reflect = true;
2294
2271
 
2295
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2296
2273
  }
2297
2274
 
2298
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2326,17 +2303,17 @@ pub const Effects = struct {
2326
2303
 
2327
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2328
2305
  const last = battle.side(player).last_selected_move;
2329
- try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.From, last });
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2330
2307
  }
2331
2308
 
2332
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2333
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2334
2311
  }
2335
2312
 
2336
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2337
2314
  var side = battle.side(player);
2338
2315
  if (side.active.volatiles.Substitute) {
2339
- return options.log.fail(.{ battle.active(player), Fail.Substitute });
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2340
2317
  }
2341
2318
 
2342
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2348,16 +2325,16 @@ pub const Effects = struct {
2348
2325
  // avoid having to convert to floating point)
2349
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2350
2327
  if (side.stored().hp < required_hp) {
2351
- return options.log.fail(.{ battle.active(player), Fail.Weak });
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2352
2329
  }
2353
2330
 
2354
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2355
2332
  side.stored().hp -= hp;
2356
2333
  side.active.volatiles.substitute = hp + 1;
2357
2334
  side.active.volatiles.Substitute = true;
2358
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2359
2336
  if (hp > 0) {
2360
- try options.log.damage(.{ battle.active(player), side.stored(), Damage.None });
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2361
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2362
2339
  }
2363
2340
  }
@@ -2371,7 +2348,7 @@ pub const Effects = struct {
2371
2348
  }
2372
2349
  battle.last_damage = 0;
2373
2350
  } else {
2374
- try options.log.fail(.{ battle.active(player), Fail.None });
2351
+ try options.log.fail(.{ battle.active(player), .None });
2375
2352
  try options.log.laststill(.{});
2376
2353
  }
2377
2354
  }
@@ -2441,20 +2418,20 @@ pub const Effects = struct {
2441
2418
  switch (move.effect) {
2442
2419
  .AttackUp1, .AttackUp2, .Rage => {
2443
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2444
- if (boosts.atk == 6) return log.fail(.{ ident, Fail.None });
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2445
2422
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2446
2423
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2447
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2448
2425
  if (stats.atk == MAX_STAT_VALUE) {
2449
2426
  boosts.atk -= 1;
2450
2427
  if (showdown) {
2451
2428
  try log.boost(.{ ident, reason, n });
2452
- try log.boost(.{ ident, Boost.Attack, -1 });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2453
2430
  if (move.effect == .Rage) return;
2454
2431
  }
2455
- return log.fail(.{ ident, Fail.None });
2432
+ return log.fail(.{ ident, .None });
2456
2433
  }
2457
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2458
2435
  const stat = unmodifiedStats(battle, side).atk;
2459
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2460
2437
  try log.boost(.{ ident, reason, n });
@@ -2463,65 +2440,65 @@ pub const Effects = struct {
2463
2440
  },
2464
2441
  .DefenseUp1, .DefenseUp2 => {
2465
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2466
- if (boosts.def == 6) return log.fail(.{ ident, Fail.None });
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2467
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2468
2445
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2469
2446
  if (stats.def == MAX_STAT_VALUE) {
2470
2447
  boosts.def -= 1;
2471
2448
  if (showdown) {
2472
- try log.boost(.{ ident, Boost.Defense, n });
2473
- try log.boost(.{ ident, Boost.Defense, -1 });
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2474
2451
  }
2475
- return log.fail(.{ ident, Fail.None });
2452
+ return log.fail(.{ ident, .None });
2476
2453
  }
2477
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2478
2455
  const stat = unmodifiedStats(battle, side).def;
2479
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2480
- try log.boost(.{ ident, Boost.Defense, n });
2457
+ try log.boost(.{ ident, .Defense, n });
2481
2458
  },
2482
2459
  .SpeedUp2 => {
2483
2460
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2484
- if (boosts.spe == 6) return log.fail(.{ ident, Fail.None });
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2485
2462
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2486
2463
  if (stats.spe == MAX_STAT_VALUE) {
2487
2464
  boosts.spe -= 1;
2488
2465
  if (showdown) {
2489
- try log.boost(.{ ident, Boost.Speed, 2 });
2490
- try log.boost(.{ ident, Boost.Speed, -1 });
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2491
2468
  }
2492
- return log.fail(.{ ident, Fail.None });
2469
+ return log.fail(.{ ident, .None });
2493
2470
  }
2494
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2495
2472
  const stat = unmodifiedStats(battle, side).spe;
2496
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2497
- try log.boost(.{ ident, Boost.Speed, 2 });
2474
+ try log.boost(.{ ident, .Speed, 2 });
2498
2475
  },
2499
2476
  .SpecialUp1, .SpecialUp2 => {
2500
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2501
- if (boosts.spc == 6) return log.fail(.{ ident, Fail.None });
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2502
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2503
2480
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2504
2481
  if (stats.spc == MAX_STAT_VALUE) {
2505
2482
  boosts.spc -= 1;
2506
2483
  if (showdown) {
2507
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2508
- try log.boost(.{ ident, Boost.SpecialAttack, -1 });
2509
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2510
- try log.boost(.{ ident, Boost.SpecialDefense, -1 });
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 });
2511
2488
  }
2512
- return log.fail(.{ ident, Fail.None });
2489
+ return log.fail(.{ ident, .None });
2513
2490
  }
2514
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2515
2492
  const stat = unmodifiedStats(battle, side).spc;
2516
2493
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2517
- try log.boost(.{ ident, Boost.SpecialAttack, n });
2518
- try log.boost(.{ ident, Boost.SpecialDefense, n });
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2519
2496
  },
2520
2497
  .EvasionUp1 => {
2521
2498
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2522
- if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2523
2500
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2524
- try log.boost(.{ ident, Boost.Evasion, 1 });
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2525
2502
  },
2526
2503
  else => unreachable,
2527
2504
  }
@@ -2536,9 +2513,7 @@ pub const Effects = struct {
2536
2513
  const foe_ident = battle.active(player.foe());
2537
2514
  const secondary = move.effect.isStatDownChance();
2538
2515
 
2539
- if (foe.active.volatiles.Substitute) {
2540
- return if (!secondary) log.fail(.{ foe_ident, Fail.None });
2541
- }
2516
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2542
2517
 
2543
2518
  if (secondary) {
2544
2519
  const proc = try Rolls.unboost(battle, player, options);
@@ -2556,82 +2531,82 @@ pub const Effects = struct {
2556
2531
  switch (move.effect) {
2557
2532
  .AttackDown1, .AttackDownChance => {
2558
2533
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2559
- if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2534
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2560
2535
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2561
2536
  if (stats.atk == 1) {
2562
2537
  boosts.atk += 1;
2563
2538
  if (showdown) {
2564
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2565
- try log.boost(.{ foe_ident, Boost.Attack, 1 });
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2566
2541
  }
2567
- return log.fail(.{ foe_ident, Fail.None });
2542
+ return log.fail(.{ foe_ident, .None });
2568
2543
  }
2569
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2570
2545
  const stat = unmodifiedStats(battle, foe).atk;
2571
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2572
- try log.boost(.{ foe_ident, Boost.Attack, -1 });
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2573
2548
  },
2574
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2575
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2576
- if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2551
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2577
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2578
2553
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2579
2554
  if (stats.def == 1) {
2580
2555
  boosts.def += 1;
2581
2556
  if (showdown) {
2582
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2583
- try log.boost(.{ foe_ident, Boost.Defense, 1 });
2557
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2558
+ try log.boost(.{ foe_ident, .Defense, 1 });
2584
2559
  }
2585
- return log.fail(.{ foe_ident, Fail.None });
2560
+ return log.fail(.{ foe_ident, .None });
2586
2561
  }
2587
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2588
2563
  const stat = unmodifiedStats(battle, foe).def;
2589
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2590
- try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2591
2566
  },
2592
2567
  .SpeedDown1, .SpeedDownChance => {
2593
2568
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2594
- if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2569
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2595
2570
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2596
2571
  if (stats.spe == 1) {
2597
2572
  boosts.spe += 1;
2598
2573
  if (showdown) {
2599
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2600
- try log.boost(.{ foe_ident, Boost.Speed, 1 });
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2601
2576
  }
2602
- return log.fail(.{ foe_ident, Fail.None });
2577
+ return log.fail(.{ foe_ident, .None });
2603
2578
  }
2604
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2605
2580
  const stat = unmodifiedStats(battle, foe).spe;
2606
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2607
- try log.boost(.{ foe_ident, Boost.Speed, -1 });
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2608
2583
  assert(boosts.spe >= -6);
2609
2584
  },
2610
2585
  .SpecialDownChance => {
2611
2586
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2612
- if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2587
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2613
2588
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2614
2589
  if (stats.spc == 1) {
2615
2590
  boosts.spc += 1;
2616
2591
  if (showdown) {
2617
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2618
- try log.boost(.{ foe_ident, Boost.SpecialAttack, 1 });
2619
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2620
- try log.boost(.{ foe_ident, Boost.SpecialDefense, 1 });
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 });
2621
2596
  }
2622
- return log.fail(.{ foe_ident, Fail.None });
2597
+ return log.fail(.{ foe_ident, .None });
2623
2598
  }
2624
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2625
2600
  const stat = unmodifiedStats(battle, foe).spc;
2626
2601
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2627
- try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2628
- try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2629
2604
  },
2630
2605
  .AccuracyDown1 => {
2631
2606
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2632
- if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2607
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2633
2608
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2634
- try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2635
2610
  },
2636
2611
  else => unreachable,
2637
2612
  }
@@ -2671,24 +2646,24 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2671
2646
  if (volatiles.disable_move != 0) {
2672
2647
  volatiles.disable_move = 0;
2673
2648
  volatiles.disable_duration = 0;
2674
- try log.end(.{ ident, End.DisableSilent });
2649
+ try log.end(.{ ident, .DisableSilent });
2675
2650
  }
2676
2651
  if (volatiles.Confusion) {
2677
2652
  // volatiles.confusion is left unchanged
2678
2653
  volatiles.Confusion = false;
2679
- try log.end(.{ ident, End.ConfusionSilent });
2654
+ try log.end(.{ ident, .ConfusionSilent });
2680
2655
  }
2681
2656
  if (volatiles.Mist) {
2682
2657
  volatiles.Mist = false;
2683
- try log.end(.{ ident, End.MistSilent });
2658
+ try log.end(.{ ident, .MistSilent });
2684
2659
  }
2685
2660
  if (volatiles.FocusEnergy) {
2686
2661
  volatiles.FocusEnergy = false;
2687
- try log.end(.{ ident, End.FocusEnergySilent });
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2688
2663
  }
2689
2664
  if (volatiles.LeechSeed) {
2690
2665
  volatiles.LeechSeed = false;
2691
- try log.end(.{ ident, End.LeechSeedSilent });
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2692
2667
  }
2693
2668
  if (!showdown and volatiles.Toxic) {
2694
2669
  volatiles.Toxic = false;
@@ -2696,17 +2671,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2696
2671
  }
2697
2672
  if (volatiles.LightScreen) {
2698
2673
  volatiles.LightScreen = false;
2699
- try log.end(.{ ident, End.LightScreenSilent });
2674
+ try log.end(.{ ident, .LightScreenSilent });
2700
2675
  }
2701
2676
  if (volatiles.Reflect) {
2702
2677
  volatiles.Reflect = false;
2703
- try log.end(.{ ident, End.ReflectSilent });
2678
+ try log.end(.{ ident, .ReflectSilent });
2704
2679
  }
2705
2680
  if (showdown and volatiles.Toxic) {
2706
2681
  volatiles.Toxic = false;
2707
2682
  // Pokémon Showdown erroneously clears the toxic counter
2708
2683
  volatiles.toxic = 0;
2709
- try log.end(.{ ident, End.ToxicSilent });
2684
+ try log.end(.{ ident, .ToxicSilent });
2710
2685
  }
2711
2686
  }
2712
2687
 
@@ -2714,6 +2689,8 @@ pub const Rolls = struct {
2714
2689
  fn speedTie(battle: anytype, options: anytype) !bool {
2715
2690
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2716
2691
  player == .P1
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2717
2694
  else if (showdown)
2718
2695
  battle.rng.range(u8, 0, 2) == 0
2719
2696
  else
@@ -2726,6 +2703,8 @@ pub const Rolls = struct {
2726
2703
  fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2727
2704
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2728
2705
  val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2729
2708
  else if (showdown)
2730
2709
  battle.rng.chance(u8, rate, 256)
2731
2710
  else
@@ -2738,6 +2717,8 @@ pub const Rolls = struct {
2738
2717
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2739
2718
  const roll = if (options.calc.overridden(player, .damage)) |val|
2740
2719
  val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2741
2722
  else roll: {
2742
2723
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2743
2724
  while (true) {
@@ -2754,6 +2735,8 @@ pub const Rolls = struct {
2754
2735
  fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2755
2736
  const ok = if (options.calc.overridden(player, .hit)) |val|
2756
2737
  val == .true
2738
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
+ battle.rng.hit(player, accuracy)
2757
2740
  else if (showdown)
2758
2741
  battle.rng.chance(u8, accuracy, 256)
2759
2742
  else
@@ -2766,6 +2749,8 @@ pub const Rolls = struct {
2766
2749
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2767
2750
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2768
2751
  val == .true
2752
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2753
+ battle.rng.confused(player)
2769
2754
  else if (showdown)
2770
2755
  !battle.rng.chance(u8, 128, 256)
2771
2756
  else
@@ -2778,6 +2763,8 @@ pub const Rolls = struct {
2778
2763
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2779
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2780
2765
  val == .true
2766
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2767
+ battle.rng.paralyzed(player)
2781
2768
  else if (showdown)
2782
2769
  battle.rng.chance(u8, 63, 256)
2783
2770
  else
@@ -2790,6 +2777,8 @@ pub const Rolls = struct {
2790
2777
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2791
2778
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2792
2779
  val == .true
2780
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2781
+ battle.rng.secondaryChance(player, 25)
2793
2782
  else if (showdown)
2794
2783
  battle.rng.chance(u8, 25, 256)
2795
2784
  else
@@ -2804,6 +2793,8 @@ pub const Rolls = struct {
2804
2793
 
2805
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2806
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2797
+ battle.rng.secondaryChance(player, rate)
2807
2798
  else if (showdown)
2808
2799
  battle.rng.chance(u8, rate, 256)
2809
2800
  else
@@ -2818,6 +2809,8 @@ pub const Rolls = struct {
2818
2809
 
2819
2810
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2820
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2813
+ battle.rng.secondaryChance(player, rate)
2821
2814
  else if (showdown)
2822
2815
  battle.rng.chance(u8, rate, 256)
2823
2816
  else
@@ -2830,6 +2823,8 @@ pub const Rolls = struct {
2830
2823
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2831
2824
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2832
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2827
+ battle.rng.secondaryChance(player, 85)
2833
2828
  else if (showdown)
2834
2829
  battle.rng.chance(u8, 85, 256)
2835
2830
  else
@@ -2842,6 +2837,8 @@ pub const Rolls = struct {
2842
2837
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2843
2838
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2844
2839
  val
2840
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2841
+ battle.rng.metronome(player)
2845
2842
  else if (showdown)
2846
2843
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2847
2844
  else move: {
@@ -2860,6 +2857,8 @@ pub const Rolls = struct {
2860
2857
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2861
2858
  const power = if (options.calc.overridden(player, .psywave)) |val|
2862
2859
  val - 1
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2861
+ battle.rng.psywave(player)
2863
2862
  else if (showdown)
2864
2863
  battle.rng.range(u8, 0, max)
2865
2864
  else power: {
@@ -2877,6 +2876,8 @@ pub const Rolls = struct {
2877
2876
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2878
2877
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2879
2878
  @intCast(val)
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
+ battle.rng.sleepDuration(player)
2880
2881
  else if (showdown)
2881
2882
  battle.rng.range(u3, 1, 8)
2882
2883
  else duration: {
@@ -2894,6 +2895,8 @@ pub const Rolls = struct {
2894
2895
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
2896
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2896
2897
  val
2898
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
+ battle.rng.disableDuration(player)
2897
2900
  else if (showdown)
2898
2901
  battle.rng.range(u4, 1, 9)
2899
2902
  else
@@ -2907,6 +2910,8 @@ pub const Rolls = struct {
2907
2910
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2908
2911
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2909
2912
  @intCast(val)
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
+ battle.rng.confusionDuration(player, self)
2910
2915
  else if (showdown)
2911
2916
  battle.rng.range(u3, 2, 6)
2912
2917
  else
@@ -2920,6 +2925,8 @@ pub const Rolls = struct {
2920
2925
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2921
2926
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2922
2927
  @intCast(val)
2928
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
+ battle.rng.attackingDuration(player)
2923
2930
  else if (showdown)
2924
2931
  battle.rng.range(u3, 2, 4)
2925
2932
  else
@@ -2941,6 +2948,8 @@ pub const Rolls = struct {
2941
2948
  const roll = if (effect == .Binding) .duration else .multi_hit;
2942
2949
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2943
2950
  @intCast(val)
2951
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
+ battle.rng.distribution(player)
2944
2953
  else if (showdown)
2945
2954
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2946
2955
  else n: {
@@ -2973,7 +2982,9 @@ pub const Rolls = struct {
2973
2982
  else
2974
2983
  null;
2975
2984
 
2976
- 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: {
2977
2988
  if (showdown) {
2978
2989
  if (check_pp == 0) {
2979
2990
  var i: usize = moves.len;
@@ -3053,7 +3064,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3053
3064
  const side = battle.side(player);
3054
3065
  const foe = battle.foe(player);
3055
3066
 
3056
- var active = &side.active;
3067
+ const active = &side.active;
3057
3068
  const stored = side.stored();
3058
3069
 
3059
3070
  // While players are not given any input options on the cartridge in these cases,