@pkmn/engine 0.1.0-dev.d3e796d5 → 0.1.0-dev.d4119c62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +8 -8
  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/index.d.ts +2 -1
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +57 -38
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/tools/debug.js +3 -1
  13. package/build/tools/debug.js.map +1 -1
  14. package/build.zig +40 -23
  15. package/package.json +31 -18
  16. package/src/bin/install-pkmn-engine +1 -1
  17. package/src/lib/binding/node.zig +1 -1
  18. package/src/lib/common/data.zig +8 -20
  19. package/src/lib/common/optional.zig +8 -1
  20. package/src/lib/common/protocol.zig +722 -296
  21. package/src/lib/common/rational.zig +5 -5
  22. package/src/lib/common/rng.zig +15 -6
  23. package/src/lib/gen1/README.md +31 -29
  24. package/src/lib/gen1/calc.zig +4 -4
  25. package/src/lib/gen1/chance.zig +55 -39
  26. package/src/lib/gen1/data/moves.zig +192 -180
  27. package/src/lib/gen1/data/species.zig +2 -2
  28. package/src/lib/gen1/data/types.zig +7 -7
  29. package/src/lib/gen1/data.zig +31 -43
  30. package/src/lib/gen1/helpers.zig +26 -41
  31. package/src/lib/gen1/mechanics.zig +297 -335
  32. package/src/lib/gen2/README.md +18 -72
  33. package/src/lib/gen2/calc.zig +143 -0
  34. package/src/lib/gen2/chance.zig +1314 -0
  35. package/src/lib/gen2/data/items.zig +12 -18
  36. package/src/lib/gen2/data/moves.zig +649 -568
  37. package/src/lib/gen2/data/species.zig +1 -1
  38. package/src/lib/gen2/data/types.zig +134 -31
  39. package/src/lib/gen2/data.zig +70 -67
  40. package/src/lib/gen2/generated.zig +808 -0
  41. package/src/lib/gen2/helpers.zig +4 -9
  42. package/src/lib/gen2/mechanics.zig +3135 -102
@@ -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;
@@ -1435,7 +1402,7 @@ fn moveHit(
1435
1402
  const overwritten = overwrite and state > 0;
1436
1403
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1437
1404
 
1438
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1405
+ var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1439
1406
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1440
1407
  accuracy = accuracy * boost[0] / boost[1];
1441
1408
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -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
  }
@@ -1545,21 +1511,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1545
1511
  var volatiles = &side.active.volatiles;
1546
1512
 
1547
1513
  const brn = Status.is(stored.status, .BRN);
1548
- if (brn or Status.is(stored.status, .PSN)) blk: {
1514
+ if (brn or Status.is(stored.status, .PSN)) {
1549
1515
  var damage = @max(stored.stats.hp / 16, 1);
1550
-
1551
1516
  if (volatiles.Toxic) {
1552
1517
  volatiles.toxic += 1;
1553
1518
  damage *= volatiles.toxic;
1554
1519
  }
1520
+ damage = @min(damage, stored.hp);
1555
1521
 
1556
- const amount = @min(damage, stored.hp);
1557
- if (showdown and amount == 0) break :blk;
1558
-
1559
- stored.hp -= amount;
1560
-
1561
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1562
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1522
+ if (!showdown or damage > 0) {
1523
+ stored.hp -= damage;
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 });
1526
+ }
1563
1527
  }
1564
1528
 
1565
1529
  if (volatiles.LeechSeed) {
@@ -1583,14 +1547,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1583
1547
  const amount = @min(damage, stored.hp);
1584
1548
  stored.hp -= amount;
1585
1549
 
1586
- // As above, Pokémon Showdown uses damageOf but its not relevant
1587
- 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 });
1588
1552
 
1589
1553
  const before = foe_stored.hp;
1590
1554
  // Uncapped damage is added back to the foe
1591
1555
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1592
1556
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1593
- 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 });
1594
1558
  }
1595
1559
  }
1596
1560
 
@@ -1601,18 +1565,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1601
1565
  battle.turn += 1;
1602
1566
 
1603
1567
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1604
- try options.log.tie();
1568
+ try options.log.tie(.{});
1605
1569
  return Result.Tie;
1606
1570
  }
1607
1571
 
1608
1572
  if (showdown and battle.turn >= 1000) {
1609
- try options.log.tie();
1573
+ try options.log.tie(.{});
1610
1574
  return Result.Tie;
1611
1575
  } else if (battle.turn >= 65535) {
1612
1576
  return Result.Error;
1613
1577
  }
1614
1578
 
1615
- try options.log.turn(battle.turn);
1579
+ try options.log.turn(.{battle.turn});
1616
1580
 
1617
1581
  return Result.Default;
1618
1582
  }
@@ -1672,9 +1636,9 @@ fn checkEBC(battle: anytype) bool {
1672
1636
  }
1673
1637
 
1674
1638
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1675
- if (side.active.volatiles.disabled_move != 0) {
1639
+ if (side.active.volatiles.disable_move != 0) {
1676
1640
  // A Pokémon that transforms after being disabled may end up with less move slots
1677
- 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;
1678
1642
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1679
1643
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1680
1644
  m
@@ -1682,14 +1646,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1682
1646
  side.last_selected_move;
1683
1647
  if (m != .None and m == last) {
1684
1648
  side.active.volatiles.Charging = false;
1685
- try options.log.disabled(ident, last);
1649
+ try options.log.cant(.{ ident, .Disable, last });
1686
1650
  return true;
1687
1651
  }
1688
1652
  }
1689
1653
  return false;
1690
1654
  }
1691
1655
 
