@pkmn/engine 0.1.0-dev.dd5b9081 → 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.
Files changed (38) hide show
  1. package/README.md +5 -5
  2. package/build/pkg/data/ids.json +12 -12
  3. package/build/pkg/data/layout.json +2 -2
  4. package/build/pkg/data/protocol.json +69 -12
  5. package/build/pkg/data.d.ts +2 -2
  6. package/build/pkg/gen1.js +2 -2
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/protocol.js +56 -37
  9. package/build/pkg/protocol.js.map +1 -1
  10. package/build/tools/debug.js +3 -1
  11. package/build/tools/debug.js.map +1 -1
  12. package/build.zig +21 -15
  13. package/package.json +28 -16
  14. package/src/lib/binding/node.zig +1 -1
  15. package/src/lib/common/data.zig +5 -5
  16. package/src/lib/common/optional.zig +8 -1
  17. package/src/lib/common/protocol.zig +695 -270
  18. package/src/lib/common/rational.zig +5 -5
  19. package/src/lib/common/rng.zig +14 -5
  20. package/src/lib/gen1/README.md +28 -28
  21. package/src/lib/gen1/calc.zig +4 -4
  22. package/src/lib/gen1/chance.zig +53 -29
  23. package/src/lib/gen1/data/moves.zig +190 -176
  24. package/src/lib/gen1/data/species.zig +2 -2
  25. package/src/lib/gen1/data/types.zig +7 -7
  26. package/src/lib/gen1/data.zig +25 -25
  27. package/src/lib/gen1/helpers.zig +26 -41
  28. package/src/lib/gen1/mechanics.zig +283 -324
  29. package/src/lib/gen2/README.md +18 -72
  30. package/src/lib/gen2/calc.zig +143 -0
  31. package/src/lib/gen2/chance.zig +1314 -0
  32. package/src/lib/gen2/data/items.zig +12 -18
  33. package/src/lib/gen2/data/moves.zig +377 -344
  34. package/src/lib/gen2/data/species.zig +1 -1
  35. package/src/lib/gen2/data/types.zig +134 -31
  36. package/src/lib/gen2/data.zig +44 -36
  37. package/src/lib/gen2/generated.zig +808 -0
  38. package/src/lib/gen2/mechanics.zig +3000 -206
@@ -7,23 +7,22 @@ const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
- const Action = @import("chance.zig").Action;
11
-
12
10
  const data = @import("data.zig");
13
11
 
14
12
  const assert = std.debug.assert;
15
13
 
16
14
  const expectEqual = std.testing.expectEqual;
17
15
 
16
+ const showdown = pkmn.options.showdown;
17
+
18
18
  const Choice = common.Choice;
19
19
  const ID = common.ID;
20
20
  const Player = common.Player;
21
21
  const Result = common.Result;
22
22
 
23
- const showdown = pkmn.options.showdown;
24
-
25
23
  const Boost = protocol.Boost;
26
24
  const Damage = protocol.Damage;
25
+ const Fail = protocol.Fail;
27
26
  const Heal = protocol.Heal;
28
27
 
29
28
  const Gen12 = rng.Gen12;
@@ -59,23 +58,15 @@ const BOOSTS = &[_][2]u8{
59
58
 
60
59
  const MAX_STAT_VALUE = 999;
61
60
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
61
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
63
  if (battle.turn == 0) return start(battle, options);
71
64
 
72
65
  var s1 = false;
73
66
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
67
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
68
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
69
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
70
 
80
71
  var p1 = battle.side(.P1);
81
72
  var p2 = battle.side(.P2);
@@ -84,9 +75,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
84
75
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
76
 
86
77
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
78
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
79
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
80
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
81
  }
91
82
 
