@pkmn/engine 0.1.0-dev.dfd46bbb → 0.1.0-dev.e0e37c1b

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.
@@ -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;
@@ -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
 
@@ -644,18 +645,18 @@ fn beforeMove(
644
645
  try options.chance.attacking(player, volatiles.attacks);
645
646
 
646
647
  if (volatiles.attacks != 0) {
647
- try log.activate(ident, .Bide);
648
+ try log.activate(.{ ident, .Bide });
648
649
  return .done;
649
650
  }
650
651
 
651
652
  volatiles.Bide = false;
652
- try log.end(ident, .Bide);
653
+ try log.end(.{ ident, .Bide });
653
654
 
654
655
  battle.last_damage = volatiles.state *% 2;
655
656
  volatiles.state = 0;
656
657
 
657
658
  if (battle.last_damage == 0) {
658
- try log.fail(ident, .None);
659
+ try log.fail(.{ ident, .None });
659
660
  return .done;
660
661
  }
661
662
 
@@ -685,15 +686,15 @@ fn beforeMove(
685
686
  try options.chance.attacking(player, volatiles.attacks);
686
687
 
687
688
  if (!showdown and handleThrashing(battle, active, player, options)) {
688
- try log.start(battle.active(player), .ConfusionSilent);
689
+ try log.start(.{ battle.active(player), .ConfusionSilent });
689
690
  }
690
- try log.move(ident, side.last_selected_move, battle.active(player.foe()));
691
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
691
692
  if (showdown) {
692
693
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
693
694
  // used in useMove and doesn't have the notion of skipping canMove semantics
694
695
  side.last_used_move = side.last_selected_move;
695
696
  if (handleThrashing(battle, active, player, options)) {
696
- try log.start(battle.active(player), .ConfusionSilent);
697
+ try log.start(.{ battle.active(player), .ConfusionSilent });
697
698
  }
698
699
  }
699
700
  return .skip_can;
@@ -709,7 +710,7 @@ fn beforeMove(
709
710
  }
710
711
  try options.chance.binding(player, volatiles.attacks);
711
712
 
712
- try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
713
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
713
714
  if (showdown or battle.last_damage != 0) {
714
715
  const sub = showdown and foe.active.volatiles.Substitute;
715
716
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -742,7 +743,7 @@ fn canMove(
742
743
  side.active.volatiles.Charging = false;
743
744
  side.active.volatiles.Invulnerable = false;
744
745
  } else if (move.effect == .Charge) {
745
- try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
746
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
746
747
  setCounterable(battle, player, side, move);
747
748
  try Effects.charge(battle, player, options);
748
749
  return false;
@@ -752,7 +753,7 @@ fn canMove(
752
753
  if (!skip) decrementPP(side, mslot, auto);
753
754
 
754
755
  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);
756
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
756
757
  setCounterable(battle, player, side, move);
757
758
 
758
759
  if (move.effect.onBegin()) {
@@ -929,16 +930,16 @@ fn doMove(
929
930
  ohko = (!showdown or (!immune and !invulnerable)) and
930
931
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
932
  if (ohko) {
932
- try log.immune(foe_ident, .OHKO);
933
+ try log.immune(.{ foe_ident, .OHKO });
933
934
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
934
- try log.immune(foe_ident, .None);
935
+ try log.immune(.{ foe_ident, .None });
935
936
  } else if (mist) {
936
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
937
- try log.fail(foe_ident, .None);
937
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
938
+ try log.fail(.{ foe_ident, .None });
938
939
  } else {
939
940
  if (showdown or !zero) try options.chance.commit(player, .miss);
940
- try log.lastmiss();
941
- try log.miss(battle.active(player));
941
+ try log.lastmiss(.{});
942
+ try log.miss(.{battle.active(player)});
942
943
  }
943
944
  if (move.effect == .JumpKick) {
944
945
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -974,15 +975,15 @@ fn doMove(
974
975
  var hit: u4 = 0;
975
976
  while (hit < hits) {
976
977
  if (hit == 0) {
977
- if (crit) try log.crit(battle.active(player.foe()));
978
+ if (crit) try log.crit(.{battle.active(player.foe())});
978
979
  if (effectiveness > Effectiveness.neutral) {
979
- try log.supereffective(battle.active(player.foe()));
980
+ try log.supereffective(.{battle.active(player.foe())});
980
981
  } else if (effectiveness < Effectiveness.neutral) {
981
- try log.resisted(battle.active(player.foe()));
982
+ try log.resisted(.{battle.active(player.foe())});
982
983
  }
983
984
  }
984
985
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
985
- if (hit == 0 and ohko) try log.ohko();
986
+ if (hit == 0 and ohko) try log.ohko(.{});
986
987
  hit += 1;
987
988
  if (foe.stored().hp == 0) break;
988
989
  if (!late and (!sub or move.effect == .Explode)) {
@@ -999,7 +1000,7 @@ fn doMove(
999
1000
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1000
1001
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1001
1002
  }
1002
- try log.hitcount(battle.active(player.foe()), hit);
1003
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1003
1004
  } else if (showdown) {
1004
1005
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1005
1006
  if (move.effect == .Binding) {
@@ -1009,9 +1010,9 @@ fn doMove(
1009
1010
  assert(foe.stored().hp > 0);
1010
1011
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1011
1012
  if (sub) {
1012
- try log.activate(battle.active(player.foe()), .Substitute);
1013
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1013
1014
  } else {
1014
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1015
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1015
1016
  try buildRage(battle, player.foe(), options);
1016
1017
  }
1017
1018
  return null;
@@ -1209,7 +1210,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1210
  const foe = battle.foe(player);
1210
1211
 
1211
1212
  if (battle.last_damage == 0) {
1212
- try options.log.fail(battle.active(player), .None);
1213
+ try options.log.fail(.{ battle.active(player), .None });
1213
1214
  return null;
1214
1215
  }
1215
1216
 
@@ -1227,14 +1228,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1228
  foe_last_selected_move.type == .Fighting);
1228
1229
 
1229
1230
  if (!used and !selected) {
1230
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1231
1232
  return null;
1232
1233
  }
1233
1234
 
1234
1235
  if (!used or !selected) {
1235
1236
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1236
1237
  if (!showdown) return Result.Error;
1237
- try options.log.fail(battle.active(player), .None);
1238
+ try options.log.fail(.{ battle.active(player), .None });
1238
1239
  return null;
1239
1240
  }
1240
1241
 
@@ -1270,20 +1271,20 @@ fn applyDamage(
1270
1271
  subbed.active.volatiles.substitute = 0;
1271
1272
  subbed.active.volatiles.Substitute = false;
1272
1273
  // battle.last_damage is not updated with the amount of HP the Substitute had
1273
- try options.log.end(battle.active(sub_player), .Substitute);
1274
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1274
1275
  options.calc.capped(target_player);
1275
1276
  return true;
1276
1277
  } else {
1277
1278
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1278
1279
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1279
- try options.log.activate(battle.active(sub_player), .Substitute);
1280
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1280
1281
  return false;
1281
1282
  }
1282
1283
  }
1283
1284
 
1284
1285
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1285
1286
  target.stored().hp -= battle.last_damage;
1286
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1287
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1287
1288
  return false;
1288
1289
  }
1289
1290
 
@@ -1302,7 +1303,7 @@ fn mirrorMove(
1302
1303
  side.last_selected_move = foe.last_used_move;
1303
1304
 
1304
1305
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1305
- try options.log.fail(battle.active(player), .None);
1306
+ try options.log.fail(.{ battle.active(player), .None });
1306
1307
  return null;
1307
1308
  } else if (!showdown or foe.last_used_move != .Struggle) {
1308
1309
  incrementPP(side, mslot);
@@ -1344,12 +1345,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1344
1345
  if (mist) {
1345
1346
  assert(!showdown);
1346
1347
  const foe_ident = battle.active(player.foe());
1347
- try options.log.activate(foe_ident, .Mist);
1348
- try options.log.fail(foe_ident, .None);
1348
+ try options.log.activate(.{ foe_ident, .Mist });
1349
+ try options.log.fail(.{ foe_ident, .None });
1349
1350
  } else {
1350
1351
  try options.chance.commit(player, .miss);
1351
- try options.log.lastmiss();
1352
- try options.log.miss(battle.active(player));
1352
+ try options.log.lastmiss(.{});
1353
+ try options.log.miss(.{battle.active(player)});
1353
1354
  }
1354
1355
 
1355
1356
  return false;
@@ -1450,13 +1451,13 @@ fn checkFaint(
1450
1451
  assert(!foe.active.volatiles.MultiHit);
1451
1452
 
1452
1453
  if (player_out and foe_out) {
1453
- try options.log.tie();
1454
+ try options.log.tie(.{});
1454
1455
  return Result.Tie;
1455
1456
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1457
+ try options.log.win(.{player.foe()});
1457
1458
  return if (player == .P1) Result.Lose else Result.Win;
1458
1459
  } else if (foe_out) {
1459
- try options.log.win(player);
1460
+ try options.log.win(.{player});
1460
1461
  return if (player == .P1) Result.Win else Result.Lose;
1461
1462
  }
1462
1463
 
@@ -1498,7 +1499,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1499
  side.stored().stats.spe;
1499
1500
  }
1500
1501
 
1501
- try options.log.faint(battle.active(player), done);
1502
+ try options.log.faint(.{ battle.active(player), done });
1502
1503
  options.calc.capped(player);
1503
1504
  return null;
1504
1505
  }
@@ -1520,8 +1521,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1521
 
1521
1522
  if (!showdown or damage > 0) {
1522
1523
  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);
1524
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1525
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1525
1526
  }
1526
1527
  }
1527
1528
 
@@ -1546,14 +1547,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1547
  const amount = @min(damage, stored.hp);
1547
1548
  stored.hp -= amount;
1548
1549
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1550
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1551
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1551
1552
 
1552
1553
  const before = foe_stored.hp;
1553
1554
  // Uncapped damage is added back to the foe
1554
1555
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1556
  // 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);
1557
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1558
  }
1558
1559
  }
1559
1560
 
@@ -1564,18 +1565,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1564
1565
  battle.turn += 1;
1565
1566
 
1566
1567
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1568
+ try options.log.tie(.{});
1568
1569
  return Result.Tie;
1569
1570
  }
1570
1571
 
1571
1572
  if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1573
+ try options.log.tie(.{});
1573
1574
  return Result.Tie;
1574
1575
  } else if (battle.turn >= 65535) {
1575
1576
  return Result.Error;
1576
1577
  }
1577
1578
 
1578
- try options.log.turn(battle.turn);
1579
+ try options.log.turn(.{battle.turn});
1579
1580
 
1580
1581
  return Result.Default;
1581
1582
  }
@@ -1645,7 +1646,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1646
  side.last_selected_move;
1646
1647
  if (m != .None and m == last) {
1647
1648
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1649
+ try options.log.cant(.{ ident, .Disable, last });
1649
1650
  return true;
1650
1651
  }
1651
1652
  }
@@ -1762,7 +1763,7 @@ pub const Effects = struct {
1762
1763
  side.active.volatiles.state = 0;
1763
1764
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1764
1765
 
1765
- try options.log.start(battle.active(player), .Bide);
1766
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1767
  }
1767
1768
 
1768
1769
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1777,11 @@ pub const Effects = struct {
1776
1777
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1778
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1779
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1780
+ try options.log.curestatus(.{
1780
1781
  battle.active(player.foe()),
1781
1782
  foe_stored.status,
1782
1783
  .Message,
1783
- );
1784
+ });
1784
1785
  foe_stored.status = 0;
1785
1786
  }