1692
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1656
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1693
1657
  const side = battle.side(who);
1694
1658
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1695
1659
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1707,7 +1671,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1707
1671
  return true;
1708
1672
  }
1709
1673
 
1710
- inline fn onBegin(
1674
+ fn onBegin(
1711
1675
  battle: anytype,
1712
1676
  player: Player,
1713
1677
  move: Move.Data,
@@ -1737,7 +1701,7 @@ inline fn onBegin(
1737
1701
  };
1738
1702
  }
1739
1703
 
1740
- 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 {
1741
1705
  assert(move.effect.onEnd());
1742
1706
  return switch (move.effect) {
1743
1707
  .Bide => Effects.bide(battle, player, options),
@@ -1753,7 +1717,7 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1753
1717
  };
1754
1718
  }
1755
1719
 
1756
- inline fn alwaysHappens(
1720
+ fn alwaysHappens(
1757
1721
  battle: anytype,
1758
1722
  player: Player,
1759
1723
  move: Move.Data,
@@ -1772,7 +1736,7 @@ inline fn alwaysHappens(
1772
1736
  };
1773
1737
  }
1774
1738
 
1775
- 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 {
1776
1740
  return switch (move.effect) {
1777
1741
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1778
1742
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,7 +1763,7 @@ pub const Effects = struct {
1799
1763
  side.active.volatiles.state = 0;
1800
1764
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1801
1765
 
1802
- try options.log.start(battle.active(player), .Bide);
1766
+ try options.log.start(.{ battle.active(player), .Bide });
1803
1767
  }
1804
1768
 
1805
1769
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1813,11 +1777,11 @@ pub const Effects = struct {
1813
1777
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1814
1778
  if (Status.is(foe_stored.status, .FRZ)) {
1815
1779
  assert(move.type == .Fire);
1816
- try options.log.curestatus(
1780
+ try options.log.curestatus(.{
1817
1781
  battle.active(player.foe()),
1818
1782
  foe_stored.status,
1819
1783
  .Message,
1820
- );
1784
+ });
1821
1785
  foe_stored.status = 0;
1822
1786
  }
1823
1787
  return;
@@ -1831,7 +1795,7 @@ pub const Effects = struct {
1831
1795
  foe_stored.status = Status.init(.BRN);
1832
1796
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1833
1797
 
1834
- 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 });
1835
1799
  }
1836
1800
 
1837
1801
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1841,8 +1805,8 @@ pub const Effects = struct {
1841
1805
  volatiles.Charging = true;
1842
1806
  const move = side.last_selected_move;
1843
1807
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1844
- try options.log.laststill();
1845
- try options.log.prepare(battle.active(player), move);
1808
+ try options.log.laststill(.{});
1809
+ try options.log.prepare(.{ battle.active(player), move });
1846
1810
  }
1847
1811
 
1848
1812
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1853,17 +1817,11 @@ pub const Effects = struct {
1853
1817
  if (!try Rolls.confusionChance(battle, player, options)) return;
1854
1818
  } else {
1855
1819
  if (showdown) {
1856
- if (!try checkHit(battle, player, move, options)) {
1857
- return;
1858
- } else if (sub) {
1859
- return options.log.fail(battle.active(player.foe()), .None);
1860
- }
1820
+ if (!try checkHit(battle, player, move, options)) return;
1821
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1861
1822
  } else {
1862
- if (sub) {
1863
- return options.log.fail(battle.active(player.foe()), .None);
1864
- } else if (!try checkHit(battle, player, move, options)) {
1865
- return;
1866
- }
1823
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
+ if (!try checkHit(battle, player, move, options)) return;
1867
1825
  }
1868
1826
  try options.chance.commit(player, .hit);
1869
1827
  }
@@ -1872,23 +1830,24 @@ pub const Effects = struct {
1872
1830
  foe.active.volatiles.Confusion = true;
1873
1831
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1874
1832
 
1875
- try options.log.start(battle.active(player.foe()), .Confusion);
1833
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1876
1834
  }
1877
1835
 
1878
1836
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1879
1837
  const foe = battle.foe(player);
1880
1838
 
1881
1839
  if (foe.active.volatiles.Invulnerable) {
1882
- try options.log.lastmiss();
1883
- return options.log.miss(battle.active(player));
1840
+ try options.log.lastmiss(.{});
1841
+ return options.log.miss(.{battle.active(player)});
1884
1842
  }
1885
1843
 
1886
1844
  battle.side(player).active.types = foe.active.types;
1887
- return options.log.typechange(
1845
+ return options.log.start(.{
1888
1846
  battle.active(player),
1847
+ .TypeChange,
1889
1848
  foe.active.types,
1890
1849
  battle.active(player.foe()),
1891
- );
1850
+ });
1892
1851
  }
1893
1852
 
1894
1853
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1902,8 +1861,8 @@ pub const Effects = struct {
1902
1861
  try options.chance.commit(player, .hit);
1903
1862
  }
1904
1863
 
1905
- if (volatiles.disabled_move != 0) {
1906
- try options.log.fail(foe_ident, .None);
1864
+ if (volatiles.disable_move != 0) {
1865
+ try options.log.fail(.{ foe_ident, .None });
1907
1866
  return null;
1908
1867
  }
1909
1868
 
@@ -1919,7 +1878,7 @@ pub const Effects = struct {
1919
1878
  // Technically this is still considered simply a "miss" on the cartridge,
1920
1879
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1921
1880
  if (n == 0) {
1922
- try options.log.fail(foe_ident, .None);
1881
+ try options.log.fail(.{ foe_ident, .None });
1923
1882
  return null;
1924
1883
  } else if (err) {
1925
1884
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1927,18 +1886,18 @@ pub const Effects = struct {
1927
1886
  return Result.Error;
1928
1887
  }
1929
1888
 
1930
- volatiles.disabled_move =
1889
+ volatiles.disable_move =
1931
1890
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1932
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1891
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1933
1892
 
1934
- const id = foe.active.move(volatiles.disabled_move).id;
1935
- 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 });
1936
1895
  return null;
1937
1896
  }
1938
1897
 
1939
1898
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1940
- var side = battle.side(player);
1941
- var stored = side.stored();
1899
+ var stored = battle.side(player).stored();
1900
+ const ident = battle.active(player);
1942
1901
 
1943
1902
  if (battle.last_damage == 0) {
1944
1903
  assert(showdown);
@@ -1951,7 +1910,7 @@ pub const Effects = struct {
1951
1910
  if (stored.hp == stored.stats.hp) return;
1952
1911
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1953
1912
 
1954
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1913
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1955
1914
  }
1956
1915
 
1957
1916
  fn explode(battle: anytype, player: Player) !void {
@@ -1983,7 +1942,7 @@ pub const Effects = struct {
1983
1942
  if (side.active.volatiles.FocusEnergy) return;
1984
1943
  side.active.volatiles.FocusEnergy = true;
1985
1944
 
1986
- try options.log.start(battle.active(player), .FocusEnergy);
1945
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1987
1946
  }
1988
1947
 
1989
1948
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2005,7 +1964,7 @@ pub const Effects = struct {
2005
1964
  foe_stored.status = Status.init(.FRZ);
2006
1965
  // GLITCH: Hyper Beam recharging status is not cleared
2007
1966
 
2008
- try options.log.status(foe_ident, foe_stored.status, .None);
1967
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2009
1968
  }
2010
1969
 
2011
1970
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2025,8 +1984,8 @@ pub const Effects = struct {
2025
1984
  side.active.stats = side_stored.stats;
2026
1985
  foe.active.stats = foe_stored.stats;
2027
1986
 
2028
- try log.activate(player_ident, .Haze);
2029
- try log.clearallboost();
1987
+ try log.activate(.{ player_ident, .Haze });
1988
+ try log.clearallboost(.{});
2030
1989
 
2031
1990
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2032
1991
  if (showdown) {
@@ -2034,11 +1993,11 @@ pub const Effects = struct {
2034
1993
  const p: Player = @enumFromInt(i);
2035
1994
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2036
1995
  if (p != player and Status.any(s.stored().status)) {
2037
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1996
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2038
1997
  s.stored().status = 0;
2039
1998
  } else if (showdown and s.stored().status == Status.TOX) {
2040
1999
  s.stored().status = Status.init(.PSN);
2041
- try log.status(battle.active(p), s.stored().status, .None);
2000
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2042
2001
  }
2043
2002
  try clearVolatiles(battle, p, p != player, options);
2044
2003
  }
@@ -2047,7 +2006,7 @@ pub const Effects = struct {
2047
2006
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2048
2007
  foe.last_selected_move = .SKIP_TURN;
2049
2008
  }
2050
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2009
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2051
2010
  foe_stored.status = 0;
2052
2011
  }
2053
2012
  try clearVolatiles(battle, player, false, options);
@@ -2063,7 +2022,7 @@ pub const Effects = struct {
2063
2022
  // GLITCH: HP recovery move failure glitches
2064
2023
  const delta = stored.stats.hp - stored.hp;
2065
2024
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2066
- return try options.log.fail(ident, .None);
2025
+ return options.log.fail(.{ ident, .None });
2067
2026
  }
2068
2027
 
2069
2028
  const rest = side.last_selected_move == .Rest;
@@ -2071,17 +2030,17 @@ pub const Effects = struct {
2071
2030
  // Adding the sleep status runs the sleep condition handler to roll duration
2072
2031
  if (showdown) battle.rng.advance(1);
2073
2032
  stored.status = Status.slf(2);
2074
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2033
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2075
2034
  stored.hp = stored.stats.hp;
2076
2035
  } else {
2077
2036
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2078
2037
  }
2079
- 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 });
2080
2039
  }
2081
2040
 
2082
2041
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2083
2042
  battle.side(player).active.volatiles.Recharging = true;
2084
- try options.log.mustrecharge(battle.active(player));
2043
+ try options.log.mustrecharge(.{battle.active(player)});
2085
2044
  }
2086
2045
 
2087
2046
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2090,33 +2049,32 @@ pub const Effects = struct {
2090
2049
  if (showdown) {
2091
2050
  // Invulnerability trumps type immunity on Pokémon Showdown
2092
2051
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2093
- return options.log.immune(battle.active(player.foe()), .None);
2052
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2094
2053
  }
2095
2054
  if (!try checkHit(battle, player, move, options)) return;
2096
2055
  if (foe.active.volatiles.LeechSeed) return;
2097
2056
  } else {
2098
2057
  if (!try checkHit(battle, player, move, options)) return;
2099
2058
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2100
- try options.log.lastmiss();
2101
- return options.log.miss(battle.active(player));
2059
+ try options.log.lastmiss(.{});
2060
+ return options.log.miss(.{battle.active(player)});
2102
2061
  }
2103
2062
  }