92
83
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -124,7 +115,6 @@ fn selectMove(
124
115
  choice: Choice,
125
116
  foe_choice: Choice,
126
117
  skip_turn: *bool,
127
- from: *?Move,
128
118
  ) ?Result {
129
119
  if (choice.type == .Pass) return null;
130
120
 
@@ -141,7 +131,6 @@ fn selectMove(
141
131
  return null;
142
132
  }
143
133
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
134
  if (showdown) {
146
135
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
136
  saveMove(battle, player, null);
@@ -151,7 +140,6 @@ fn selectMove(
151
140
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
141
  if (!showdown) volatiles.Flinch = false;
153
142
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
143
  if (showdown) {
156
144
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
145
  saveMove(battle, player, null);
@@ -172,9 +160,7 @@ fn selectMove(
172
160
  return null;
173
161
  }
174
162
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
163
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
164
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
165
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
166
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -210,7 +196,7 @@ fn selectMove(
210
196
  if (choice.data == 0) {
211
197
  const struggle = ok: {
212
198
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
199
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
200
  }
215
201
  break :ok true;
216
202
  };
@@ -229,7 +215,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
229
215
  if (c.data == 0) {
230
216
  side.last_selected_move = .Struggle;
231
217
  } else {
232
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
233
219
  const move = side.active.move(c.data);
234
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
235
221
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
256
 
271
257
  foe.active.volatiles.Binding = false;
272
258
 
273
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
274
260
  options.chance.switched(player, side.order[0], out);
275
261
 
276
262
  if (showdown and incoming.status == Status.TOX) {
277
263
  incoming.status = Status.init(.PSN);
278
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
279
265
  // rather not clutter up turnOrder just for this (incorrect) log message
280
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
281
267
  }
282
268
  }
283
269
 
@@ -340,12 +326,10 @@ fn doTurn(
340
326
  player_choice: Choice,
341
327
  player_rewrap: bool,
342
328
  player_skip: bool,
343
- player_from: ?Move,
344
329
  foe_player: Player,
345
330
  foe_choice: Choice,
346
331
  foe_rewrap: bool,
347
332
  foe_skip: bool,
348
- foe_from: ?Move,
349
333
  options: anytype,
350
334
  ) !?Result {
351
335
  assert(player_choice.type != .Pass);
@@ -358,7 +342,6 @@ fn doTurn(
358
342
  player_choice,
359
343
  player_rewrap,
360
344
  player_skip,
361
- player_from,
362
345
  &residual,
363
346
  options,
364
347
  )) |r| return r;
@@ -381,7 +364,6 @@ fn doTurn(
381
364
  foe_choice,
382
365
  foe_rewrap,
383
366
  foe_skip,
384
- foe_from,
385
367
  &residual,
386
368
  options,
387
369
  )) |r| return r;
@@ -409,7 +391,6 @@ fn executeMove(
409
391
  choice: Choice,
410
392
  rewrap: bool,
411
393
  skip: bool,
412
- from: ?Move,
413
394
  residual: *bool,
414
395
  options: anytype,
415
396
  ) !?Result {
@@ -426,7 +407,7 @@ fn executeMove(
426
407
  if (side.last_selected_move == .SKIP_TURN) {
427
408
  assert(!showdown);
428
409
  if (battle.foe(player).active.volatiles.Binding) {
429
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
430
411
  }
431
412
  return null;
432
413
  }
@@ -444,8 +425,10 @@ fn executeMove(
444
425
  mslot = @intCast(battle.lastMove(player).index);
445
426
  const stored = side.stored();
446
427
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
447
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
448
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
428
+ auto = isForced(&side.active) or
429
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
430
+ side.last_selected_move == .HyperBeam or
431
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
449
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
450
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
451
434
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -467,7 +450,7 @@ fn executeMove(
467
450
 
468
451
  var skip_can = false;
469
452
  var skip_pp = false;
470
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
471
454
  .done => return null,
472
455
  .skip_can => skip_can = true,
473
456
  .skip_pp => skip_pp = true,
@@ -475,9 +458,9 @@ fn executeMove(
475
458
  .err => return @as(?Result, Result.Error),
476
459
  }
477
460
 
478
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
479
- return null;
480
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
481
464
 
482
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
483
466
  }
@@ -488,7 +471,6 @@ fn beforeMove(
488
471
  battle: anytype,
489
472
  player: Player,
490
473
  skip: bool,
491
- from: ?Move,
492
474
  residual: *bool,
493
475
  options: anytype,
494
476
  ) !BeforeMove {
@@ -515,23 +497,23 @@ fn beforeMove(
515
497
  }
516
498
 
517
499
  if (duration == 0) {
518
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
519
501
  stored.status = 0; // clears EXT if present
520
502
  } else {
521
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
522
504
  }
523
505
  side.last_used_move = .None;
524
506
  return .done;
525
507
  }
526
508
 
527
509
  if (Status.is(stored.status, .FRZ)) {
528
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
529
511
  side.last_used_move = .None;
530
512
  return .done;
531
513
  }
532
514
 
533
515
  if (skip or foe.active.volatiles.Binding) {
534
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
535
517
  return .done;
536
518
  }
537
519
 
@@ -539,27 +521,27 @@ fn beforeMove(
539
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
540
522
  // Pokémon can sometimes flinch multiple times from the same original hit
541
523
  if (!showdown) volatiles.Flinch = false;
542
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
543
525
  return .done;
544
526
  }
545
527
 
546
528
  if (volatiles.Recharging) {
547
529
  volatiles.Recharging = false;
548
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
549
531
  return .done;
550
532
  }
551
533
 
552
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
553
535
  if (options.calc.modify(player, .disable)) |extend| {
554
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
555
537
  } else {
556
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
557
539
  }
558
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
559
541
 
560
- if (volatiles.disabled_duration == 0) {
561
- volatiles.disabled_move = 0;
562
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
563
545
  }
564
546
  }
565
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -584,9 +566,9 @@ fn beforeMove(
584
566
 
585
567
  if (volatiles.confusion == 0) {
586
568
  volatiles.Confusion = false;
587
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
588
570
  } else {
589
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
590
572
 
591
573
  if (try Rolls.confused(battle, player, options)) {
592
574
  assert(!volatiles.MultiHit);
@@ -637,7 +619,7 @@ fn beforeMove(
637
619
  volatiles.Binding = false;
638
620
  options.chance.clearDurations(player, null);
639
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
640
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
641
623
  return .done;
642
624
  }
643
625
 
@@ -663,18 +645,18 @@ fn beforeMove(
663
645
  try options.chance.attacking(player, volatiles.attacks);
664
646
 
665
647
  if (volatiles.attacks != 0) {
666
- try log.activate(ident, .Bide);
648
+ try log.activate(.{ ident, .Bide });
667
649
  return .done;
668
650
  }
669
651
 
670
652
  volatiles.Bide = false;
671
- try log.end(ident, .Bide);
653
+ try log.end(.{ ident, .Bide });
672
654
 
673
655
  battle.last_damage = volatiles.state *% 2;
674
656
  volatiles.state = 0;
675
657
 
676
658
  if (battle.last_damage == 0) {
677
- try log.fail(ident, .None);
659
+ try log.fail(.{ ident, .None });
678
660
  return .done;
679
661
  }
680
662
 
@@ -695,7 +677,6 @@ fn beforeMove(
695
677
 
696
678
  if (volatiles.Thrashing) {
697
679
  assert(volatiles.attacks > 0);
698
- assert(from != null);
699
680
 
700
681
  if (options.calc.modify(player, .attacking)) |extend| {
701
682
  if (!extend) volatiles.attacks = 0;
@@ -705,15 +686,15 @@ fn beforeMove(
705
686
  try options.chance.attacking(player, volatiles.attacks);
706
687
 
707
688
  if (!showdown and handleThrashing(battle, active, player, options)) {
708
- try log.start(battle.active(player), .ConfusionSilent);
689
+ try log.start(.{ battle.active(player), .ConfusionSilent });
709
690
  }
710
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
691
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
711
692
  if (showdown) {
712
693
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
713
694
  // used in useMove and doesn't have the notion of skipping canMove semantics
714
695
  side.last_used_move = side.last_selected_move;
715
696
  if (handleThrashing(battle, active, player, options)) {
716
- try log.start(battle.active(player), .ConfusionSilent);
697
+ try log.start(.{ battle.active(player), .ConfusionSilent });
717
698
  }
718
699
  }
719
700
  return .skip_can;
@@ -729,12 +710,7 @@ fn beforeMove(
729
710
  }
730
711
  try options.chance.binding(player, volatiles.attacks);
731
712
 
732
- try log.move(
733
- battle.active(player),
734
- side.last_selected_move,
735
- battle.active(player.foe()),
736
- side.last_selected_move,
737
- );
713
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
738
714
  if (showdown or battle.last_damage != 0) {
739
715
  const sub = showdown and foe.active.volatiles.Substitute;
740
716
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -754,7 +730,7 @@ fn canMove(
754
730
  mslot: u4,
755
731
  auto: bool,
756
732
  skip_pp: bool,
757
- from: ?Move,
733
+ from: Move,
758
734
  residual: *bool,
759
735
  options: anytype,
760
736
  ) !bool {
@@ -762,33 +738,22 @@ fn canMove(
762
738
  const player_ident = battle.active(player);
763
739
  const move = Move.get(side.last_selected_move);
764
740
 
765
- const foe_last = battle.foe(player).last_used_move;
766
- const special = from != null and (from.? == .Metronome and
767
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
768
-
769
741
  var skip = skip_pp;
770
742
  if (side.active.volatiles.Charging) {
771
743
  side.active.volatiles.Charging = false;
772
744
  side.active.volatiles.Invulnerable = false;
773
745
  } else if (move.effect == .Charge) {
774
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
746
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
775
747
  setCounterable(battle, player, side, move);
776
748
  try Effects.charge(battle, player, options);
777
749
  return false;
778
750
  }
779
751
 
780
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
752
+ side.last_used_move = side.last_selected_move;
781
753
  if (!skip) decrementPP(side, mslot, auto);
782
754
 
783
755
  const target = if (move.target == .Self) player else player.foe();
784
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
785
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
786
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
787
- // meaning sometimes we lose this information. from is still set because it not being null is
788
- // relevant for control flow, but we don't have anything useful to put in the log. This is
789
- // regrettable, but this information is more "nice to have" than required
790
- const f = if (from != null and from.? == .None) null else from;
791
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
756
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
792
757
  setCounterable(battle, player, side, move);
793
758
 
794
759
  if (move.effect.onBegin()) {
@@ -826,12 +791,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
826
791
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
827
792
  if (volatiles.Bide) return;
828
793
 
829
- assert(active.move(mslot).pp > 0 or auto);
830
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
794
+ var move_slot = active.move(mslot);
795
+ assert(move_slot.pp > 0 or auto);
796
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
831
797
  if (volatiles.Transform) return;
832
798
 
833
- assert(side.stored().move(mslot).pp > 0 or auto);
834
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
799
+ move_slot = side.stored().move(mslot);
800
+ assert(move_slot.pp > 0 or auto);
801
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
835
802
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
836
803
  }
837
804
 
@@ -963,16 +930,16 @@ fn doMove(
963
930
  ohko = (!showdown or (!immune and !invulnerable)) and
964
931
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
965
932
  if (ohko) {
966
- try log.immune(foe_ident, .OHKO);
933
+ try log.immune(.{ foe_ident, .OHKO });
967
934
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
968
- try log.immune(foe_ident, .None);
935
+ try log.immune(.{ foe_ident, .None });
969
936
  } else if (mist) {
970
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
971
- 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 });
972
939
  } else {
973
940
  if (showdown or !zero) try options.chance.commit(player, .miss);
974
- try log.lastmiss();
975
- try log.miss(battle.active(player));
941
+ try log.lastmiss(.{});
942
+ try log.miss(.{battle.active(player)});
976
943
  }
977
944
  if (move.effect == .JumpKick) {
978
945
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1008,15 +975,15 @@ fn doMove(
1008
975
  var hit: u4 = 0;
1009
976
  while (hit < hits) {
1010
977
  if (hit == 0) {
1011
- if (crit) try log.crit(battle.active(player.foe()));
978
+ if (crit) try log.crit(.{battle.active(player.foe())});
1012
979
  if (effectiveness > Effectiveness.neutral) {
1013
- try log.supereffective(battle.active(player.foe()));
980
+ try log.supereffective(.{battle.active(player.foe())});
1014
981
  } else if (effectiveness < Effectiveness.neutral) {
1015
- try log.resisted(battle.active(player.foe()));
982
+ try log.resisted(.{battle.active(player.foe())});
1016
983
  }
1017
984
  }
1018
985
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1019
- if (hit == 0 and ohko) try log.ohko();
986
+ if (hit == 0 and ohko) try log.ohko(.{});
1020
987
  hit += 1;
1021
988
  if (foe.stored().hp == 0) break;
1022
989
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1033,7 +1000,7 @@ fn doMove(
1033
1000
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1034
1001
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1035
1002
  }
1036
- try log.hitcount(battle.active(player.foe()), hit);
1003
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1037
1004
  } else if (showdown) {
1038
1005
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1039
1006
  if (move.effect == .Binding) {
@@ -1043,9 +1010,9 @@ fn doMove(
1043
1010
  assert(foe.stored().hp > 0);
1044
1011
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1045
1012
  if (sub) {
1046
- try log.activate(battle.active(player.foe()), .Substitute);
1013
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1047
1014
  } else {
1048
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1015
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1049
1016
  try buildRage(battle, player.foe(), options);
1050
1017
  }
1051
1018
  return null;
@@ -1243,7 +1210,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1243
1210
  const foe = battle.foe(player);
1244
1211
 
1245
1212
  if (battle.last_damage == 0) {
1246
- try options.log.fail(battle.active(player), .None);
1213
+ try options.log.fail(.{ battle.active(player), .None });
1247
1214
  return null;
1248
1215
  }
1249
1216
 
@@ -1261,14 +1228,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1261
1228
  foe_last_selected_move.type == .Fighting);
1262
1229
 
1263
1230
  if (!used and !selected) {
1264
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1265
1232
  return null;
1266
1233
  }
1267
1234
 
1268
1235
  if (!used or !selected) {
1269
1236
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1270
1237
  if (!showdown) return Result.Error;
1271
- try options.log.fail(battle.active(player), .None);
1238
+ try options.log.fail(.{ battle.active(player), .None });
1272
1239
  return null;
1273
1240
  }
1274
1241
 
@@ -1304,20 +1271,20 @@ fn applyDamage(
1304
1271
  subbed.active.volatiles.substitute = 0;
1305
1272
  subbed.active.volatiles.Substitute = false;
1306
1273
  // battle.last_damage is not updated with the amount of HP the Substitute had
1307
- try options.log.end(battle.active(sub_player), .Substitute);
1274
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1308
1275
  options.calc.capped(target_player);
1309
1276
  return true;
1310
1277
  } else {
1311
1278
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1312
1279
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1313
- try options.log.activate(battle.active(sub_player), .Substitute);
1280
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1314
1281
  return false;
1315
1282
  }
1316
1283
  }
1317
1284
 
1318
1285
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1319
1286
  target.stored().hp -= battle.last_damage;
1320
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1287
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1321
1288
  return false;
1322
1289
  }
1323
1290
 
@@ -1336,7 +1303,7 @@ fn mirrorMove(
1336
1303
  side.last_selected_move = foe.last_used_move;
1337
1304
 
1338
1305
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1339
- try options.log.fail(battle.active(player), .None);
1306
+ try options.log.fail(.{ battle.active(player), .None });
1340
1307
  return null;
1341
1308
  } else if (!showdown or foe.last_used_move != .Struggle) {
1342
1309
  incrementPP(side, mslot);
@@ -1378,12 +1345,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1378
1345
  if (mist) {
1379
1346
  assert(!showdown);
1380
1347
  const foe_ident = battle.active(player.foe());
1381
- try options.log.activate(foe_ident, .Mist);
1382
- try options.log.fail(foe_ident, .None);
1348
+ try options.log.activate(.{ foe_ident, .Mist });
1349
+ try options.log.fail(.{ foe_ident, .None });
1383
1350
  } else {
1384
1351
  try options.chance.commit(player, .miss);
1385
- try options.log.lastmiss();
1386
- try options.log.miss(battle.active(player));
1352
+ try options.log.lastmiss(.{});
1353
+ try options.log.miss(.{battle.active(player)});
1387
1354
  }
1388
1355
 
1389
1356
  return false;
@@ -1474,9 +1441,8 @@ fn checkFaint(
1474
1441
  const foe_fainted = foe.stored().hp == 0;
1475
1442
 
1476
1443
  const player_out = findFirstAlive(side) == 0;
1477
- const foe_out = findFirstAlive(foe) == 0;
1478
- const tie = player_out and foe_out;
1479
- const more = tie or player_out or foe_out;
1444
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1445
+ const more = player_out or foe_out;
1480
1446
 
1481
1447
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1482
1448
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1484,20 +1450,20 @@ fn checkFaint(
1484
1450
  assert(!side.active.volatiles.MultiHit);
1485
1451
  assert(!foe.active.volatiles.MultiHit);
1486
1452
 
1487
- if (tie) {
1488
- try options.log.tie();
1453
+ if (player_out and foe_out) {
1454
+ try options.log.tie(.{});
1489
1455
  return Result.Tie;
1490
1456
  } else if (player_out) {
1491
- try options.log.win(player.foe());
1457
+ try options.log.win(.{player.foe()});
1492
1458
  return if (player == .P1) Result.Lose else Result.Win;
1493
1459
  } else if (foe_out) {
1494
- try options.log.win(player);
1460
+ try options.log.win(.{player});
1495
1461
  return if (player == .P1) Result.Win else Result.Lose;
1496
1462
  }
1497
1463
 
1498
1464
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1499
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1500
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1465
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1466
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1501
1467
  }
1502
1468
 
1503
1469
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1533,7 +1499,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1533
1499
  side.stored().stats.spe;
1534
1500
  }
1535
1501
 
1536
- try options.log.faint(battle.active(player), done);
1502
+ try options.log.faint(.{ battle.active(player), done });
1537
1503
  options.calc.capped(player);
1538
1504
  return null;
1539
1505
  }
@@ -1555,8 +1521,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1555
1521
 
1556
1522
  if (!showdown or damage > 0) {
1557
1523
  stored.hp -= damage;
1558
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1559
- 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 });
1560
1526
  }
1561
1527
  }
1562
1528
 
@@ -1581,14 +1547,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1581
1547
  const amount = @min(damage, stored.hp);
1582
1548
  stored.hp -= amount;
1583
1549
 
1584
- // As above, Pokémon Showdown uses damageOf but its not relevant
1585
- 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 });
1586
1552
 
1587
1553
  const before = foe_stored.hp;
1588
1554
  // Uncapped damage is added back to the foe
1589
1555
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1590
1556
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1591
- 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 });
1592
1558
  }
1593
1559
  }
1594
1560
 
@@ -1599,18 +1565,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1599
1565
  battle.turn += 1;
1600
1566
 
1601
1567
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1602
- try options.log.tie();
1568
+ try options.log.tie(.{});
1603
1569
  return Result.Tie;
1604
1570
  }
1605
1571
 
1606
1572
  if (showdown and battle.turn >= 1000) {
1607
- try options.log.tie();
1573
+ try options.log.tie(.{});
1608
1574
  return Result.Tie;
1609
1575
  } else if (battle.turn >= 65535) {
1610
1576
  return Result.Error;
1611
1577
  }
1612
1578
 
1613
- try options.log.turn(battle.turn);
1579
+ try options.log.turn(.{battle.turn});
1614
1580
 
1615
1581
  return Result.Default;
1616
1582
  }
@@ -1670,9 +1636,9 @@ fn checkEBC(battle: anytype) bool {
1670
1636
  }
1671
1637
 
1672
1638
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1673
- if (side.active.volatiles.disabled_move != 0) {
1639
+ if (side.active.volatiles.disable_move != 0) {
1674
1640
  // A Pokémon that transforms after being disabled may end up with less move slots
1675
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1641
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1676
1642
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1677
1643
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1678
1644
  m
@@ -1680,14 +1646,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1680
1646
  side.last_selected_move;
1681
1647
  if (m != .None and m == last) {
1682
1648
  side.active.volatiles.Charging = false;
1683
- try options.log.disabled(ident, last);
1649
+ try options.log.cant(.{ ident, .Disable, last });
1684
1650
  return true;
1685
1651
  }
1686
1652
  }
1687
1653
  return false;
1688
1654
  }
1689
1655
 
1690
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1656
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1691
1657
  const side = battle.side(who);
1692
1658
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1693
1659
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1705,7 +1671,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1705
1671
  return true;
1706
1672
  }
1707
1673
 
1708
- inline fn onBegin(
1674
+ fn onBegin(
1709
1675
  battle: anytype,
1710
1676
  player: Player,
1711
1677
  move: Move.Data,
@@ -1735,7 +1701,7 @@ inline fn onBegin(
1735
1701
  };
1736
1702
  }
1737
1703
 
1738
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1739
1705
  assert(move.effect.onEnd());
1740
1706
  return switch (move.effect) {
1741
1707
  .Bide => Effects.bide(battle, player, options),
@@ -1751,7 +1717,7 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1751
1717
  };
1752
1718
  }
1753
1719
 
1754
- inline fn alwaysHappens(
1720
+ fn alwaysHappens(
1755
1721
  battle: anytype,
1756
1722
  player: Player,
1757
1723
  move: Move.Data,
@@ -1770,7 +1736,7 @@ inline fn alwaysHappens(
1770
1736
  };
1771
1737
  }
1772
1738
 
1773
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1739
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1774
1740
  return switch (move.effect) {
1775
1741
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1776
1742
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1797,7 +1763,7 @@ pub const Effects = struct {
1797
1763
  side.active.volatiles.state = 0;
1798
1764
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1799
1765
 
1800
- try options.log.start(battle.active(player), .Bide);
1766
+ try options.log.start(.{ battle.active(player), .Bide });
1801
1767
  }
1802
1768
 
1803
1769
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1811,11 +1777,11 @@ pub const Effects = struct {
1811
1777
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1812
1778
  if (Status.is(foe_stored.status, .FRZ)) {
1813
1779
  assert(move.type == .Fire);
1814
- try options.log.curestatus(
1780
+ try options.log.curestatus(.{
1815
1781
  battle.active(player.foe()),
1816
1782
  foe_stored.status,
1817
1783
  .Message,
1818
- );
1784
+ });
1819
1785
  foe_stored.status = 0;
1820
1786
  }
1821
1787
  return;
@@ -1829,7 +1795,7 @@ pub const Effects = struct {
1829
1795
  foe_stored.status = Status.init(.BRN);
1830
1796
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1831
1797
 
1832
- 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 });
1833
1799
  }
1834
1800
 
1835
1801
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1839,8 +1805,8 @@ pub const Effects = struct {
1839
1805
  volatiles.Charging = true;
1840
1806
  const move = side.last_selected_move;
1841
1807
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1842
- try options.log.laststill();
1843
- try options.log.prepare(battle.active(player), move);
1808
+ try options.log.laststill(.{});
1809
+ try options.log.prepare(.{ battle.active(player), move });
1844
1810
  }
1845
1811
 
1846
1812
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1851,17 +1817,11 @@ pub const Effects = struct {
1851
1817
  if (!try Rolls.confusionChance(battle, player, options)) return;
1852
1818
  } else {
1853
1819
  if (showdown) {
1854
- if (!try checkHit(battle, player, move, options)) {
1855
- return;
1856
- } else if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1858
- }
1820
+ if (!try checkHit(battle, player, move, options)) return;
1821
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1859
1822
  } else {
1860
- if (sub) {
1861
- return options.log.fail(battle.active(player.foe()), .None);
1862
- } else if (!try checkHit(battle, player, move, options)) {
1863
- return;
1864
- }
1823
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
+ if (!try checkHit(battle, player, move, options)) return;
1865
1825
  }
1866
1826
  try options.chance.commit(player, .hit);
1867
1827
  }
@@ -1870,23 +1830,24 @@ pub const Effects = struct {
1870
1830
  foe.active.volatiles.Confusion = true;
1871
1831
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1872
1832
 
1873
- try options.log.start(battle.active(player.foe()), .Confusion);
1833
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1874
1834
  }
1875
1835
 
1876
1836
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1877
1837
  const foe = battle.foe(player);
1878
1838
 
1879
1839
  if (foe.active.volatiles.Invulnerable) {
1880
- try options.log.lastmiss();
1881
- return options.log.miss(battle.active(player));
1840
+ try options.log.lastmiss(.{});
1841
+ return options.log.miss(.{battle.active(player)});
1882
1842
  }
1883
1843
 
1884
1844
  battle.side(player).active.types = foe.active.types;
1885
- return options.log.typechange(
1845
+ return options.log.start(.{
1886
1846
  battle.active(player),
1847
+ .TypeChange,
1887
1848
  foe.active.types,
1888
1849
  battle.active(player.foe()),
1889
- );
1850
+ });
1890
1851
  }
1891
1852
 
1892
1853
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,8 +1861,8 @@ pub const Effects = struct {
1900
1861
  try options.chance.commit(player, .hit);
1901
1862
  }
1902
1863
 
1903
- if (volatiles.disabled_move != 0) {
1904
- try options.log.fail(foe_ident, .None);
1864
+ if (volatiles.disable_move != 0) {
1865
+ try options.log.fail(.{ foe_ident, .None });
1905
1866
  return null;
1906
1867
  }
1907
1868
 
@@ -1917,7 +1878,7 @@ pub const Effects = struct {
1917
1878
  // Technically this is still considered simply a "miss" on the cartridge,
1918
1879
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1919
1880
  if (n == 0) {
1920
- try options.log.fail(foe_ident, .None);
1881
+ try options.log.fail(.{ foe_ident, .None });
1921
1882
  return null;
1922
1883
  } else if (err) {
1923
1884
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1925,18 +1886,18 @@ pub const Effects = struct {
1925
1886
  return Result.Error;
1926
1887
  }
1927
1888
 
1928
- volatiles.disabled_move =
1889
+ volatiles.disable_move =
1929
1890
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1930
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1891
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1931
1892
 
1932
- const id = foe.active.move(volatiles.disabled_move).id;
1933
- try options.log.startEffect(foe_ident, .Disable, id);
1893
+ const id = foe.active.move(volatiles.disable_move).id;
1894
+ try options.log.start(.{ foe_ident, .Disable, id });
1934
1895
  return null;
1935
1896
  }
1936
1897
 
1937
1898
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1938
- var side = battle.side(player);
1939
- var stored = side.stored();
1899
+ var stored = battle.side(player).stored();
1900
+ const ident = battle.active(player);
1940
1901
 
1941
1902
  if (battle.last_damage == 0) {
1942
1903
  assert(showdown);
@@ -1949,7 +1910,7 @@ pub const Effects = struct {
1949
1910
  if (stored.hp == stored.stats.hp) return;
1950
1911
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1951
1912
 
1952
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1913
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1953
1914
  }
1954
1915
 
1955
1916
  fn explode(battle: anytype, player: Player) !void {
@@ -1981,7 +1942,7 @@ pub const Effects = struct {
1981
1942
  if (side.active.volatiles.FocusEnergy) return;
1982
1943
  side.active.volatiles.FocusEnergy = true;
1983
1944
 
1984
- try options.log.start(battle.active(player), .FocusEnergy);
1945
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1985
1946
  }
1986
1947
 
1987
1948
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2003,7 +1964,7 @@ pub const Effects = struct {
2003
1964
  foe_stored.status = Status.init(.FRZ);
2004
1965
  // GLITCH: Hyper Beam recharging status is not cleared
2005
1966
 
2006
- try options.log.status(foe_ident, foe_stored.status, .None);
1967
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2007
1968
  }
2008
1969
 
2009
1970
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2023,8 +1984,8 @@ pub const Effects = struct {
2023
1984
  side.active.stats = side_stored.stats;
2024
1985
  foe.active.stats = foe_stored.stats;
2025
1986
 
2026
- try log.activate(player_ident, .Haze);
2027
- try log.clearallboost();
1987
+ try log.activate(.{ player_ident, .Haze });
1988
+ try log.clearallboost(.{});
2028
1989
 
2029
1990
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2030
1991
  if (showdown) {
@@ -2032,11 +1993,11 @@ pub const Effects = struct {
2032
1993
  const p: Player = @enumFromInt(i);
2033
1994
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2034
1995
  if (p != player and Status.any(s.stored().status)) {
2035
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1996
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2036
1997
  s.stored().status = 0;
2037
1998
  } else if (showdown and s.stored().status == Status.TOX) {
2038
1999
  s.stored().status = Status.init(.PSN);
2039
- try log.status(battle.active(p), s.stored().status, .None);
2000
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2040
2001
  }
2041
2002
  try clearVolatiles(battle, p, p != player, options);
2042
2003
  }
@@ -2045,7 +2006,7 @@ pub const Effects = struct {
2045
2006
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2046
2007
  foe.last_selected_move = .SKIP_TURN;
2047
2008
  }
2048
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2009
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2049
2010
  foe_stored.status = 0;
2050
2011
  }
2051
2012
  try clearVolatiles(battle, player, false, options);
@@ -2061,7 +2022,7 @@ pub const Effects = struct {
2061
2022
  // GLITCH: HP recovery move failure glitches
2062
2023
  const delta = stored.stats.hp - stored.hp;
2063
2024
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2064
- return try options.log.fail(ident, .None);
2025
+ return options.log.fail(.{ ident, .None });
2065
2026
  }
2066
2027
 
2067
2028
  const rest = side.last_selected_move == .Rest;
@@ -2069,17 +2030,17 @@ pub const Effects = struct {
2069
2030
  // Adding the sleep status runs the sleep condition handler to roll duration
2070
2031
  if (showdown) battle.rng.advance(1);
2071
2032
  stored.status = Status.slf(2);
2072
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2033
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2073
2034
  stored.hp = stored.stats.hp;
2074
2035
  } else {
2075
2036
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2076
2037
  }
2077
- 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 });
2078
2039
  }
2079
2040
 
2080
2041
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2081
2042
  battle.side(player).active.volatiles.Recharging = true;
2082
- try options.log.mustrecharge(battle.active(player));
2043
+ try options.log.mustrecharge(.{battle.active(player)});
2083
2044
  }
2084
2045
 
2085
2046
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2088,33 +2049,32 @@ pub const Effects = struct {
2088
2049
  if (showdown) {
2089
2050
  // Invulnerability trumps type immunity on Pokémon Showdown
2090
2051
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2091
- return options.log.immune(battle.active(player.foe()), .None);
2052
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2092
2053
  }
2093
2054
  if (!try checkHit(battle, player, move, options)) return;
2094
2055
  if (foe.active.volatiles.LeechSeed) return;
2095
2056
  } else {
2096
2057
  if (!try checkHit(battle, player, move, options)) return;
2097
2058
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2098
- try options.log.lastmiss();
2099
- return options.log.miss(battle.active(player));
2059
+ try options.log.lastmiss(.{});
2060
+ return options.log.miss(.{battle.active(player)});
2100
2061
  }
2101
2062
  }
2102
2063
 
2103
2064
  try options.chance.commit(player, .hit);
2104
2065
  foe.active.volatiles.LeechSeed = true;
2105
2066
 
2106
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2067
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2107
2068
  }
2108
2069
 
2109
2070
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2110
2071
  var side = battle.side(player);
2072
+ const ident = battle.active(player);
2111
2073
 
2112
- if (side.active.volatiles.LightScreen) {
2113
- return options.log.fail(battle.active(player), .None);
2114
- }
2074
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2115
2075
  side.active.volatiles.LightScreen = true;
2116
2076
 
2117
- try options.log.start(battle.active(player), .LightScreen);
2077
+ try options.log.start(.{ ident, .LightScreen });
2118
2078
  }
2119
2079
 
2120
2080
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2143,7 +2103,7 @@ pub const Effects = struct {
2143
2103
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2144
2104
  if (!try checkHit(battle, player, move, options)) return;
2145
2105
  try options.chance.commit(player, .hit);
2146
- return try options.log.fail(battle.active(player.foe()), .None);
2106
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2147
2107
  }
2148
2108
  }
2149
2109
  if (!try checkHit(battle, player, move, options)) return;
@@ -2152,7 +2112,7 @@ pub const Effects = struct {
2152
2112
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2153
2113
  side.active.move(oslot).id = foe.active.move(rslot).id;
2154
2114
 
2155
- 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 });
2156
2116
  }
2157
2117
 
2158
2118
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2161,7 +2121,7 @@ pub const Effects = struct {
2161
2121
  if (side.active.volatiles.Mist) return;
2162
2122
  side.active.volatiles.Mist = true;
2163
2123
 
2164
- try options.log.start(battle.active(player), .Mist);
2124
+ try options.log.start(.{ battle.active(player), .Mist });
2165
2125
  }
2166
2126
 
2167
2127
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2187,17 +2147,18 @@ pub const Effects = struct {
2187
2147
 
2188
2148
  if (showdown) {
2189
2149
  // Invulnerability trumps type immunity on Pokémon Showdown
2190
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2191
- 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;
2192
2153
  }
2193
2154
  if (Status.any(foe_stored.status)) {
2194
- return log.fail(
2155
+ return log.fail(.{
2195
2156
  foe_ident,
2196
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2197
- );
2157
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2158
+ });
2198
2159
  }
2199
2160
  if (!showdown) {
2200
- if (immune) return log.immune(foe_ident, .None);
2161
+ if (immune) return log.immune(.{ foe_ident, .None });
2201
2162
  if (!try checkHit(battle, player, move, options)) return;
2202
2163
  }
2203
2164
  try options.chance.commit(player, .hit);
@@ -2205,7 +2166,7 @@ pub const Effects = struct {
2205
2166
  foe_stored.status = Status.init(.PAR);
2206
2167
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2207
2168
 
2208
- try log.status(foe_ident, foe_stored.status, .None);
2169
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2209
2170
  }
2210
2171
 
2211
2172
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2227,12 +2188,12 @@ pub const Effects = struct {
2227
2188
  foe_stored.status = Status.init(.PAR);
2228
2189
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2229
2190
 
2230
- 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 });
2231
2192
  }
2232
2193
 
2233
2194
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2234
2195
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2235
- try options.log.fieldactivate();
2196
+ try options.log.fieldactivate(.{});
2236
2197
  }