1786
1787
  return;
@@ -1794,7 +1795,7 @@ pub const Effects = struct {
1794
1795
  foe_stored.status = Status.init(.BRN);
1795
1796
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1797
 
1797
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1798
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1798
1799
  }
1799
1800
 
1800
1801
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1805,8 @@ pub const Effects = struct {
1804
1805
  volatiles.Charging = true;
1805
1806
  const move = side.last_selected_move;
1806
1807
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1808
+ try options.log.laststill(.{});
1809
+ try options.log.prepare(.{ battle.active(player), move });
1809
1810
  }
1810
1811
 
1811
1812
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,17 +1817,11 @@ pub const Effects = struct {
1816
1817
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1818
  } else {
1818
1819
  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
- }
1820
+ if (!try checkHit(battle, player, move, options)) return;
1821
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1822
  } 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
- }
1823
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
+ if (!try checkHit(battle, player, move, options)) return;
1830
1825
  }
1831
1826
  try options.chance.commit(player, .hit);
1832
1827
  }
@@ -1835,23 +1830,24 @@ pub const Effects = struct {
1835
1830
  foe.active.volatiles.Confusion = true;
1836
1831
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1832
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1833
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1834
  }
1840
1835
 
1841
1836
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1837
  const foe = battle.foe(player);