2104
2063
 
2105
2064
  try options.chance.commit(player, .hit);
2106
2065
  foe.active.volatiles.LeechSeed = true;
2107
2066
 
2108
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2067
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2109
2068
  }
2110
2069
 
2111
2070
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2112
2071
  var side = battle.side(player);
2072
+ const ident = battle.active(player);
2113
2073
 
2114
- if (side.active.volatiles.LightScreen) {
2115
- return options.log.fail(battle.active(player), .None);
2116
- }
2074
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2117
2075
  side.active.volatiles.LightScreen = true;
2118
2076
 
2119
- try options.log.start(battle.active(player), .LightScreen);
2077
+ try options.log.start(.{ ident, .LightScreen });
2120
2078
  }
2121
2079
 
2122
2080
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2145,7 +2103,7 @@ pub const Effects = struct {
2145
2103
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2146
2104
  if (!try checkHit(battle, player, move, options)) return;
2147
2105
  try options.chance.commit(player, .hit);
2148
- return try options.log.fail(battle.active(player.foe()), .None);
2106
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2149
2107
  }
2150
2108
  }
2151
2109
  if (!try checkHit(battle, player, move, options)) return;
@@ -2154,7 +2112,7 @@ pub const Effects = struct {
2154
2112
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2155
2113
  side.active.move(oslot).id = foe.active.move(rslot).id;
2156
2114
 
2157
- 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 });
2158
2116
  }