2237
2198
  }
2238
2199
 
@@ -2247,23 +2208,23 @@ pub const Effects = struct {
2247
2208
  return;
2248
2209
  } else if (foe.active.volatiles.Substitute) {
2249
2210
  if (move.effect != .Poison) return;
2250
- return log.fail(foe_ident, .None);
2211
+ return log.fail(.{ foe_ident, .None });
2251
2212
  } else if (Status.any(foe_stored.status)) {
2252
2213
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2253
2214
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2254
2215
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2255
- 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))
2256
2217
  if (!showdown)
2257
- .Poison
2218
+ Fail.Poison
2258
2219
  else if (toxic == (foe_stored.status == Status.TOX))
2259
- if (toxic) .Toxic else .Poison
2220
+ if (toxic) Fail.Toxic else Fail.Poison
2260
2221
  else
2261
- .None
2222
+ Fail.None
2262
2223
  else
2263
- .None);
2224
+ Fail.None });
2264
2225
  } else if (foe.active.types.includes(.Poison)) {
2265
2226
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2266
- return log.immune(foe_ident, .None);
2227
+ return log.immune(.{ foe_ident, .None });
2267
2228
  }
2268
2229
 
2269
2230
  if (move.effect == .Poison) {
@@ -2282,7 +2243,7 @@ pub const Effects = struct {
2282
2243
  foe.active.volatiles.toxic = 0;
2283
2244
  }
2284
2245
 
2285
- try log.status(foe_ident, foe_stored.status, .None);
2246
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2286
2247
  }