1843
1838
 
1844
1839
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1840
+ try options.log.lastmiss(.{});
1841
+ return options.log.miss(.{battle.active(player)});
1847
1842
  }
1848
1843
 
1849
1844
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1845
+ return options.log.start(.{
1851
1846
  battle.active(player),
1847
+ .TypeChange,
1852
1848
  foe.active.types,
1853
1849
  battle.active(player.foe()),
1854
- );
1850
+ });
1855
1851
  }
1856
1852
 
1857
1853
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1866,7 +1862,7 @@ pub const Effects = struct {
1866
1862
  }
1867
1863
 
1868
1864
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1865
+ try options.log.fail(.{ foe_ident, .None });
1870
1866
  return null;
1871
1867
  }
1872
1868
 
@@ -1882,7 +1878,7 @@ pub const Effects = struct {
1882
1878
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1879
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
1880
  if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1881
+ try options.log.fail(.{ foe_ident, .None });
1886
1882
  return null;
1887
1883
  } else if (err) {
1888
1884
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1895,12 +1891,13 @@ pub const Effects = struct {
1895
1891
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1896
1892
 
1897
1893
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1894
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1895
  return null;
1900
1896
  }
1901
1897
 
1902
1898
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1899
  var stored = battle.side(player).stored();
1900
+ const ident = battle.active(player);
1904
1901
 
1905
1902
  if (battle.last_damage == 0) {
1906
1903
  assert(showdown);
@@ -1913,7 +1910,7 @@ pub const Effects = struct {
1913
1910
  if (stored.hp == stored.stats.hp) return;
1914
1911
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1912
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1913
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1914
  }
1918
1915
 
1919
1916
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +1942,7 @@ pub const Effects = struct {
1945
1942
  if (side.active.volatiles.FocusEnergy) return;
1946
1943
  side.active.volatiles.FocusEnergy = true;
1947
1944
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
1945
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
1946
  }
1950
1947
 
1951
1948
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1967,7 +1964,7 @@ pub const Effects = struct {
1967
1964
  foe_stored.status = Status.init(.FRZ);
1968
1965
  // GLITCH: Hyper Beam recharging status is not cleared
1969
1966
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
1967
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
1968
  }
1972
1969
 
1973
1970
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1987,8 +1984,8 @@ pub const Effects = struct {
1987
1984
  side.active.stats = side_stored.stats;
1988
1985
  foe.active.stats = foe_stored.stats;
1989
1986
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
1987
+ try log.activate(.{ player_ident, .Haze });
1988
+ try log.clearallboost(.{});
1992
1989
 
1993
1990
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
1991
  if (showdown) {
@@ -1996,11 +1993,11 @@ pub const Effects = struct {
1996
1993
  const p: Player = @enumFromInt(i);
1997
1994
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
1995
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1996
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
1997
  s.stored().status = 0;
2001
1998
  } else if (showdown and s.stored().status == Status.TOX) {
2002
1999
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
2000
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
2001
  }
2005
2002
  try clearVolatiles(battle, p, p != player, options);
2006
2003
  }
@@ -2009,7 +2006,7 @@ pub const Effects = struct {
2009
2006
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
2007
  foe.last_selected_move = .SKIP_TURN;
2011
2008
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2009
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
2010
  foe_stored.status = 0;
2014
2011
  }
2015
2012
  try clearVolatiles(battle, player, false, options);
@@ -2025,7 +2022,7 @@ pub const Effects = struct {
2025
2022
  // GLITCH: HP recovery move failure glitches
2026
2023
  const delta = stored.stats.hp - stored.hp;
2027
2024
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return options.log.fail(ident, .None);
2025
+ return options.log.fail(.{ ident, .None });
2029
2026
  }
2030
2027
 
2031
2028
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2030,17 @@ pub const Effects = struct {
2033
2030
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2031
  if (showdown) battle.rng.advance(1);
2035
2032
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2033
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2034
  stored.hp = stored.stats.hp;
2038
2035
  } else {
2039
2036
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2037
  }
2041
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2038
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2042
2039
  }
2043
2040
 
2044
2041
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2042
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2043
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2044
  }
2048
2045
 
2049
2046
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2049,32 @@ pub const Effects = struct {
2052
2049
  if (showdown) {
2053
2050
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2051
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2052
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2053
  }
2057
2054
  if (!try checkHit(battle, player, move, options)) return;
2058
2055
  if (foe.active.volatiles.LeechSeed) return;
2059
2056
  } else {
2060
2057
  if (!try checkHit(battle, player, move, options)) return;
2061
2058
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2059
+ try options.log.lastmiss(.{});
2060
+ return options.log.miss(.{battle.active(player)});
2064
2061
  }
2065
2062
  }