2159
2117
 
2160
2118
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2163,7 +2121,7 @@ pub const Effects = struct {
2163
2121
  if (side.active.volatiles.Mist) return;
2164
2122
  side.active.volatiles.Mist = true;
2165
2123
 
2166
- try options.log.start(battle.active(player), .Mist);
2124
+ try options.log.start(.{ battle.active(player), .Mist });
2167
2125
  }
2168
2126
 
2169
2127
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2189,17 +2147,18 @@ pub const Effects = struct {
2189
2147
 
2190
2148
  if (showdown) {
2191
2149
  // Invulnerability trumps type immunity on Pokémon Showdown
2192
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2193
- 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;
2194
2153
  }
2195
2154
  if (Status.any(foe_stored.status)) {
2196
- return log.fail(
2155
+ return log.fail(.{
2197
2156
  foe_ident,
2198
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2199
- );
2157
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2158
+ });
2200
2159
  }
2201
2160
  if (!showdown) {
2202
- if (immune) return log.immune(foe_ident, .None);
2161
+ if (immune) return log.immune(.{ foe_ident, .None });
2203
2162
  if (!try checkHit(battle, player, move, options)) return;
2204
2163
  }
2205
2164
  try options.chance.commit(player, .hit);
@@ -2207,7 +2166,7 @@ pub const Effects = struct {
2207
2166
  foe_stored.status = Status.init(.PAR);
2208
2167
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2209
2168
 
2210
- try log.status(foe_ident, foe_stored.status, .None);
2169
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2211
2170
  }
2212
2171
 
2213
2172
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2229,12 +2188,12 @@ pub const Effects = struct {
2229
2188
  foe_stored.status = Status.init(.PAR);
2230
2189
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2231
2190
 
2232
- 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 });
2233
2192
  }
2234
2193
 
2235
2194
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2236
2195
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2237
- try options.log.fieldactivate();
2196
+ try options.log.fieldactivate(.{});
2238
2197
  }
2239
2198
  }
2240
2199
 
@@ -2249,23 +2208,23 @@ pub const Effects = struct {
2249
2208
  return;
2250
2209
  } else if (foe.active.volatiles.Substitute) {
2251
2210
  if (move.effect != .Poison) return;
2252
- return log.fail(foe_ident, .None);
2211
+ return log.fail(.{ foe_ident, .None });
2253
2212
  } else if (Status.any(foe_stored.status)) {
2254
2213
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2255
2214
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2256
2215
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2257
- 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))
2258
2217
  if (!showdown)
2259
- .Poison
2218
+ Fail.Poison
2260
2219
  else if (toxic == (foe_stored.status == Status.TOX))
2261
- if (toxic) .Toxic else .Poison
2220
+ if (toxic) Fail.Toxic else Fail.Poison
2262
2221
  else
2263
- .None
2222
+ Fail.None
2264
2223
  else
2265
- .None);
2224
+ Fail.None });
2266
2225
  } else if (foe.active.types.includes(.Poison)) {
2267
2226
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2268
- return log.immune(foe_ident, .None);
2227
+ return log.immune(.{ foe_ident, .None });
2269
2228
  }
2270
2229
 
2271
2230
  if (move.effect == .Poison) {
@@ -2284,7 +2243,7 @@ pub const Effects = struct {
2284
2243
  foe.active.volatiles.toxic = 0;
2285
2244
  }
2286
2245
 
2287
- try log.status(foe_ident, foe_stored.status, .None);
2246
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2288
2247
  }
2289
2248
 
2290
2249
  fn rage(battle: anytype, player: Player) !void {
@@ -2304,22 +2263,23 @@ pub const Effects = struct {
2304
2263
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2305
2264
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2306
2265
 
2307
- try options.log.damageOf(
2266
+ try options.log.damage(.{
2308
2267
  battle.active(player),
2309
2268
  stored,
2310
2269
  .RecoilOf,
2311
2270
  battle.active(player.foe()),
2312
- );
2271
+ });
2313
2272
  if (showdown and stored.hp == 0) residual.* = false;
2314
2273
  }
2315
2274
 
2316
2275
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2317
2276
  var side = battle.side(player);
2277
+ const ident = battle.active(player);
2318
2278
 
2319
- 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 });
2320
2280
  side.active.volatiles.Reflect = true;
2321
2281
 
2322
- try options.log.start(battle.active(player), .Reflect);
2282
+ try options.log.start(.{ ident, .Reflect });
2323
2283
  }
2324
2284
 