2287
2248
 
2288
2249
  fn rage(battle: anytype, player: Player) !void {
@@ -2302,22 +2263,23 @@ pub const Effects = struct {
2302
2263
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2303
2264
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2304
2265
 
2305
- try options.log.damageOf(
2266
+ try options.log.damage(.{
2306
2267
  battle.active(player),
2307
2268
  stored,
2308
2269
  .RecoilOf,
2309
2270
  battle.active(player.foe()),
2310
- );
2271
+ });
2311
2272
  if (showdown and stored.hp == 0) residual.* = false;
2312
2273
  }
2313
2274
 
2314
2275
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2315
2276
  var side = battle.side(player);
2277
+ const ident = battle.active(player);
2316
2278
 
2317
- 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 });
2318
2280
  side.active.volatiles.Reflect = true;
2319
2281
 
2320
- try options.log.start(battle.active(player), .Reflect);
2282
+ try options.log.start(.{ ident, .Reflect });
2321
2283
  }
2322
2284
 
2323
2285
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2331,10 +2293,10 @@ pub const Effects = struct {
2331
2293
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2332
2294
  } else {
2333
2295
  if (Status.any(foe_stored.status)) {
2334
- return options.log.fail(
2296
+ return options.log.fail(.{
2335
2297
  foe_ident,
2336
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2337
- );
2298
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
+ });
2338
2300
  }
2339
2301
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2340
2302
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2351,18 +2313,17 @@ pub const Effects = struct {
2351
2313
 
2352
2314
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2353
2315
  const last = battle.side(player).last_selected_move;
2354
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2316
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2355
2317
  }
2356
2318
 
2357
2319
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2358
- try options.log.activate(battle.active(player), .Splash);
2320
+ try options.log.activate(.{ battle.active(player), .Splash });
2359
2321
  }