2066
2063
 
2067
2064
  try options.chance.commit(player, .hit);
2068
2065
  foe.active.volatiles.LeechSeed = true;
2069
2066
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2067
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2068
  }
2072
2069
 
2073
2070
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2071
  var side = battle.side(player);
2072
+ const ident = battle.active(player);
2075
2073
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2074
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2075
  side.active.volatiles.LightScreen = true;
2080
2076
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2077
+ try options.log.start(.{ ident, .LightScreen });
2082
2078
  }
2083
2079
 
2084
2080
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2107,7 +2103,7 @@ pub const Effects = struct {
2107
2103
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2104
  if (!try checkHit(battle, player, move, options)) return;
2109
2105
  try options.chance.commit(player, .hit);
2110
- return options.log.fail(battle.active(player.foe()), .None);
2106
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2111
2107
  }
2112
2108
  }
2113
2109
  if (!try checkHit(battle, player, move, options)) return;
@@ -2116,7 +2112,7 @@ pub const Effects = struct {
2116
2112
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2113
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2114
 
2119
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2115
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2120
2116
  }
2121
2117
 
2122
2118
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2121,7 @@ pub const Effects = struct {
2125
2121
  if (side.active.volatiles.Mist) return;
2126
2122
  side.active.volatiles.Mist = true;
2127
2123
 
2128
- try options.log.start(battle.active(player), .Mist);
2124
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2125
  }