2325
2285
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2333,10 +2293,10 @@ pub const Effects = struct {
2333
2293
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2334
2294
  } else {
2335
2295
  if (Status.any(foe_stored.status)) {
2336
- return options.log.fail(
2296
+ return options.log.fail(.{
2337
2297
  foe_ident,
2338
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2339
- );
2298
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
+ });
2340
2300
  }
2341
2301
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2342
2302
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2353,18 +2313,17 @@ pub const Effects = struct {
2353
2313
 
2354
2314
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2355
2315
  const last = battle.side(player).last_selected_move;
2356
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2316
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2357
2317
  }
2358
2318
 
2359
2319
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2360
- try options.log.activate(battle.active(player), .Splash);
2320
+ try options.log.activate(.{ battle.active(player), .Splash });
2361
2321
  }
2362
2322
 
2363
2323
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2364
2324
  var side = battle.side(player);
2365
2325
  if (side.active.volatiles.Substitute) {
2366
- try options.log.fail(battle.active(player), .Substitute);
2367
- return;
2326
+ return options.log.fail(.{ battle.active(player), .Substitute });
2368
2327
  }
2369
2328
 
2370
2329
  assert(side.stored().stats.hp <= 1023);
@@ -2376,28 +2335,32 @@ pub const Effects = struct {
2376
2335
  // avoid having to convert to floating point)
2377
2336
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2378
2337
  if (side.stored().hp < required_hp) {
2379
- try options.log.fail(battle.active(player), .Weak);
2380
- return;
2338
+ return options.log.fail(.{ battle.active(player), .Weak });
2381
2339
  }
2382
2340
 
2383
2341
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2384
2342
  side.stored().hp -= hp;
2385
2343
  side.active.volatiles.substitute = hp + 1;
2386
2344
  side.active.volatiles.Substitute = true;
2387
- try options.log.start(battle.active(player), .Substitute);
2345
+ try options.log.start(.{ battle.active(player), .Substitute });
2388
2346
  if (hp > 0) {
2389
- try options.log.damage(battle.active(player), side.stored(), .None);
2347
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2390
2348
  if (showdown and side.stored().hp == 0) residual.* = false;
2391
2349
  }
2392
2350
  }
2393
2351
 
2394
2352
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2395
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2396
-
2397
2353
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2398
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2399
-
2400
- battle.last_damage = 0;
2354
+ if (showdown) {
2355
+ if (battle.side(player).last_selected_move == .Teleport) return;
2356
+ if (try checkHit(battle, player, move, options)) {
2357
+ try options.chance.commit(player, .hit);
2358
+ }
2359
+ battle.last_damage = 0;
2360
+ } else {
2361
+ try options.log.fail(.{ battle.active(player), .None });
2362
+ try options.log.laststill(.{});
2363
+ }
2401
2364
  }
2402
2365
 
2403
2366
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2435,7 +2398,7 @@ pub const Effects = struct {
2435
2398
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2436
2399
  }
2437
2400
 
2438
- try options.log.transform(battle.active(player), foe_ident);
2401
+ try options.log.transform(.{ battle.active(player), foe_ident });
2439
2402
  }
2440
2403
 
2441
2404
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2465,87 +2428,87 @@ pub const Effects = struct {
2465
2428
  switch (move.effect) {
2466
2429
  .AttackUp1, .AttackUp2, .Rage => {
2467
2430
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2468
- if (boosts.atk == 6) return try log.fail(ident, .None);
2431
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2469
2432
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2470
2433
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2471
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2434
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2472
2435
  if (stats.atk == MAX_STAT_VALUE) {
2473
2436
  boosts.atk -= 1;
2474
2437
  if (showdown) {
2475
- try log.boost(ident, reason, n);
2476
- try log.boost(ident, Boost.Attack, -1);
2438
+ try log.boost(.{ ident, reason, n });
2439
+ try log.boost(.{ ident, .Attack, -1 });
2477
2440
  if (move.effect == .Rage) return;
2478
2441
  }
2479
- return try log.fail(ident, .None);
2442
+ return log.fail(.{ ident, .None });
2480
2443
  }
2481
2444
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2482
2445
  const stat = unmodifiedStats(battle, side).atk;
2483
2446
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
- try log.boost(ident, reason, n);
2447
+ try log.boost(.{ ident, reason, n });
2485
2448
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2486
2449
  if (showdown and move.effect == .Rage) return;
2487
2450
  },
2488
2451
  .DefenseUp1, .DefenseUp2 => {
2489
2452
  assert(boosts.def >= -6 and boosts.def <= 6);
2490
- if (boosts.def == 6) return try log.fail(ident, .None);
2453
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2491
2454
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2492
2455
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2493
2456
  if (stats.def == MAX_STAT_VALUE) {
2494
2457
  boosts.def -= 1;
2495
2458
  if (showdown) {
2496
- try log.boost(ident, .Defense, n);
2497
- try log.boost(ident, .Defense, -1);
2459
+ try log.boost(.{ ident, .Defense, n });
2460
+ try log.boost(.{ ident, .Defense, -1 });
2498
2461
  }
2499
- return try log.fail(ident, .None);
2462
+ return log.fail(.{ ident, .None });
2500
2463
  }
2501
2464
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2502
2465
  const stat = unmodifiedStats(battle, side).def;
2503
2466
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
- try log.boost(ident, .Defense, n);
2467
+ try log.boost(.{ ident, .Defense, n });
2505
2468
  },
2506
2469
  .SpeedUp2 => {
2507
2470
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2508
- if (boosts.spe == 6) return try log.fail(ident, .None);
2471
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2509
2472
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2510
2473
  if (stats.spe == MAX_STAT_VALUE) {
2511
2474
  boosts.spe -= 1;
2512
2475
  if (showdown) {
2513
- try log.boost(ident, .Speed, 2);
2514
- try log.boost(ident, .Speed, -1);
2476
+ try log.boost(.{ ident, .Speed, 2 });
2477
+ try log.boost(.{ ident, .Speed, -1 });
2515
2478
  }
2516
- return try log.fail(ident, .None);
2479
+ return log.fail(.{ ident, .None });
2517
2480
  }
2518
2481
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2519
2482
  const stat = unmodifiedStats(battle, side).spe;
2520
2483
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2521
- try log.boost(ident, .Speed, 2);
2484
+ try log.boost(.{ ident, .Speed, 2 });
2522
2485
  },
2523
2486
  .SpecialUp1, .SpecialUp2 => {
2524
2487
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2525
- if (boosts.spc == 6) return try log.fail(ident, .None);
2488
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2526
2489
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2527
2490
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2528
2491
  if (stats.spc == MAX_STAT_VALUE) {
2529
2492
  boosts.spc -= 1;
2530
2493
  if (showdown) {
2531
- try log.boost(ident, .SpecialAttack, n);
2532
- try log.boost(ident, .SpecialAttack, -1);
2533
- try log.boost(ident, .SpecialDefense, n);
2534
- 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 });
2535
2498
  }
2536
- return try log.fail(ident, .None);
2499
+ return log.fail(.{ ident, .None });
2537
2500
  }
2538
2501
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2539
2502
  const stat = unmodifiedStats(battle, side).spc;
2540
2503
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2541
- try log.boost(ident, .SpecialAttack, n);
2542
- try log.boost(ident, .SpecialDefense, n);
2504
+ try log.boost(.{ ident, .SpecialAttack, n });
2505
+ try log.boost(.{ ident, .SpecialDefense, n });
2543
2506
  },