2360
2322
 
2361
2323
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2362
2324
  var side = battle.side(player);
2363
2325
  if (side.active.volatiles.Substitute) {
2364
- try options.log.fail(battle.active(player), .Substitute);
2365
- return;
2326
+ return options.log.fail(.{ battle.active(player), .Substitute });
2366
2327
  }
2367
2328
 
2368
2329
  assert(side.stored().stats.hp <= 1023);
@@ -2374,17 +2335,16 @@ pub const Effects = struct {
2374
2335
  // avoid having to convert to floating point)
2375
2336
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2376
2337
  if (side.stored().hp < required_hp) {
2377
- try options.log.fail(battle.active(player), .Weak);
2378
- return;
2338
+ return options.log.fail(.{ battle.active(player), .Weak });
2379
2339
  }
2380
2340
 
2381
2341
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2382
2342
  side.stored().hp -= hp;
2383
2343
  side.active.volatiles.substitute = hp + 1;
2384
2344
  side.active.volatiles.Substitute = true;
2385
- try options.log.start(battle.active(player), .Substitute);
2345
+ try options.log.start(.{ battle.active(player), .Substitute });
2386
2346
  if (hp > 0) {
2387
- try options.log.damage(battle.active(player), side.stored(), .None);
2347
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2388
2348
  if (showdown and side.stored().hp == 0) residual.* = false;
2389
2349
  }
2390
2350
  }