2130
2126
 
2131
2127
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,17 +2147,18 @@ pub const Effects = struct {
2151
2147
 
2152
2148
  if (showdown) {
2153
2149
  // 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;
2150
+ if (immune and !foe.active.volatiles.Invulnerable) {
2151
+ return log.immune(.{ foe_ident, .None });
2152
+ } else if (!try checkHit(battle, player, move, options)) return;
2156
2153
  }
2157
2154
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2155
+ return log.fail(.{
2159
2156
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2157
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2158
+ });
2162
2159
  }
2163
2160
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2161
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2162
  if (!try checkHit(battle, player, move, options)) return;
2166
2163
  }
2167
2164
  try options.chance.commit(player, .hit);
@@ -2169,7 +2166,7 @@ pub const Effects = struct {
2169
2166
  foe_stored.status = Status.init(.PAR);
2170
2167
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2168
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2169
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2170
  }
2174
2171
 
2175
2172
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2188,12 @@ pub const Effects = struct {
2191
2188
  foe_stored.status = Status.init(.PAR);
2192
2189
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2190
 
2194
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2191
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2195
2192
  }
2196
2193
 
2197
2194
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2195
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2196
+ try options.log.fieldactivate(.{});
2200
2197
  }
2201
2198
  }
2202
2199
 
@@ -2211,23 +2208,23 @@ pub const Effects = struct {
2211
2208
  return;
2212
2209
  } else if (foe.active.volatiles.Substitute) {
2213
2210
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2211
+ return log.fail(.{ foe_ident, .None });
2215
2212
  } else if (Status.any(foe_stored.status)) {
2216
2213
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2214
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2215
  // 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))
2216
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2217
  if (!showdown)
2221
- .Poison
2218
+ Fail.Poison
2222
2219
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2220
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2221
  else
2225
- .None
2222
+ Fail.None
2226
2223
  else
2227
- .None);
2224
+ Fail.None });
2228
2225
  } else if (foe.active.types.includes(.Poison)) {
2229
2226
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2227
+ return log.immune(.{ foe_ident, .None });
2231
2228
  }
2232
2229
 
2233
2230
  if (move.effect == .Poison) {
@@ -2246,7 +2243,7 @@ pub const Effects = struct {
2246
2243
  foe.active.volatiles.toxic = 0;
2247
2244
  }
2248
2245
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2246
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2247
  }
2251
2248
 
2252
2249
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2263,23 @@ pub const Effects = struct {
2266
2263
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2264
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2265
 
2269
- try options.log.damageOf(
2266
+ try options.log.damage(.{
2270
2267
  battle.active(player),
2271
2268
  stored,
2272
2269
  .RecoilOf,
2273
2270
  battle.active(player.foe()),
2274
- );
2271
+ });
2275
2272
  if (showdown and stored.hp == 0) residual.* = false;
2276
2273
  }
2277
2274
 
2278
2275
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2276
  var side = battle.side(player);
2277
+ const ident = battle.active(player);
2280
2278
 
2281
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2279
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2282
2280
  side.active.volatiles.Reflect = true;
2283
2281
 
2284
- try options.log.start(battle.active(player), .Reflect);
2282
+ try options.log.start(.{ ident, .Reflect });
2285
2283
  }
2286
2284
 
2287
2285
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,10 +2293,10 @@ pub const Effects = struct {
2295
2293
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2294
  } else {
2297
2295
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2296
+ return options.log.fail(.{
2299
2297
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2298
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
+ });
2302
2300
  }
2303
2301
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2304
2302
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2315,17 +2313,17 @@ pub const Effects = struct {
2315
2313
 
2316
2314
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2317
2315
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2316
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2317
  }
2320
2318
 
2321
2319
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2320
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2321
  }
2324
2322
 
2325
2323
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2324
  var side = battle.side(player);