2544
2507
  .EvasionUp1 => {
2545
2508
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2546
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2509
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2547
2510
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2548
- try log.boost(ident, .Evasion, 1);
2511
+ try log.boost(.{ ident, .Evasion, 1 });
2549
2512
  },
2550
2513
  else => unreachable,
2551
2514
  }
@@ -2560,7 +2523,7 @@ pub const Effects = struct {
2560
2523
  const foe_ident = battle.active(player.foe());
2561
2524
  const secondary = move.effect.isStatDownChance();
2562
2525
 
2563
- 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 });
2564
2527
 
2565
2528
  if (secondary) {
2566
2529
  const proc = try Rolls.unboost(battle, player, options);
@@ -2578,82 +2541,82 @@ pub const Effects = struct {
2578
2541
  switch (move.effect) {
2579
2542
  .AttackDown1, .AttackDownChance => {
2580
2543
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2581
- 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 });
2582
2545
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2583
2546
  if (stats.atk == 1) {
2584
2547
  boosts.atk += 1;
2585
2548
  if (showdown) {
2586
- try log.boost(foe_ident, .Attack, -1);
2587
- try log.boost(foe_ident, .Attack, 1);
2549
+ try log.boost(.{ foe_ident, .Attack, -1 });
2550
+ try log.boost(.{ foe_ident, .Attack, 1 });
2588
2551
  }
2589
- return try log.fail(foe_ident, .None);
2552
+ return log.fail(.{ foe_ident, .None });
2590
2553
  }
2591
2554
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2592
2555
  const stat = unmodifiedStats(battle, foe).atk;
2593
2556
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2594
- try log.boost(foe_ident, .Attack, -1);
2557
+ try log.boost(.{ foe_ident, .Attack, -1 });
2595
2558
  },
2596
2559
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2597
2560
  assert(boosts.def >= -6 and boosts.def <= 6);
2598
- 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 });
2599
2562
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2600
2563
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2601
2564
  if (stats.def == 1) {
2602
2565
  boosts.def += 1;
2603
2566
  if (showdown) {
2604
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2605
- 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 });
2606
2569
  }
2607
- return try log.fail(foe_ident, .None);
2570
+ return log.fail(.{ foe_ident, .None });
2608
2571
  }
2609
2572
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2610
2573
  const stat = unmodifiedStats(battle, foe).def;
2611
2574
  stats.def = @max(1, stat * mod[0] / mod[1]);
2612
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2575
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2613
2576
  },
2614
2577
  .SpeedDown1, .SpeedDownChance => {
2615
2578
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2616
- 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 });
2617
2580
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2618
2581
  if (stats.spe == 1) {
2619
2582
  boosts.spe += 1;
2620
2583
  if (showdown) {
2621
- try log.boost(foe_ident, .Speed, -1);
2622
- try log.boost(foe_ident, .Speed, 1);
2584
+ try log.boost(.{ foe_ident, .Speed, -1 });
2585
+ try log.boost(.{ foe_ident, .Speed, 1 });
2623
2586
  }
2624
- return try log.fail(foe_ident, .None);
2587
+ return log.fail(.{ foe_ident, .None });
2625
2588
  }
2626
2589
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2627
2590
  const stat = unmodifiedStats(battle, foe).spe;
2628
2591
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2629
- try log.boost(foe_ident, .Speed, -1);
2592
+ try log.boost(.{ foe_ident, .Speed, -1 });
2630
2593
  assert(boosts.spe >= -6);
2631
2594
  },
2632
2595
  .SpecialDownChance => {
2633
2596
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2634
- 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 });
2635
2598
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2636
2599
  if (stats.spc == 1) {
2637
2600
  boosts.spc += 1;
2638
2601
  if (showdown) {
2639
- try log.boost(foe_ident, .SpecialAttack, -1);
2640
- try log.boost(foe_ident, .SpecialAttack, 1);
2641
- try log.boost(foe_ident, .SpecialDefense, -1);
2642
- 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 });
2643
2606
  }