@@ -2398,8 +2358,8 @@ pub const Effects = struct {
2398
2358
  }
2399
2359
  battle.last_damage = 0;
2400
2360
  } else {
2401
- try options.log.fail(battle.active(player), .None);
2402
- try options.log.laststill();
2361
+ try options.log.fail(.{ battle.active(player), .None });
2362
+ try options.log.laststill(.{});
2403
2363
  }
2404
2364
  }
2405
2365
 
@@ -2438,7 +2398,7 @@ pub const Effects = struct {
2438
2398
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2399
  }
2440
2400
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2401
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2402
  }
2443
2403
 
2444
2404
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2468,87 +2428,87 @@ pub const Effects = struct {
2468
2428
  switch (move.effect) {
2469
2429
  .AttackUp1, .AttackUp2, .Rage => {
2470
2430
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2431
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2432
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2433
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2434
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2435
  if (stats.atk == MAX_STAT_VALUE) {
2476
2436
  boosts.atk -= 1;
2477
2437
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2438
+ try log.boost(.{ ident, reason, n });
2439
+ try log.boost(.{ ident, .Attack, -1 });
2480
2440
  if (move.effect == .Rage) return;
2481
2441
  }
2482
- return try log.fail(ident, .None);
2442
+ return log.fail(.{ ident, .None });
2483
2443
  }
2484
2444
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2445
  const stat = unmodifiedStats(battle, side).atk;
2486
2446
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2447
+ try log.boost(.{ ident, reason, n });
2488
2448
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2449
  if (showdown and move.effect == .Rage) return;
2490
2450
  },
2491
2451
  .DefenseUp1, .DefenseUp2 => {
2492
2452
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2453
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2454
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2455
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2456
  if (stats.def == MAX_STAT_VALUE) {
2497
2457
  boosts.def -= 1;
2498
2458
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2459
+ try log.boost(.{ ident, .Defense, n });
2460
+ try log.boost(.{ ident, .Defense, -1 });
2501
2461
  }
2502
- return try log.fail(ident, .None);
2462
+ return log.fail(.{ ident, .None });
2503
2463
  }
2504
2464
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2465
  const stat = unmodifiedStats(battle, side).def;
2506
2466
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2467
+ try log.boost(.{ ident, .Defense, n });
2508
2468
  },