2327
2325
  if (side.active.volatiles.Substitute) {
2328
- return options.log.fail(battle.active(player), .Substitute);
2326
+ return options.log.fail(.{ battle.active(player), .Substitute });
2329
2327
  }
2330
2328
 
2331
2329
  assert(side.stored().stats.hp <= 1023);
@@ -2336,15 +2334,17 @@ pub const Effects = struct {
2336
2334
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2337
2335
  // avoid having to convert to floating point)
2338
2336
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2339
- if (side.stored().hp < required_hp) return options.log.fail(battle.active(player), .Weak);
2337
+ if (side.stored().hp < required_hp) {
2338
+ return options.log.fail(.{ battle.active(player), .Weak });
2339
+ }
2340
2340
 
2341
2341
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2342
2342
  side.stored().hp -= hp;
2343
2343
  side.active.volatiles.substitute = hp + 1;
2344
2344
  side.active.volatiles.Substitute = true;
2345
- try options.log.start(battle.active(player), .Substitute);
2345
+ try options.log.start(.{ battle.active(player), .Substitute });
2346
2346
  if (hp > 0) {
2347
- try options.log.damage(battle.active(player), side.stored(), .None);
2347
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2348
2348
  if (showdown and side.stored().hp == 0) residual.* = false;
2349
2349
  }
2350
2350
  }
@@ -2358,8 +2358,8 @@ pub const Effects = struct {
2358
2358
  }
2359
2359
  battle.last_damage = 0;
2360
2360
  } else {
2361
- try options.log.fail(battle.active(player), .None);
2362
- try options.log.laststill();
2361
+ try options.log.fail(.{ battle.active(player), .None });
2362
+ try options.log.laststill(.{});
2363
2363
  }
2364
2364
  }
2365
2365
 
@@ -2398,7 +2398,7 @@ pub const Effects = struct {
2398
2398
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2399
2399
  }
2400
2400
 
2401
- try options.log.transform(battle.active(player), foe_ident);
2401
+ try options.log.transform(.{ battle.active(player), foe_ident });
2402
2402
  }
2403
2403
 