2644
- return try log.fail(foe_ident, .None);
2607
+ return log.fail(.{ foe_ident, .None });
2645
2608
  }
2646
2609
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2647
2610
  const stat = unmodifiedStats(battle, foe).spc;
2648
2611
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2649
- try log.boost(foe_ident, .SpecialAttack, -1);
2650
- try log.boost(foe_ident, .SpecialDefense, -1);
2612
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2613
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2651
2614
  },
2652
2615
  .AccuracyDown1 => {
2653
2616
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2654
- 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 });
2655
2618
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2656
- try log.boost(foe_ident, .Accuracy, -1);
2619
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2657
2620
  },
2658
2621
  else => unreachable,
2659
2622
  }
@@ -2677,7 +2640,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2677
2640
  }
2678
2641
  }
2679
2642
 
2680
- inline fn isForced(active: anytype) bool {
2643
+ fn isForced(active: anytype) bool {
2681
2644
  return active.volatiles.Recharging or active.volatiles.Rage or
2682
2645
  active.volatiles.Thrashing or active.volatiles.Charging;
2683
2646
  }
@@ -2690,27 +2653,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2690
2653
 
2691
2654
  options.chance.clearDurations(who, clear);
2692
2655
 
2693
- if (volatiles.disabled_move != 0) {
2694
- volatiles.disabled_move = 0;
2695
- volatiles.disabled_duration = 0;
2696
- 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 });
2697
2660
  }
2698
2661
  if (volatiles.Confusion) {
2699
2662
  // volatiles.confusion is left unchanged
2700
2663
  volatiles.Confusion = false;
2701
- try log.end(ident, .ConfusionSilent);
2664
+ try log.end(.{ ident, .ConfusionSilent });
2702
2665
  }
2703
2666
  if (volatiles.Mist) {
2704
2667
  volatiles.Mist = false;
2705
- try log.end(ident, .Mist);
2668
+ try log.end(.{ ident, .MistSilent });
2706
2669
  }
2707
2670
  if (volatiles.FocusEnergy) {
2708
2671
  volatiles.FocusEnergy = false;
2709
- try log.end(ident, .FocusEnergy);
2672
+ try log.end(.{ ident, .FocusEnergySilent });
2710
2673
  }
2711
2674
  if (volatiles.LeechSeed) {
2712
2675
  volatiles.LeechSeed = false;
2713
- try log.end(ident, .LeechSeed);
2676
+ try log.end(.{ ident, .LeechSeedSilent });
2714
2677
  }
2715
2678
  if (!showdown and volatiles.Toxic) {
2716
2679
  volatiles.Toxic = false;
@@ -2718,22 +2681,22 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2718
2681
  }
2719
2682
  if (volatiles.LightScreen) {
2720
2683
  volatiles.LightScreen = false;
2721
- try log.end(ident, .LightScreen);
2684
+ try log.end(.{ ident, .LightScreenSilent });
2722
2685
  }
2723
2686
  if (volatiles.Reflect) {
2724
2687
  volatiles.Reflect = false;
2725
- try log.end(ident, .Reflect);
2688
+ try log.end(.{ ident, .ReflectSilent });
2726
2689
  }
2727
2690
  if (showdown and volatiles.Toxic) {
2728
2691
  volatiles.Toxic = false;
2729
2692
  // Pokémon Showdown erroneously clears the toxic counter
2730
2693
  volatiles.toxic = 0;
2731
- try log.end(ident, .Toxic);
2694
+ try log.end(.{ ident, .ToxicSilent });
2732
2695
  }
2733
2696
  }
2734
2697
 