2509
2469
  .SpeedUp2 => {
2510
2470
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2471
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2472
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2473
  if (stats.spe == MAX_STAT_VALUE) {
2514
2474
  boosts.spe -= 1;
2515
2475
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2476
+ try log.boost(.{ ident, .Speed, 2 });
2477
+ try log.boost(.{ ident, .Speed, -1 });
2518
2478
  }
2519
- return try log.fail(ident, .None);
2479
+ return log.fail(.{ ident, .None });
2520
2480
  }
2521
2481
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2482
  const stat = unmodifiedStats(battle, side).spe;
2523
2483
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2484
+ try log.boost(.{ ident, .Speed, 2 });
2525
2485
  },
2526
2486
  .SpecialUp1, .SpecialUp2 => {
2527
2487
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2488
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2489
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2490
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2491
  if (stats.spc == MAX_STAT_VALUE) {
2532
2492
  boosts.spc -= 1;
2533
2493
  if (showdown) {
2534
- try log.boost(ident, .SpecialAttack, n);
2535
- try log.boost(ident, .SpecialAttack, -1);
2536
- try log.boost(ident, .SpecialDefense, n);
2537
- 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 });
2538
2498
  }
2539
- return try log.fail(ident, .None);
2499
+ return log.fail(.{ ident, .None });
2540
2500
  }
2541
2501
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2502
  const stat = unmodifiedStats(battle, side).spc;
2543
2503
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2544
- try log.boost(ident, .SpecialAttack, n);
2545
- try log.boost(ident, .SpecialDefense, n);
2504
+ try log.boost(.{ ident, .SpecialAttack, n });
2505
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2506
  },
2547
2507
  .EvasionUp1 => {
2548
2508
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2509
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2510
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2511
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2512
  },
2553
2513
  else => unreachable,
2554
2514
  }
@@ -2563,7 +2523,7 @@ pub const Effects = struct {
2563
2523
  const foe_ident = battle.active(player.foe());
2564
2524
  const secondary = move.effect.isStatDownChance();
2565
2525
 
2566
- 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 });
2567
2527
 
2568
2528
  if (secondary) {
2569
2529
  const proc = try Rolls.unboost(battle, player, options);
@@ -2581,82 +2541,82 @@ pub const Effects = struct {
2581
2541
  switch (move.effect) {
2582
2542
  .AttackDown1, .AttackDownChance => {
2583
2543
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- 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 });
2585
2545
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2546
  if (stats.atk == 1) {
2587
2547
  boosts.atk += 1;
2588
2548
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2549
+ try log.boost(.{ foe_ident, .Attack, -1 });
2550
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2551
  }
2592
- return try log.fail(foe_ident, .None);
2552
+ return log.fail(.{ foe_ident, .None });
2593
2553
  }
2594
2554
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2555
  const stat = unmodifiedStats(battle, foe).atk;
2596
2556
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2557
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2558
  },
2599
2559
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2560
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- 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 });
2602
2562
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2563
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2564
  if (stats.def == 1) {
2605
2565
  boosts.def += 1;
2606
2566
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- 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 });
2609
2569
  }
2610
- return try log.fail(foe_ident, .None);
2570
+ return log.fail(.{ foe_ident, .None });
2611
2571
  }
2612
2572
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2573
  const stat = unmodifiedStats(battle, foe).def;
2614
2574
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2575
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2576
  },
2617
2577
  .SpeedDown1, .SpeedDownChance => {
2618
2578
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- 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 });
2620
2580
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2581
  if (stats.spe == 1) {
2622
2582
  boosts.spe += 1;
2623
2583
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2584
+ try log.boost(.{ foe_ident, .Speed, -1 });
2585
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2586
  }
2627
- return try log.fail(foe_ident, .None);
2587
+ return log.fail(.{ foe_ident, .None });
2628
2588
  }
2629
2589
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2590
  const stat = unmodifiedStats(battle, foe).spe;
2631
2591
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2592
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2593
  assert(boosts.spe >= -6);
2634
2594
  },
2635
2595
  .SpecialDownChance => {
2636
2596
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- 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 });
2638
2598
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2599
  if (stats.spc == 1) {
2640
2600
  boosts.spc += 1;
2641
2601
  if (showdown) {
2642
- try log.boost(foe_ident, .SpecialAttack, -1);
2643
- try log.boost(foe_ident, .SpecialAttack, 1);
2644
- try log.boost(foe_ident, .SpecialDefense, -1);
2645
- 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 });
2646
2606
  }
2647
- return try log.fail(foe_ident, .None);
2607
+ return log.fail(.{ foe_ident, .None });
2648
2608
  }
2649
2609
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2610
  const stat = unmodifiedStats(battle, foe).spc;
2651
2611
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2652
- try log.boost(foe_ident, .SpecialAttack, -1);
2653
- try log.boost(foe_ident, .SpecialDefense, -1);
2612
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2613
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2614
  },
2655
2615
  .AccuracyDown1 => {
2656
2616
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- 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 });
2658
2618
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2619
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2620
  },
2661
2621
  else => unreachable,
2662
2622
  }
@@ -2680,7 +2640,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2640
  }
2681
2641
  }
2682
2642
 