2404
2404
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2428,87 +2428,87 @@ pub const Effects = struct {
2428
2428
  switch (move.effect) {
2429
2429
  .AttackUp1, .AttackUp2, .Rage => {
2430
2430
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2431
- if (boosts.atk == 6) return log.fail(ident, .None);
2431
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2432
2432
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2433
2433
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2434
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2434
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2435
2435
  if (stats.atk == MAX_STAT_VALUE) {
2436
2436
  boosts.atk -= 1;
2437
2437
  if (showdown) {
2438
- try log.boost(ident, reason, n);
2439
- try log.boost(ident, Boost.Attack, -1);
2438
+ try log.boost(.{ ident, reason, n });
2439
+ try log.boost(.{ ident, .Attack, -1 });
2440
2440
  if (move.effect == .Rage) return;
2441
2441
  }
2442
- return log.fail(ident, .None);
2442
+ return log.fail(.{ ident, .None });
2443
2443
  }
2444
2444
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2445
  const stat = unmodifiedStats(battle, side).atk;
2446
2446
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
- try log.boost(ident, reason, n);
2447
+ try log.boost(.{ ident, reason, n });
2448
2448
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2449
2449
  if (showdown and move.effect == .Rage) return;
2450
2450
  },
2451
2451
  .DefenseUp1, .DefenseUp2 => {
2452
2452
  assert(boosts.def >= -6 and boosts.def <= 6);
2453
- if (boosts.def == 6) return log.fail(ident, .None);
2453
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2454
2454
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2455
2455
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2456
2456
  if (stats.def == MAX_STAT_VALUE) {
2457
2457
  boosts.def -= 1;
2458
2458
  if (showdown) {
2459
- try log.boost(ident, .Defense, n);
2460
- try log.boost(ident, .Defense, -1);
2459
+ try log.boost(.{ ident, .Defense, n });
2460
+ try log.boost(.{ ident, .Defense, -1 });
2461
2461
  }
2462
- return log.fail(ident, .None);
2462
+ return log.fail(.{ ident, .None });
2463
2463
  }
2464
2464
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2465
  const stat = unmodifiedStats(battle, side).def;
2466
2466
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
- try log.boost(ident, .Defense, n);
2467
+ try log.boost(.{ ident, .Defense, n });
2468
2468
  },
2469
2469
  .SpeedUp2 => {
2470
2470
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2471
- if (boosts.spe == 6) return log.fail(ident, .None);
2471
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2472
2472
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2473
2473
  if (stats.spe == MAX_STAT_VALUE) {
2474
2474
  boosts.spe -= 1;
2475
2475
  if (showdown) {
2476
- try log.boost(ident, .Speed, 2);
2477
- try log.boost(ident, .Speed, -1);
2476
+ try log.boost(.{ ident, .Speed, 2 });
2477
+ try log.boost(.{ ident, .Speed, -1 });
2478
2478
  }
2479
- return log.fail(ident, .None);
2479
+ return log.fail(.{ ident, .None });
2480
2480
  }
2481
2481
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2482
  const stat = unmodifiedStats(battle, side).spe;
2483
2483
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
- try log.boost(ident, .Speed, 2);
2484
+ try log.boost(.{ ident, .Speed, 2 });
2485
2485
  },
2486
2486
  .SpecialUp1, .SpecialUp2 => {
2487
2487
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2488
- if (boosts.spc == 6) return log.fail(ident, .None);
2488
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2489
2489
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2490
2490
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2491
2491
  if (stats.spc == MAX_STAT_VALUE) {
2492
2492
  boosts.spc -= 1;
2493
2493
  if (showdown) {
2494
- try log.boost(ident, .SpecialAttack, n);
2495
- try log.boost(ident, .SpecialAttack, -1);
2496
- try log.boost(ident, .SpecialDefense, n);
2497
- try log.boost(ident, .SpecialDefense, -1);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2496
+ try log.boost(.{ ident, .SpecialDefense, n });
2497
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2498
2498
  }
2499
- return log.fail(ident, .None);
2499
+ return log.fail(.{ ident, .None });
2500
2500
  }
2501
2501
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2502
  const stat = unmodifiedStats(battle, side).spc;
2503
2503
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
- try log.boost(ident, .SpecialAttack, n);
2505
- try log.boost(ident, .SpecialDefense, n);
2504
+ try log.boost(.{ ident, .SpecialAttack, n });
2505
+ try log.boost(.{ ident, .SpecialDefense, n });
2506
2506
  },
2507
2507
  .EvasionUp1 => {
2508
2508
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2509
- if (boosts.evasion == 6) return log.fail(ident, .None);
2509
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2510
2510
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2511
- try log.boost(ident, .Evasion, 1);
2511
+ try log.boost(.{ ident, .Evasion, 1 });
2512
2512
  },
2513
2513
  else => unreachable,
2514
2514
  }
@@ -2523,7 +2523,7 @@ pub const Effects = struct {
2523
2523
  const foe_ident = battle.active(player.foe());
2524
2524
  const secondary = move.effect.isStatDownChance();
2525
2525
 
2526
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2526
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2527
2527
 
2528
2528
  if (secondary) {
2529
2529
  const proc = try Rolls.unboost(battle, player, options);
@@ -2541,82 +2541,82 @@ pub const Effects = struct {
2541
2541
  switch (move.effect) {
2542
2542
  .AttackDown1, .AttackDownChance => {
2543
2543
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2544
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2544
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2545
2545
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2546
2546
  if (stats.atk == 1) {
2547
2547
  boosts.atk += 1;
2548
2548
  if (showdown) {
2549
- try log.boost(foe_ident, .Attack, -1);
2550
- try log.boost(foe_ident, .Attack, 1);
2549
+ try log.boost(.{ foe_ident, .Attack, -1 });
2550
+ try log.boost(.{ foe_ident, .Attack, 1 });
2551
2551
  }
2552
- return log.fail(foe_ident, .None);
2552
+ return log.fail(.{ foe_ident, .None });
2553
2553
  }
2554
2554
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2555
  const stat = unmodifiedStats(battle, foe).atk;
2556
2556
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
- try log.boost(foe_ident, .Attack, -1);
2557
+ try log.boost(.{ foe_ident, .Attack, -1 });
2558
2558
  },
2559
2559
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2560
2560
  assert(boosts.def >= -6 and boosts.def <= 6);
2561
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2561
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2562
2562
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2563
2563
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2564
2564
  if (stats.def == 1) {
2565
2565
  boosts.def += 1;
2566
2566
  if (showdown) {
2567
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2568
- try log.boost(foe_ident, .Defense, 1);
2567
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2568
+ try log.boost(.{ foe_ident, .Defense, 1 });
2569
2569
  }
2570
- return log.fail(foe_ident, .None);
2570
+ return log.fail(.{ foe_ident, .None });
2571
2571
  }
2572
2572
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2573
  const stat = unmodifiedStats(battle, foe).def;
2574
2574
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2575
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2576
2576
  },
2577
2577
  .SpeedDown1, .SpeedDownChance => {
2578
2578
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2579
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2579
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2580
2580
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2581
2581
  if (stats.spe == 1) {
2582
2582
  boosts.spe += 1;
2583
2583
  if (showdown) {
2584
- try log.boost(foe_ident, .Speed, -1);
2585
- try log.boost(foe_ident, .Speed, 1);
2584
+ try log.boost(.{ foe_ident, .Speed, -1 });
2585
+ try log.boost(.{ foe_ident, .Speed, 1 });
2586
2586
  }
2587
- return log.fail(foe_ident, .None);
2587
+ return log.fail(.{ foe_ident, .None });
2588
2588
  }
2589
2589
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2590
  const stat = unmodifiedStats(battle, foe).spe;
2591
2591
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
- try log.boost(foe_ident, .Speed, -1);
2592
+ try log.boost(.{ foe_ident, .Speed, -1 });
2593
2593
  assert(boosts.spe >= -6);
2594
2594
  },
2595
2595
  .SpecialDownChance => {
2596
2596
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2597
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2597
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2598
2598
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2599
2599
  if (stats.spc == 1) {
2600
2600
  boosts.spc += 1;
2601
2601
  if (showdown) {
2602
- try log.boost(foe_ident, .SpecialAttack, -1);
2603
- try log.boost(foe_ident, .SpecialAttack, 1);
2604
- try log.boost(foe_ident, .SpecialDefense, -1);
2605
- try log.boost(foe_ident, .SpecialDefense, 1);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2604
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2605
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2606
2606
  }
2607
- return log.fail(foe_ident, .None);
2607
+ return log.fail(.{ foe_ident, .None });
2608
2608
  }
2609
2609
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2610
  const stat = unmodifiedStats(battle, foe).spc;
2611
2611
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
- try log.boost(foe_ident, .SpecialAttack, -1);
2613
- try log.boost(foe_ident, .SpecialDefense, -1);
2612
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2613
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2614
2614
  },
2615
2615
  .AccuracyDown1 => {
2616
2616
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2617
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2617
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2618
2618
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2619
- try log.boost(foe_ident, .Accuracy, -1);
2619
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2620
2620
  },
2621
2621
  else => unreachable,
2622
2622
  }
@@ -2656,24 +2656,24 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2656
2656
  if (volatiles.disable_move != 0) {
2657
2657
  volatiles.disable_move = 0;
2658
2658
  volatiles.disable_duration = 0;
2659
- try log.end(ident, .DisableSilent);
2659
+ try log.end(.{ ident, .DisableSilent });
2660
2660
  }
2661
2661
  if (volatiles.Confusion) {
2662
2662
  // volatiles.confusion is left unchanged
2663
2663
  volatiles.Confusion = false;
2664
- try log.end(ident, .ConfusionSilent);
2664
+ try log.end(.{ ident, .ConfusionSilent });
2665
2665
  }
2666
2666
  if (volatiles.Mist) {
2667
2667
  volatiles.Mist = false;
2668
- try log.end(ident, .MistSilent);
2668
+ try log.end(.{ ident, .MistSilent });
2669
2669
  }
2670
2670
  if (volatiles.FocusEnergy) {
2671
2671
  volatiles.FocusEnergy = false;
2672
- try log.end(ident, .FocusEnergySilent);
2672
+ try log.end(.{ ident, .FocusEnergySilent });
2673
2673
  }
2674
2674
  if (volatiles.LeechSeed) {
2675
2675
  volatiles.LeechSeed = false;
2676
- try log.end(ident, .LeechSeedSilent);
2676
+ try log.end(.{ ident, .LeechSeedSilent });
2677
2677
  }
2678
2678
  if (!showdown and volatiles.Toxic) {
2679
2679
  volatiles.Toxic = false;
@@ -2681,17 +2681,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2681
2681
  }
2682
2682
  if (volatiles.LightScreen) {
2683
2683
  volatiles.LightScreen = false;
2684
- try log.end(ident, .LightScreenSilent);
2684
+ try log.end(.{ ident, .LightScreenSilent });
2685
2685
  }
2686
2686
  if (volatiles.Reflect) {
2687
2687
  volatiles.Reflect = false;
2688
- try log.end(ident, .ReflectSilent);
2688
+ try log.end(.{ ident, .ReflectSilent });
2689
2689
  }
2690
2690
  if (showdown and volatiles.Toxic) {
2691
2691
  volatiles.Toxic = false;
2692
2692
  // Pokémon Showdown erroneously clears the toxic counter
2693
2693
  volatiles.toxic = 0;
2694
- try log.end(ident, .ToxicSilent);
2694
+ try log.end(.{ ident, .ToxicSilent });
2695
2695
  }
2696
2696
  }
2697
2697