2735
2698
  pub const Rolls = struct {
2736
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2699
+ fn speedTie(battle: anytype, options: anytype) !bool {
2737
2700
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2738
2701
  player == .P1
2739
2702
  else if (showdown)
@@ -2745,7 +2708,7 @@ pub const Rolls = struct {
2745
2708
  return p1;
2746
2709
  }
2747
2710
 
2748
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2711
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2749
2712
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2750
2713
  val == .true
2751
2714
  else if (showdown)
@@ -2757,7 +2720,7 @@ pub const Rolls = struct {
2757
2720
  return crit;
2758
2721
  }
2759
2722
 
2760
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2723
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2761
2724
  const roll = if (options.calc.overridden(player, .damage)) |val|
2762
2725
  val
2763
2726
  else roll: {
@@ -2773,7 +2736,7 @@ pub const Rolls = struct {
2773
2736
  return roll;
2774
2737
  }
2775
2738
 
2776
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2739
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2777
2740
  const ok = if (options.calc.overridden(player, .hit)) |val|
2778
2741
  val == .true
2779
2742
  else if (showdown)
@@ -2785,7 +2748,7 @@ pub const Rolls = struct {
2785
2748
  return ok;
2786
2749
  }
2787
2750
 
2788
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2751
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2789
2752
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2790
2753
  val == .true
2791
2754
  else if (showdown)
@@ -2797,7 +2760,7 @@ pub const Rolls = struct {
2797
2760
  return cfz;
2798
2761
  }
2799
2762
 
2800
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2801
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2802
2765
  val == .true
2803
2766
  else if (showdown)
@@ -2809,7 +2772,7 @@ pub const Rolls = struct {
2809
2772
  return par;
2810
2773
  }
2811
2774
 
2812
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2775
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2813
2776
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2814
2777
  val == .true
2815
2778
  else if (showdown)
@@ -2821,7 +2784,7 @@ pub const Rolls = struct {
2821
2784
  return proc;
2822
2785
  }
2823
2786
 
2824
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2787
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2825
2788
  const rate: u8 = if (low) 26 else 77;
2826
2789
 
2827
2790
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2835,7 +2798,7 @@ pub const Rolls = struct {
2835
2798
  return proc;
2836
2799
  }
2837
2800
 
2838
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2801
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2839
2802
  const rate: u8 = if (low) 52 else 103;
2840
2803
 
2841
2804
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2849,7 +2812,7 @@ pub const Rolls = struct {
2849
2812
  return proc;
2850
2813
  }
2851
2814
 
2852
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2815
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2853
2816
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2854
2817
  val == .true
2855
2818
  else if (showdown)
@@ -2861,14 +2824,12 @@ pub const Rolls = struct {
2861
2824
  return proc;
2862
2825
  }
2863
2826
 
2864
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2827
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2865
2828
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2866
2829
  val
2867
- else if (showdown) move: {
2868
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2869
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2870
- break :move @enumFromInt(r + mod);
2871
- } else move: {
2830
+ else if (showdown)
2831
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
+ else move: {
2872
2833
  while (true) {
2873
2834
  const r = battle.rng.next();
2874
2835
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2881,7 +2842,7 @@ pub const Rolls = struct {
2881
2842
  return move;
2882
2843
  }
2883
2844
 
2884
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2845
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2885
2846
  const power = if (options.calc.overridden(player, .psywave)) |val|
2886
2847
  val - 1
2887
2848
  else if (showdown)
@@ -2898,11 +2859,11 @@ pub const Rolls = struct {
2898
2859
  return power;
2899
2860
  }
2900
2861
 
2901
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2862
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2902
2863
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2903
2864
  @intCast(val)
2904
2865
  else if (showdown)
2905
- @intCast(battle.rng.range(u8, 1, 8))
2866
+ battle.rng.range(u3, 1, 8)
2906
2867
  else duration: {
2907
2868
  while (true) {
2908
2869
  const r = battle.rng.next() & 7;
@@ -2915,11 +2876,11 @@ pub const Rolls = struct {
2915
2876
  return duration;
2916
2877
  }
2917
2878
 
2918
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2879
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2919
2880
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2920
2881
  val
2921
2882
  else if (showdown)
2922
- @intCast(battle.rng.range(u8, 1, 9))
2883
+ battle.rng.range(u4, 1, 9)
2923
2884
  else
2924
2885
  @intCast((battle.rng.next() & 7) + 1);
2925
2886
 
@@ -2928,11 +2889,11 @@ pub const Rolls = struct {
2928
2889
  return duration;
2929
2890
  }
2930
2891
 
2931
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2892
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2932
2893
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2933
2894
  @intCast(val)
2934
2895
  else if (showdown)
2935
- @intCast(battle.rng.range(u8, 2, 6))
2896
+ battle.rng.range(u3, 2, 6)
2936
2897
  else
2937
2898
  @intCast((battle.rng.next() & 3) + 2);
2938
2899
 
@@ -2941,11 +2902,11 @@ pub const Rolls = struct {
2941
2902
  return duration;
2942
2903
  }
2943
2904
 
2944
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2945
2906
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2946
2907
  @intCast(val)
2947
2908
  else if (showdown)
2948
- @intCast(battle.rng.range(u4, 2, 4))
2909
+ battle.rng.range(u3, 2, 4)
2949
2910
  else
2950
2911
  @intCast((battle.rng.next() & 1) + 2);
2951
2912
 
@@ -2954,7 +2915,9 @@ pub const Rolls = struct {
2954
2915
  return duration;
2955
2916
  }
2956
2917
 
2957
- inline fn distribution(
2918
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2919
+
2920
+ fn distribution(
2958
2921
  battle: anytype,
2959
2922
  comptime effect: Move.Effect,
2960
2923
  player: Player,
@@ -2964,7 +2927,7 @@ pub const Rolls = struct {
2964
2927
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2965
2928
  @intCast(val)
2966
2929
  else if (showdown)
2967
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2930
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2968
2931
  else n: {
2969
2932
  const r = (battle.rng.next() & 3);
2970
2933
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3011,10 +2974,9 @@ pub const Rolls = struct {
3011
2974
  while (i < moves.len and r > 0) : (i += 1) {
3012
2975
  if (moves[i].pp > 0) {
3013
2976
  r -= 1;
3014
- if (r == 0) break;
2977
+ if (r == 0) break :slot @intCast(i + 1);
3015
2978
  }
3016
2979
  }
3017
- break :slot @intCast(i + 1);
3018
2980
  }
3019
2981
  }
3020
2982
 
@@ -3036,11 +2998,11 @@ test "RNG agreement" {
3036
2998
  expected[i] = @intCast(i * 0x1000000);
3037
2999
  }
3038
3000
 
3039
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3040
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3041
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3042
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- 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 };
3044
3006
 
3045
3007
  for (0..expected.len) |i| {
3046
3008
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3130,7 +3092,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3130
3092
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3131
3093
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3132
3094
  const struggle =
3133
- 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);
3134
3096
  const s = if (struggle) 0 else slot;
3135
3097
  out[n] = .{ .type = .Move, .data = s };
3136
3098
  n += 1;
@@ -3145,7 +3107,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3145
3107
  const m = active.moves[slot - 1];
3146
3108
  if (m.id == .None) break;
3147
3109
  if (m.pp == 0) continue;
3148
- if (active.volatiles.disabled_move == slot) continue;
3110
+ if (active.volatiles.disable_move == slot) continue;
3149
3111
  out[n] = .{ .type = .Move, .data = slot };
3150
3112
  n += 1;
3151
3113
  }