2683
- inline fn isForced(active: anytype) bool {
2643
+ fn isForced(active: anytype) bool {
2684
2644
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2645
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2646
  }
@@ -2693,27 +2653,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2693
2653
 
2694
2654
  options.chance.clearDurations(who, clear);
2695
2655
 
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2699
- try log.end(ident, .DisableSilent);
2656
+ if (volatiles.disable_move != 0) {
2657
+ volatiles.disable_move = 0;
2658
+ volatiles.disable_duration = 0;
2659
+ try log.end(.{ ident, .DisableSilent });
2700
2660
  }
2701
2661
  if (volatiles.Confusion) {
2702
2662
  // volatiles.confusion is left unchanged
2703
2663
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2664
+ try log.end(.{ ident, .ConfusionSilent });
2705
2665
  }
2706
2666
  if (volatiles.Mist) {
2707
2667
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2668
+ try log.end(.{ ident, .MistSilent });
2709
2669
  }
2710
2670
  if (volatiles.FocusEnergy) {
2711
2671
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2672
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2673
  }
2714
2674
  if (volatiles.LeechSeed) {
2715
2675
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2676
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2677
  }
2718
2678
  if (!showdown and volatiles.Toxic) {
2719
2679
  volatiles.Toxic = false;
@@ -2721,22 +2681,22 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2681
  }
2722
2682
  if (volatiles.LightScreen) {
2723
2683
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2684
+ try log.end(.{ ident, .LightScreenSilent });
2725
2685
  }
2726
2686
  if (volatiles.Reflect) {
2727
2687
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2688
+ try log.end(.{ ident, .ReflectSilent });
2729
2689
  }
2730
2690
  if (showdown and volatiles.Toxic) {
2731
2691
  volatiles.Toxic = false;
2732
2692
  // Pokémon Showdown erroneously clears the toxic counter
2733
2693
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2694
+ try log.end(.{ ident, .ToxicSilent });
2735
2695
  }
2736
2696
  }
2737
2697
 
2738
2698
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2699
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2700
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2701
  player == .P1
2742
2702
  else if (showdown)
@@ -2748,7 +2708,7 @@ pub const Rolls = struct {
2748
2708
  return p1;
2749
2709
  }
2750
2710
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2711
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2752
2712
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2713
  val == .true
2754
2714
  else if (showdown)
@@ -2760,7 +2720,7 @@ pub const Rolls = struct {
2760
2720
  return crit;
2761
2721
  }
2762
2722
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2723
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2724
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2725
  val
2766
2726
  else roll: {
@@ -2776,7 +2736,7 @@ pub const Rolls = struct {
2776
2736
  return roll;
2777
2737
  }
2778
2738
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2739
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2780
2740
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2741
  val == .true
2782
2742
  else if (showdown)
@@ -2788,7 +2748,7 @@ pub const Rolls = struct {
2788
2748
  return ok;
2789
2749
  }
2790
2750
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2751
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2752
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2753
  val == .true
2794
2754
  else if (showdown)
@@ -2800,7 +2760,7 @@ pub const Rolls = struct {
2800
2760
  return cfz;
2801
2761
  }
2802
2762
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2765
  val == .true
2806
2766
  else if (showdown)
@@ -2812,7 +2772,7 @@ pub const Rolls = struct {
2812
2772
  return par;
2813
2773
  }
2814
2774
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2775
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2776
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2777
  val == .true
2818
2778
  else if (showdown)
@@ -2824,7 +2784,7 @@ pub const Rolls = struct {
2824
2784
  return proc;
2825
2785
  }
2826
2786
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2787
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2788
  const rate: u8 = if (low) 26 else 77;
2829
2789
 
2830
2790
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2838,7 +2798,7 @@ pub const Rolls = struct {
2838
2798
  return proc;
2839
2799
  }
2840
2800
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2801
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2802
  const rate: u8 = if (low) 52 else 103;
2843
2803
 
2844
2804
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2852,7 +2812,7 @@ pub const Rolls = struct {
2852
2812
  return proc;
2853
2813
  }
2854
2814
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2815
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2816
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2817
  val == .true
2858
2818
  else if (showdown)
@@ -2864,14 +2824,12 @@ pub const Rolls = struct {
2864
2824
  return proc;
2865
2825
  }
2866
2826
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2827
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2828
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2829
  val
2870
- else if (showdown) move: {
2871
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2872
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2873
- break :move @enumFromInt(r + mod);
2874
- } else move: {
2830
+ else if (showdown)
2831
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
+ else move: {
2875
2833
  while (true) {
2876
2834
  const r = battle.rng.next();
2877
2835
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,7 +2842,7 @@ pub const Rolls = struct {
2884
2842
  return move;
2885
2843
  }
2886
2844
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2845
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2846
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2847
  val - 1
2890
2848
  else if (showdown)
@@ -2901,11 +2859,11 @@ pub const Rolls = struct {
2901
2859
  return power;
2902
2860
  }
2903
2861
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2862
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2863
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2864
  @intCast(val)
2907
2865
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2866
+ battle.rng.range(u3, 1, 8)
2909
2867
  else duration: {
2910
2868
  while (true) {
2911
2869
  const r = battle.rng.next() & 7;
@@ -2918,11 +2876,11 @@ pub const Rolls = struct {
2918
2876
  return duration;
2919
2877
  }
2920
2878
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2879
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
2880
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2881
  val
2924
2882
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2883
+ battle.rng.range(u4, 1, 9)
2926
2884
  else
2927
2885
  @intCast((battle.rng.next() & 7) + 1);
2928
2886
 
@@ -2931,11 +2889,11 @@ pub const Rolls = struct {
2931
2889
  return duration;
2932
2890
  }
2933
2891
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2892
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2935
2893
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2894
  @intCast(val)
2937
2895
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2896
+ battle.rng.range(u3, 2, 6)
2939
2897
  else
2940
2898
  @intCast((battle.rng.next() & 3) + 2);
2941
2899
 
@@ -2944,11 +2902,11 @@ pub const Rolls = struct {
2944
2902
  return duration;
2945
2903
  }
2946
2904
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
2906
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
2907
  @intCast(val)
2950
2908
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
2909
+ battle.rng.range(u3, 2, 4)
2952
2910
  else
2953
2911
  @intCast((battle.rng.next() & 1) + 2);
2954
2912
 
@@ -2957,7 +2915,9 @@ pub const Rolls = struct {
2957
2915
  return duration;
2958
2916
  }
2959
2917
 
2960
- inline fn distribution(
2918
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2919
+
2920
+ fn distribution(
2961
2921
  battle: anytype,
2962
2922
  comptime effect: Move.Effect,
2963
2923
  player: Player,
@@ -2967,7 +2927,7 @@ pub const Rolls = struct {
2967
2927
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
2928
  @intCast(val)
2969
2929
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2930
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
2931
  else n: {
2972
2932
  const r = (battle.rng.next() & 3);
2973
2933
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3014,10 +2974,9 @@ pub const Rolls = struct {
3014
2974
  while (i < moves.len and r > 0) : (i += 1) {
3015
2975
  if (moves[i].pp > 0) {
3016
2976
  r -= 1;
3017
- if (r == 0) break;
2977
+ if (r == 0) break :slot @intCast(i + 1);
3018
2978
  }
3019
2979
  }
3020
- break :slot @intCast(i + 1);
3021
2980
  }
3022
2981
  }
3023
2982
 
@@ -3039,11 +2998,11 @@ test "RNG agreement" {
3039
2998
  expected[i] = @intCast(i * 0x1000000);
3040
2999
  }
3041
3000
 
3042
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3044
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3045
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3046
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3001
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3002
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3003
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3004
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3005
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3006
 
3048
3007
  for (0..expected.len) |i| {
3049
3008
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3133,7 +3092,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3092
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3093
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3094
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3095
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3096
  const s = if (struggle) 0 else slot;
3138
3097
  out[n] = .{ .type = .Move, .data = s };
3139
3098
  n += 1;
@@ -3148,7 +3107,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3107
  const m = active.moves[slot - 1];
3149
3108
  if (m.id == .None) break;
3150
3109
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3110
+ if (active.volatiles.disable_move == slot) continue;
3152
3111
  out[n] = .{ .type = .Move, .data = slot };
3153
3112
  n += 1;
3154
3113
  }