@pkmn/engine 0.1.0-dev.305a932f → 0.1.0-dev.30c63015

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.
@@ -7,24 +7,27 @@ 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;
24
+ const Cant = protocol.Cant;
25
+ const CureStatus = protocol.CureStatus;
26
26
  const Damage = protocol.Damage;
27
+ const End = protocol.End;
28
+ const Fail = protocol.Fail;
27
29
  const Heal = protocol.Heal;
30
+ const Immune = protocol.Immune;
28
31
 
29
32
  const Gen12 = rng.Gen12;
30
33
 
@@ -59,19 +62,15 @@ const BOOSTS = &[_][2]u8{
59
62
 
60
63
  const MAX_STAT_VALUE = 999;
61
64
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
65
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
66
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
66
67
  if (battle.turn == 0) return start(battle, options);
67
68
 
68
69
  var s1 = false;
69
70
  var s2 = false;
70
- var f1: ?Move = null;
71
- var f2: ?Move = null;
72
71
 
73
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
74
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
73
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
75
74
 
76
75
  var p1 = battle.side(.P1);
77
76
  var p2 = battle.side(.P2);
@@ -80,9 +79,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
80
79
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
81
80
 
82
81
  if (try turnOrder(battle, c1, c2, options) == .P1) {
83
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
82
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
84
83
  } else {
85
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
84
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
86
85
  }
87
86
 
88
87
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -120,7 +119,6 @@ fn selectMove(
120
119
  choice: Choice,
121
120
  foe_choice: Choice,
122
121
  skip_turn: *bool,
123
- from: *?Move,
124
122
  ) ?Result {
125
123
  if (choice.type == .Pass) return null;
126
124
 
@@ -137,7 +135,6 @@ fn selectMove(
137
135
  return null;
138
136
  }
139
137
  if (volatiles.Rage) {
140
- from.* = side.last_used_move;
141
138
  if (showdown) {
142
139
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
143
140
  saveMove(battle, player, null);
@@ -147,7 +144,6 @@ fn selectMove(
147
144
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
148
145
  if (!showdown) volatiles.Flinch = false;
149
146
  if (volatiles.Thrashing or volatiles.Charging) {
150
- from.* = side.last_selected_move;
151
147
  if (showdown) {
152
148
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
153
149
  saveMove(battle, player, null);
@@ -168,9 +164,7 @@ fn selectMove(
168
164
  return null;
169
165
  }
170
166
  if (volatiles.Binding) {
171
- from.* = side.last_used_move;
172
167
  if (showdown) {
173
- if (foe_choice.type == .Switch) from.* = null;
174
168
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
175
169
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
176
170
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -206,7 +200,7 @@ fn selectMove(
206
200
  if (choice.data == 0) {
207
201
  const struggle = ok: {
208
202
  for (side.active.moves, 0..) |move, i| {
209
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
203
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
210
204
  }
211
205
  break :ok true;
212
206
  };
@@ -225,7 +219,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
225
219
  if (c.data == 0) {
226
220
  side.last_selected_move = .Struggle;
227
221
  } else {
228
- assert(showdown or side.active.volatiles.disabled_move != c.data);
222
+ assert(showdown or side.active.volatiles.disable_move != c.data);
229
223
  const move = side.active.move(c.data);
230
224
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
231
225
  // sometimes required...), but a 0 PP move can be used automatically
@@ -266,14 +260,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
266
260
 
267
261
  foe.active.volatiles.Binding = false;
268
262
 
269
- try options.log.switched(battle.active(player), incoming);
263
+ try options.log.switched(.{ battle.active(player), incoming });
270
264
  options.chance.switched(player, side.order[0], out);
271
265
 
272
266
  if (showdown and incoming.status == Status.TOX) {
273
267
  incoming.status = Status.init(.PSN);
274
268
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
275
269
  // rather not clutter up turnOrder just for this (incorrect) log message
276
- try options.log.status(battle.active(player), incoming.status, .Silent);
270
+ try options.log.status(.{ battle.active(player), incoming.status, protocol.Status.Silent });
277
271
  }
278
272
  }
279
273
 
@@ -336,12 +330,10 @@ fn doTurn(
336
330
  player_choice: Choice,
337
331
  player_rewrap: bool,
338
332
  player_skip: bool,
339
- player_from: ?Move,
340
333
  foe_player: Player,
341
334
  foe_choice: Choice,
342
335
  foe_rewrap: bool,
343
336
  foe_skip: bool,
344
- foe_from: ?Move,
345
337
  options: anytype,
346
338
  ) !?Result {
347
339
  assert(player_choice.type != .Pass);
@@ -354,7 +346,6 @@ fn doTurn(
354
346
  player_choice,
355
347
  player_rewrap,
356
348
  player_skip,
357
- player_from,
358
349
  &residual,
359
350
  options,
360
351
  )) |r| return r;
@@ -377,7 +368,6 @@ fn doTurn(
377
368
  foe_choice,
378
369
  foe_rewrap,
379
370
  foe_skip,
380
- foe_from,
381
371
  &residual,
382
372
  options,
383
373
  )) |r| return r;
@@ -405,7 +395,6 @@ fn executeMove(
405
395
  choice: Choice,
406
396
  rewrap: bool,
407
397
  skip: bool,
408
- from: ?Move,
409
398
  residual: *bool,
410
399
  options: anytype,
411
400
  ) !?Result {
@@ -422,7 +411,7 @@ fn executeMove(
422
411
  if (side.last_selected_move == .SKIP_TURN) {
423
412
  assert(!showdown);
424
413
  if (battle.foe(player).active.volatiles.Binding) {
425
- try options.log.cant(battle.active(player), .Bound);
414
+ try options.log.cant(.{ battle.active(player), Cant.Bound });
426
415
  }
427
416
  return null;
428
417
  }
@@ -440,8 +429,10 @@ fn executeMove(
440
429
  mslot = @intCast(battle.lastMove(player).index);
441
430
  const stored = side.stored();
442
431
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
443
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
444
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
+ auto = isForced(&side.active) or
433
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
434
+ side.last_selected_move == .HyperBeam or
435
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
445
436
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
446
437
  // thawed, in which case we will desync unless the last_selected_move happened to be at
447
438
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -463,7 +454,7 @@ fn executeMove(
463
454
 
464
455
  var skip_can = false;
465
456
  var skip_pp = false;
466
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
457
+ switch (try beforeMove(battle, player, skip, residual, options)) {
467
458
  .done => return null,
468
459
  .skip_can => skip_can = true,
469
460
  .skip_pp => skip_pp = true,
@@ -471,9 +462,9 @@ fn executeMove(
471
462
  .err => return @as(?Result, Result.Error),
472
463
  }
473
464
 
474
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
475
- return null;
476
- }
465
+ const can = skip_can or
466
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
467
+ if (!can) return null;
477
468
 
478
469
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
479
470
  }
@@ -484,7 +475,6 @@ fn beforeMove(
484
475
  battle: anytype,
485
476
  player: Player,
486
477
  skip: bool,
487
- from: ?Move,
488
478
  residual: *bool,
489
479
  options: anytype,
490
480
  ) !BeforeMove {
@@ -511,23 +501,23 @@ fn beforeMove(
511
501
  }
512
502
 
513
503
  if (duration == 0) {
514
- try log.curestatus(ident, before, .Message);
504
+ try log.curestatus(.{ ident, before, CureStatus.Message });
515
505
  stored.status = 0; // clears EXT if present
516
506
  } else {
517
- try log.cant(ident, .Sleep);
507
+ try log.cant(.{ ident, Cant.Sleep });
518
508
  }
519
509
  side.last_used_move = .None;
520
510
  return .done;
521
511
  }
522
512
 
523
513
  if (Status.is(stored.status, .FRZ)) {
524
- try log.cant(ident, .Freeze);
514
+ try log.cant(.{ ident, Cant.Freeze });
525
515
  side.last_used_move = .None;
526
516
  return .done;
527
517
  }
528
518
 
529
519
  if (skip or foe.active.volatiles.Binding) {
530
- try log.cant(ident, .Bound);
520
+ try log.cant(.{ ident, Cant.Bound });
531
521
  return .done;
532
522
  }
533
523
 
@@ -535,27 +525,27 @@ fn beforeMove(
535
525
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
536
526
  // Pokémon can sometimes flinch multiple times from the same original hit
537
527
  if (!showdown) volatiles.Flinch = false;
538
- try log.cant(ident, .Flinch);
528
+ try log.cant(.{ ident, Cant.Flinch });
539
529
  return .done;
540
530
  }
541
531
 
542
532
  if (volatiles.Recharging) {
543
533
  volatiles.Recharging = false;
544
- try log.cant(ident, .Recharge);
534
+ try log.cant(.{ ident, Cant.Recharge });
545
535
  return .done;
546
536
  }
547
537
 
548
- if (volatiles.disabled_duration > 0) {
538
+ if (volatiles.disable_duration > 0) {
549
539
  if (options.calc.modify(player, .disable)) |extend| {
550
- if (!extend) volatiles.disabled_duration = 0;
540
+ if (!extend) volatiles.disable_duration = 0;
551
541
  } else {
552
- volatiles.disabled_duration -= 1;
542
+ volatiles.disable_duration -= 1;
553
543
  }
554
- try options.chance.disable(player, volatiles.disabled_duration);
544
+ try options.chance.disable(player, volatiles.disable_duration);
555
545
 
556
- if (volatiles.disabled_duration == 0) {
557
- volatiles.disabled_move = 0;
558
- try log.end(ident, .Disable);
546
+ if (volatiles.disable_duration == 0) {
547
+ volatiles.disable_move = 0;
548
+ try log.end(.{ ident, End.Disable });
559
549
  }
560
550
  }
561
551
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -580,7 +570,7 @@ fn beforeMove(
580
570
 
581
571
  if (volatiles.confusion == 0) {
582
572
  volatiles.Confusion = false;
583
- try log.end(ident, .Confusion);
573
+ try log.end(.{ ident, End.Confusion });
584
574
  } else {
585
575
  try log.activate(ident, .Confusion);
586
576
 
@@ -633,7 +623,7 @@ fn beforeMove(
633
623
  volatiles.Binding = false;
634
624
  options.chance.clearDurations(player, null);
635
625
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
636
- try log.cant(ident, .Paralysis);
626
+ try log.cant(.{ ident, Cant.Paralysis });
637
627
  return .done;
638
628
  }
639
629
 
@@ -664,13 +654,13 @@ fn beforeMove(
664
654
  }
665
655
 
666
656
  volatiles.Bide = false;
667
- try log.end(ident, .Bide);
657
+ try log.end(.{ ident, End.Bide });
668
658
 
669
659
  battle.last_damage = volatiles.state *% 2;
670
660
  volatiles.state = 0;
671
661
 
672
662
  if (battle.last_damage == 0) {
673
- try log.fail(ident, .None);
663
+ try log.fail(.{ ident, Fail.None });
674
664
  return .done;
675
665
  }
676
666
 
@@ -691,7 +681,6 @@ fn beforeMove(
691
681
 
692
682
  if (volatiles.Thrashing) {
693
683
  assert(volatiles.attacks > 0);
694
- assert(from != null);
695
684
 
696
685
  if (options.calc.modify(player, .attacking)) |extend| {
697
686
  if (!extend) volatiles.attacks = 0;
@@ -703,7 +692,7 @@ fn beforeMove(
703
692
  if (!showdown and handleThrashing(battle, active, player, options)) {
704
693
  try log.start(battle.active(player), .ConfusionSilent);
705
694
  }
706
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
695
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
707
696
  if (showdown) {
708
697
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
709
698
  // used in useMove and doesn't have the notion of skipping canMove semantics
@@ -725,12 +714,7 @@ fn beforeMove(
725
714
  }
726
715
  try options.chance.binding(player, volatiles.attacks);
727
716
 
728
- try log.move(
729
- battle.active(player),
730
- side.last_selected_move,
731
- battle.active(player.foe()),
732
- side.last_selected_move,
733
- );
717
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
734
718
  if (showdown or battle.last_damage != 0) {
735
719
  const sub = showdown and foe.active.volatiles.Substitute;
736
720
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -750,7 +734,7 @@ fn canMove(
750
734
  mslot: u4,
751
735
  auto: bool,
752
736
  skip_pp: bool,
753
- from: ?Move,
737
+ from: Move,
754
738
  residual: *bool,
755
739
  options: anytype,
756
740
  ) !bool {
@@ -758,33 +742,22 @@ fn canMove(
758
742
  const player_ident = battle.active(player);
759
743
  const move = Move.get(side.last_selected_move);
760
744
 
761
- const foe_last = battle.foe(player).last_used_move;
762
- const special = from != null and (from.? == .Metronome and
763
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
764
-
765
745
  var skip = skip_pp;
766
746
  if (side.active.volatiles.Charging) {
767
747
  side.active.volatiles.Charging = false;
768
748
  side.active.volatiles.Invulnerable = false;
769
749
  } else if (move.effect == .Charge) {
770
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
750
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
771
751
  setCounterable(battle, player, side, move);
772
752
  try Effects.charge(battle, player, options);
773
753
  return false;
774
754
  }
775
755
 
776
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
756
+ side.last_used_move = side.last_selected_move;
777
757
  if (!skip) decrementPP(side, mslot, auto);
778
758
 
779
759
  const target = if (move.target == .Self) player else player.foe();
780
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
781
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
782
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
783
- // meaning sometimes we lose this information. from is still set because it not being null is
784
- // relevant for control flow, but we don't have anything useful to put in the log. This is
785
- // regrettable, but this information is more "nice to have" than required
786
- const f = if (from != null and from.? == .None) null else from;
787
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
760
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
788
761
  setCounterable(battle, player, side, move);
789
762
 
790
763
  if (move.effect.onBegin()) {
@@ -822,12 +795,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
822
795
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
823
796
  if (volatiles.Bide) return;
824
797
 
825
- assert(active.move(mslot).pp > 0 or auto);
826
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
798
+ var move_slot = active.move(mslot);
799
+ assert(move_slot.pp > 0 or auto);
800
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
827
801
  if (volatiles.Transform) return;
828
802
 
829
- assert(side.stored().move(mslot).pp > 0 or auto);
830
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
803
+ move_slot = side.stored().move(mslot);
804
+ assert(move_slot.pp > 0 or auto);
805
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
831
806
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
832
807
  }
833
808
 
@@ -959,16 +934,16 @@ fn doMove(
959
934
  ohko = (!showdown or (!immune and !invulnerable)) and
960
935
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
961
936
  if (ohko) {
962
- try log.immune(foe_ident, .OHKO);
937
+ try log.immune(.{ foe_ident, Immune.OHKO });
963
938
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
964
- try log.immune(foe_ident, .None);
939
+ try log.immune(.{ foe_ident, Immune.None });
965
940
  } else if (mist) {
966
941
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
967
- try log.fail(foe_ident, .None);
942
+ try log.fail(.{ foe_ident, Fail.None });
968
943
  } else {
969
944
  if (showdown or !zero) try options.chance.commit(player, .miss);
970
- try log.lastmiss();
971
- try log.miss(battle.active(player));
945
+ try log.lastmiss(.{});
946
+ try log.miss(.{battle.active(player)});
972
947
  }
973
948
  if (move.effect == .JumpKick) {
974
949
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1004,15 +979,15 @@ fn doMove(
1004
979
  var hit: u4 = 0;
1005
980
  while (hit < hits) {
1006
981
  if (hit == 0) {
1007
- if (crit) try log.crit(battle.active(player.foe()));
982
+ if (crit) try log.crit(.{battle.active(player.foe())});
1008
983
  if (effectiveness > Effectiveness.neutral) {
1009
- try log.supereffective(battle.active(player.foe()));
984
+ try log.supereffective(.{battle.active(player.foe())});
1010
985
  } else if (effectiveness < Effectiveness.neutral) {
1011
- try log.resisted(battle.active(player.foe()));
986
+ try log.resisted(.{battle.active(player.foe())});
1012
987
  }
1013
988
  }
1014
989
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1015
- if (hit == 0 and ohko) try log.ohko();
990
+ if (hit == 0 and ohko) try log.ohko(.{});
1016
991
  hit += 1;
1017
992
  if (foe.stored().hp == 0) break;
1018
993
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1029,7 +1004,7 @@ fn doMove(
1029
1004
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1030
1005
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1031
1006
  }
1032
- try log.hitcount(battle.active(player.foe()), hit);
1007
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1033
1008
  } else if (showdown) {
1034
1009
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1035
1010
  if (move.effect == .Binding) {
@@ -1041,7 +1016,7 @@ fn doMove(
1041
1016
  if (sub) {
1042
1017
  try log.activate(battle.active(player.foe()), .Substitute);
1043
1018
  } else {
1044
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1019
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), Damage.None });
1045
1020
  try buildRage(battle, player.foe(), options);
1046
1021
  }
1047
1022
  return null;
@@ -1239,7 +1214,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1239
1214
  const foe = battle.foe(player);
1240
1215
 
1241
1216
  if (battle.last_damage == 0) {
1242
- try options.log.fail(battle.active(player), .None);
1217
+ try options.log.fail(.{ battle.active(player), Fail.None });
1243
1218
  return null;
1244
1219
  }
1245
1220
 
@@ -1257,14 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1257
1232
  foe_last_selected_move.type == .Fighting);
1258
1233
 
1259
1234
  if (!used and !selected) {
1260
- try options.log.fail(battle.active(player), .None);
1235
+ try options.log.fail(.{ battle.active(player), Fail.None });
1261
1236
  return null;
1262
1237
  }
1263
1238
 
1264
1239
  if (!used or !selected) {
1265
1240
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1266
1241
  if (!showdown) return Result.Error;
1267
- try options.log.fail(battle.active(player), .None);
1242
+ try options.log.fail(.{ battle.active(player), Fail.None });
1268
1243
  return null;
1269
1244
  }
1270
1245
 
@@ -1300,7 +1275,7 @@ fn applyDamage(
1300
1275
  subbed.active.volatiles.substitute = 0;
1301
1276
  subbed.active.volatiles.Substitute = false;
1302
1277
  // battle.last_damage is not updated with the amount of HP the Substitute had
1303
- try options.log.end(battle.active(sub_player), .Substitute);
1278
+ try options.log.end(.{ battle.active(sub_player), End.Substitute });
1304
1279
  options.calc.capped(target_player);
1305
1280
  return true;
1306
1281
  } else {
@@ -1313,7 +1288,7 @@ fn applyDamage(
1313
1288
 
1314
1289
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1315
1290
  target.stored().hp -= battle.last_damage;
1316
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1291
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1317
1292
  return false;
1318
1293
  }
1319
1294
 
@@ -1332,7 +1307,7 @@ fn mirrorMove(
1332
1307
  side.last_selected_move = foe.last_used_move;
1333
1308
 
1334
1309
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1335
- try options.log.fail(battle.active(player), .None);
1310
+ try options.log.fail(.{ battle.active(player), Fail.None });
1336
1311
  return null;
1337
1312
  } else if (!showdown or foe.last_used_move != .Struggle) {
1338
1313
  incrementPP(side, mslot);
@@ -1375,11 +1350,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1350
  assert(!showdown);
1376
1351
  const foe_ident = battle.active(player.foe());
1377
1352
  try options.log.activate(foe_ident, .Mist);
1378
- try options.log.fail(foe_ident, .None);
1353
+ try options.log.fail(.{ foe_ident, Fail.None });
1379
1354
  } else {
1380
1355
  try options.chance.commit(player, .miss);
1381
- try options.log.lastmiss();
1382
- try options.log.miss(battle.active(player));
1356
+ try options.log.lastmiss(.{});
1357
+ try options.log.miss(.{battle.active(player)});
1383
1358
  }
1384
1359
 
1385
1360
  return false;
@@ -1470,9 +1445,8 @@ fn checkFaint(
1470
1445
  const foe_fainted = foe.stored().hp == 0;
1471
1446
 
1472
1447
  const player_out = findFirstAlive(side) == 0;
1473
- const foe_out = findFirstAlive(foe) == 0;
1474
- const tie = player_out and foe_out;
1475
- const more = tie or player_out or foe_out;
1448
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1449
+ const more = player_out or foe_out;
1476
1450
 
1477
1451
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1478
1452
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1480,14 +1454,14 @@ fn checkFaint(
1480
1454
  assert(!side.active.volatiles.MultiHit);
1481
1455
  assert(!foe.active.volatiles.MultiHit);
1482
1456
 
1483
- if (tie) {
1484
- try options.log.tie();
1457
+ if (player_out and foe_out) {
1458
+ try options.log.tie(.{});
1485
1459
  return Result.Tie;
1486
1460
  } else if (player_out) {
1487
- try options.log.win(player.foe());
1461
+ try options.log.win(.{player.foe()});
1488
1462
  return if (player == .P1) Result.Lose else Result.Win;
1489
1463
  } else if (foe_out) {
1490
- try options.log.win(player);
1464
+ try options.log.win(.{player});
1491
1465
  return if (player == .P1) Result.Win else Result.Lose;
1492
1466
  }
1493
1467
 
@@ -1529,7 +1503,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1529
1503
  side.stored().stats.spe;
1530
1504
  }
1531
1505
 
1532
- try options.log.faint(battle.active(player), done);
1506
+ try options.log.faint(.{ battle.active(player), done });
1533
1507
  options.calc.capped(player);
1534
1508
  return null;
1535
1509
  }
@@ -1551,8 +1525,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1551
1525
 
1552
1526
  if (!showdown or damage > 0) {
1553
1527
  stored.hp -= damage;
1554
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1555
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1528
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1529
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1556
1530
  }
1557
1531
  }
1558
1532
 
@@ -1577,14 +1551,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1577
1551
  const amount = @min(damage, stored.hp);
1578
1552
  stored.hp -= amount;
1579
1553
 
1580
- // As above, Pokémon Showdown uses damageOf but its not relevant
1581
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1554
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1555
+ if (amount > 0) try options.log.damage(.{ ident, stored, Damage.LeechSeed });
1582
1556
 
1583
1557
  const before = foe_stored.hp;
1584
1558
  // Uncapped damage is added back to the foe
1585
1559
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1586
1560
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1587
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1561
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, Heal.Silent });
1588
1562
  }
1589
1563
  }
1590
1564
 
@@ -1595,18 +1569,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1595
1569
  battle.turn += 1;
1596
1570
 
1597
1571
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1598
- try options.log.tie();
1572
+ try options.log.tie(.{});
1599
1573
  return Result.Tie;
1600
1574
  }
1601
1575
 
1602
1576
  if (showdown and battle.turn >= 1000) {
1603
- try options.log.tie();
1577
+ try options.log.tie(.{});
1604
1578
  return Result.Tie;
1605
1579
  } else if (battle.turn >= 65535) {
1606
1580
  return Result.Error;
1607
1581
  }
1608
1582
 
1609
- try options.log.turn(battle.turn);
1583
+ try options.log.turn(.{battle.turn});
1610
1584
 
1611
1585
  return Result.Default;
1612
1586
  }
@@ -1666,9 +1640,9 @@ fn checkEBC(battle: anytype) bool {
1666
1640
  }
1667
1641
 
1668
1642
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1669
- if (side.active.volatiles.disabled_move != 0) {
1643
+ if (side.active.volatiles.disable_move != 0) {
1670
1644
  // A Pokémon that transforms after being disabled may end up with less move slots
1671
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1645
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1672
1646
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1673
1647
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1674
1648
  m
@@ -1676,14 +1650,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1676
1650
  side.last_selected_move;
1677
1651
  if (m != .None and m == last) {
1678
1652
  side.active.volatiles.Charging = false;
1679
- try options.log.disabled(ident, last);
1653
+ try options.log.cant(.{ ident, Cant.Disable, last });
1680
1654
  return true;
1681
1655
  }
1682
1656
  }
1683
1657
  return false;
1684
1658
  }
1685
1659
 
1686
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1660
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1687
1661
  const side = battle.side(who);
1688
1662
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1689
1663
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1701,7 +1675,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1701
1675
  return true;
1702
1676
  }
1703
1677
 
1704
- inline fn onBegin(
1678
+ fn onBegin(
1705
1679
  battle: anytype,
1706
1680
  player: Player,
1707
1681
  move: Move.Data,
@@ -1731,7 +1705,7 @@ inline fn onBegin(
1731
1705
  };
1732
1706
  }
1733
1707
 
1734
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1708
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1735
1709
  assert(move.effect.onEnd());
1736
1710
  return switch (move.effect) {
1737
1711
  .Bide => Effects.bide(battle, player, options),
@@ -1747,7 +1721,7 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1747
1721
  };
1748
1722
  }
1749
1723
 
1750
- inline fn alwaysHappens(
1724
+ fn alwaysHappens(
1751
1725
  battle: anytype,
1752
1726
  player: Player,
1753
1727
  move: Move.Data,
@@ -1766,7 +1740,7 @@ inline fn alwaysHappens(
1766
1740
  };
1767
1741
  }
1768
1742
 
1769
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1743
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1770
1744
  return switch (move.effect) {
1771
1745
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1772
1746
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1807,11 +1781,11 @@ pub const Effects = struct {
1807
1781
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1808
1782
  if (Status.is(foe_stored.status, .FRZ)) {
1809
1783
  assert(move.type == .Fire);
1810
- try options.log.curestatus(
1784
+ try options.log.curestatus(.{
1811
1785
  battle.active(player.foe()),
1812
1786
  foe_stored.status,
1813
- .Message,
1814
- );
1787
+ CureStatus.Message,
1788
+ });
1815
1789
  foe_stored.status = 0;
1816
1790
  }
1817
1791
  return;
@@ -1825,7 +1799,8 @@ pub const Effects = struct {
1825
1799
  foe_stored.status = Status.init(.BRN);
1826
1800
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1827
1801
 
1828
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1802
+ const reason: protocol.Status = .None;
1803
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
1829
1804
  }
1830
1805
 
1831
1806
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1835,8 +1810,8 @@ pub const Effects = struct {
1835
1810
  volatiles.Charging = true;
1836
1811
  const move = side.last_selected_move;
1837
1812
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1838
- try options.log.laststill();
1839
- try options.log.prepare(battle.active(player), move);
1813
+ try options.log.laststill(.{});
1814
+ try options.log.prepare(.{ battle.active(player), move });
1840
1815
  }
1841
1816
 
1842
1817
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1850,11 +1825,11 @@ pub const Effects = struct {
1850
1825
  if (!try checkHit(battle, player, move, options)) {
1851
1826
  return;
1852
1827
  } else if (sub) {
1853
- return options.log.fail(battle.active(player.foe()), .None);
1828
+ return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1854
1829
  }
1855
1830
  } else {
1856
1831
  if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1832
+ return options.log.fail(.{ battle.active(player.foe()), Fail.None });
1858
1833
  } else if (!try checkHit(battle, player, move, options)) {
1859
1834
  return;
1860
1835
  }
@@ -1873,8 +1848,8 @@ pub const Effects = struct {
1873
1848
  const foe = battle.foe(player);
1874
1849
 
1875
1850
  if (foe.active.volatiles.Invulnerable) {
1876
- try options.log.lastmiss();
1877
- return options.log.miss(battle.active(player));
1851
+ try options.log.lastmiss(.{});
1852
+ return options.log.miss(.{battle.active(player)});
1878
1853
  }
1879
1854
 
1880
1855
  battle.side(player).active.types = foe.active.types;
@@ -1896,8 +1871,8 @@ pub const Effects = struct {
1896
1871
  try options.chance.commit(player, .hit);
1897
1872
  }
1898
1873
 
1899
- if (volatiles.disabled_move != 0) {
1900
- try options.log.fail(foe_ident, .None);
1874
+ if (volatiles.disable_move != 0) {
1875
+ try options.log.fail(.{ foe_ident, Fail.None });
1901
1876
  return null;
1902
1877
  }
1903
1878
 
@@ -1913,7 +1888,7 @@ pub const Effects = struct {
1913
1888
  // Technically this is still considered simply a "miss" on the cartridge,
1914
1889
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1915
1890
  if (n == 0) {
1916
- try options.log.fail(foe_ident, .None);
1891
+ try options.log.fail(.{ foe_ident, Fail.None });
1917
1892
  return null;
1918
1893
  } else if (err) {
1919
1894
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1921,18 +1896,18 @@ pub const Effects = struct {
1921
1896
  return Result.Error;
1922
1897
  }
1923
1898
 
1924
- volatiles.disabled_move =
1899
+ volatiles.disable_move =
1925
1900
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1926
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1901
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1927
1902
 
1928
- const id = foe.active.move(volatiles.disabled_move).id;
1903
+ const id = foe.active.move(volatiles.disable_move).id;
1929
1904
  try options.log.startEffect(foe_ident, .Disable, id);
1930
1905
  return null;
1931
1906
  }
1932
1907
 
1933
1908
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1934
- var side = battle.side(player);
1935
- var stored = side.stored();
1909
+ var stored = battle.side(player).stored();
1910
+ const ident = battle.active(player);
1936
1911
 
1937
1912
  if (battle.last_damage == 0) {
1938
1913
  assert(showdown);
@@ -1945,7 +1920,7 @@ pub const Effects = struct {
1945
1920
  if (stored.hp == stored.stats.hp) return;
1946
1921
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1947
1922
 
1948
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1923
+ try options.log.heal(.{ ident, stored, Heal.Drain, battle.active(player.foe()) });
1949
1924
  }
1950
1925
 
1951
1926
  fn explode(battle: anytype, player: Player) !void {
@@ -1999,7 +1974,7 @@ pub const Effects = struct {
1999
1974
  foe_stored.status = Status.init(.FRZ);
2000
1975
  // GLITCH: Hyper Beam recharging status is not cleared
2001
1976
 
2002
- try options.log.status(foe_ident, foe_stored.status, .None);
1977
+ try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2003
1978
  }
2004
1979
 
2005
1980
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2020,7 +1995,7 @@ pub const Effects = struct {
2020
1995
  foe.active.stats = foe_stored.stats;
2021
1996
 
2022
1997
  try log.activate(player_ident, .Haze);
2023
- try log.clearallboost();
1998
+ try log.clearallboost(.{});
2024
1999
 
2025
2000
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2026
2001
  if (showdown) {
@@ -2028,11 +2003,11 @@ pub const Effects = struct {
2028
2003
  const p: Player = @enumFromInt(i);
2029
2004
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2030
2005
  if (p != player and Status.any(s.stored().status)) {
2031
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2006
+ try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2032
2007
  s.stored().status = 0;
2033
2008
  } else if (showdown and s.stored().status == Status.TOX) {
2034
2009
  s.stored().status = Status.init(.PSN);
2035
- try log.status(battle.active(p), s.stored().status, .None);
2010
+ try log.status(.{ battle.active(p), s.stored().status, protocol.Status.None });
2036
2011
  }
2037
2012
  try clearVolatiles(battle, p, p != player, options);
2038
2013
  }
@@ -2041,7 +2016,7 @@ pub const Effects = struct {
2041
2016
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2042
2017
  foe.last_selected_move = .SKIP_TURN;
2043
2018
  }
2044
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2019
+ try log.curestatus(.{ foe_ident, foe_stored.status, CureStatus.Silent });
2045
2020
  foe_stored.status = 0;
2046
2021
  }
2047
2022
  try clearVolatiles(battle, player, false, options);
@@ -2057,7 +2032,7 @@ pub const Effects = struct {
2057
2032
  // GLITCH: HP recovery move failure glitches
2058
2033
  const delta = stored.stats.hp - stored.hp;
2059
2034
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2060
- return try options.log.fail(ident, .None);
2035
+ return options.log.fail(.{ ident, Fail.None });
2061
2036
  }
2062
2037
 
2063
2038
  const rest = side.last_selected_move == .Rest;
@@ -2065,17 +2040,17 @@ pub const Effects = struct {
2065
2040
  // Adding the sleep status runs the sleep condition handler to roll duration
2066
2041
  if (showdown) battle.rng.advance(1);
2067
2042
  stored.status = Status.slf(2);
2068
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2043
+ try options.log.status(.{ ident, stored.status, protocol.Status.From, Move.Rest });
2069
2044
  stored.hp = stored.stats.hp;
2070
2045
  } else {
2071
2046
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2072
2047
  }
2073
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2048
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2074
2049
  }
2075
2050
 
2076
2051
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2077
2052
  battle.side(player).active.volatiles.Recharging = true;
2078
- try options.log.mustrecharge(battle.active(player));
2053
+ try options.log.mustrecharge(.{battle.active(player)});
2079
2054
  }
2080
2055
 
2081
2056
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2084,15 +2059,15 @@ pub const Effects = struct {
2084
2059
  if (showdown) {
2085
2060
  // Invulnerability trumps type immunity on Pokémon Showdown
2086
2061
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2087
- return options.log.immune(battle.active(player.foe()), .None);
2062
+ return options.log.immune(.{ battle.active(player.foe()), Immune.None });
2088
2063
  }
2089
2064
  if (!try checkHit(battle, player, move, options)) return;
2090
2065
  if (foe.active.volatiles.LeechSeed) return;
2091
2066
  } else {
2092
2067
  if (!try checkHit(battle, player, move, options)) return;
2093
2068
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2094
- try options.log.lastmiss();
2095
- return options.log.miss(battle.active(player));
2069
+ try options.log.lastmiss(.{});
2070
+ return options.log.miss(.{battle.active(player)});
2096
2071
  }
2097
2072
  }
2098
2073
 
@@ -2106,7 +2081,7 @@ pub const Effects = struct {
2106
2081
  var side = battle.side(player);
2107
2082
 
2108
2083
  if (side.active.volatiles.LightScreen) {
2109
- return options.log.fail(battle.active(player), .None);
2084
+ return options.log.fail(.{ battle.active(player), Fail.None });
2110
2085
  }
2111
2086
  side.active.volatiles.LightScreen = true;
2112
2087
 
@@ -2139,7 +2114,7 @@ pub const Effects = struct {
2139
2114
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2140
2115
  if (!try checkHit(battle, player, move, options)) return;
2141
2116
  try options.chance.commit(player, .hit);
2142
- return try options.log.fail(battle.active(player.foe()), .None);
2117
+ return options.log.fail(.{ battle.active(player.foe()), Fail.None });
2143
2118
  }
2144
2119
  }
2145
2120
  if (!try checkHit(battle, player, move, options)) return;
@@ -2183,17 +2158,18 @@ pub const Effects = struct {
2183
2158
 
2184
2159
  if (showdown) {
2185
2160
  // Invulnerability trumps type immunity on Pokémon Showdown
2186
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2187
- if (!try checkHit(battle, player, move, options)) return;
2161
+ if (immune and !foe.active.volatiles.Invulnerable) {
2162
+ return log.immune(.{ foe_ident, Immune.None });
2163
+ } else if (!try checkHit(battle, player, move, options)) return;
2188
2164
  }
2189
2165
  if (Status.any(foe_stored.status)) {
2190
- return log.fail(
2166
+ return log.fail(.{
2191
2167
  foe_ident,
2192
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2193
- );
2168
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2169
+ });
2194
2170
  }
2195
2171
  if (!showdown) {
2196
- if (immune) return log.immune(foe_ident, .None);
2172
+ if (immune) return log.immune(.{ foe_ident, Immune.None });
2197
2173
  if (!try checkHit(battle, player, move, options)) return;
2198
2174
  }
2199
2175
  try options.chance.commit(player, .hit);
@@ -2201,7 +2177,7 @@ pub const Effects = struct {
2201
2177
  foe_stored.status = Status.init(.PAR);
2202
2178
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2203
2179
 
2204
- try log.status(foe_ident, foe_stored.status, .None);
2180
+ try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2205
2181
  }
2206
2182
 
2207
2183
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2223,12 +2199,13 @@ pub const Effects = struct {
2223
2199
  foe_stored.status = Status.init(.PAR);
2224
2200
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2225
2201
 
2226
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2202
+ const reason: protocol.Status = .None;
2203
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, reason });
2227
2204
  }
2228
2205
 
2229
2206
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2230
2207
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2231
- try options.log.fieldactivate();
2208
+ try options.log.fieldactivate(.{});
2232
2209
  }
2233
2210
  }
2234
2211
 
@@ -2243,23 +2220,23 @@ pub const Effects = struct {
2243
2220
  return;
2244
2221
  } else if (foe.active.volatiles.Substitute) {
2245
2222
  if (move.effect != .Poison) return;
2246
- return log.fail(foe_ident, .None);
2223
+ return log.fail(.{ foe_ident, Fail.None });
2247
2224
  } else if (Status.any(foe_stored.status)) {
2248
2225
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2249
2226
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2250
2227
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2251
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2228
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2252
2229
  if (!showdown)
2253
- .Poison
2230
+ Fail.Poison
2254
2231
  else if (toxic == (foe_stored.status == Status.TOX))
2255
- if (toxic) .Toxic else .Poison
2232
+ if (toxic) Fail.Toxic else Fail.Poison
2256
2233
  else
2257
- .None
2234
+ Fail.None
2258
2235
  else
2259
- .None);
2236
+ Fail.None });
2260
2237
  } else if (foe.active.types.includes(.Poison)) {
2261
2238
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2262
- return log.immune(foe_ident, .None);
2239
+ return log.immune(.{ foe_ident, Immune.None });
2263
2240
  }
2264
2241
 
2265
2242
  if (move.effect == .Poison) {
@@ -2278,7 +2255,7 @@ pub const Effects = struct {
2278
2255
  foe.active.volatiles.toxic = 0;
2279
2256
  }
2280
2257
 
2281
- try log.status(foe_ident, foe_stored.status, .None);
2258
+ try log.status(.{ foe_ident, foe_stored.status, protocol.Status.None });
2282
2259
  }
2283
2260
 
2284
2261
  fn rage(battle: anytype, player: Player) !void {
@@ -2298,19 +2275,21 @@ pub const Effects = struct {
2298
2275
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2299
2276
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2300
2277
 
2301
- try options.log.damageOf(
2278
+ try options.log.damage(.{
2302
2279
  battle.active(player),
2303
2280
  stored,
2304
- .RecoilOf,
2281
+ Damage.RecoilOf,
2305
2282
  battle.active(player.foe()),
2306
- );
2283
+ });
2307
2284
  if (showdown and stored.hp == 0) residual.* = false;
2308
2285
  }
2309
2286
 
2310
2287
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2311
2288
  var side = battle.side(player);
2312
2289
 
2313
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2290
+ if (side.active.volatiles.Reflect) {
2291
+ return options.log.fail(.{ battle.active(player), Fail.None });
2292
+ }
2314
2293
  side.active.volatiles.Reflect = true;
2315
2294
 
2316
2295
  try options.log.start(battle.active(player), .Reflect);
@@ -2327,10 +2306,10 @@ pub const Effects = struct {
2327
2306
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2328
2307
  } else {
2329
2308
  if (Status.any(foe_stored.status)) {
2330
- return options.log.fail(
2309
+ return options.log.fail(.{
2331
2310
  foe_ident,
2332
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2333
- );
2311
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2312
+ });
2334
2313
  }
2335
2314
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2336
2315
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2347,7 +2326,7 @@ pub const Effects = struct {
2347
2326
 
2348
2327
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2349
2328
  const last = battle.side(player).last_selected_move;
2350
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2329
+ try options.log.status(.{ foe_ident, foe_stored.status, protocol.Status.From, last });
2351
2330
  }
2352
2331
 
2353
2332
  fn splash(battle: anytype, player: Player, options: anytype) !void {
@@ -2357,8 +2336,7 @@ pub const Effects = struct {
2357
2336
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2358
2337
  var side = battle.side(player);
2359
2338
  if (side.active.volatiles.Substitute) {
2360
- try options.log.fail(battle.active(player), .Substitute);
2361
- return;
2339
+ return options.log.fail(.{ battle.active(player), Fail.Substitute });
2362
2340
  }
2363
2341
 
2364
2342
  assert(side.stored().stats.hp <= 1023);
@@ -2370,8 +2348,7 @@ pub const Effects = struct {
2370
2348
  // avoid having to convert to floating point)
2371
2349
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2372
2350
  if (side.stored().hp < required_hp) {
2373
- try options.log.fail(battle.active(player), .Weak);
2374
- return;
2351
+ return options.log.fail(.{ battle.active(player), Fail.Weak });
2375
2352
  }
2376
2353
 
2377
2354
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
@@ -2380,7 +2357,7 @@ pub const Effects = struct {
2380
2357
  side.active.volatiles.Substitute = true;
2381
2358
  try options.log.start(battle.active(player), .Substitute);
2382
2359
  if (hp > 0) {
2383
- try options.log.damage(battle.active(player), side.stored(), .None);
2360
+ try options.log.damage(.{ battle.active(player), side.stored(), Damage.None });
2384
2361
  if (showdown and side.stored().hp == 0) residual.* = false;
2385
2362
  }
2386
2363
  }
@@ -2394,8 +2371,8 @@ pub const Effects = struct {
2394
2371
  }
2395
2372
  battle.last_damage = 0;
2396
2373
  } else {
2397
- try options.log.fail(battle.active(player), .None);
2398
- try options.log.laststill();
2374
+ try options.log.fail(.{ battle.active(player), Fail.None });
2375
+ try options.log.laststill(.{});
2399
2376
  }
2400
2377
  }
2401
2378
 
@@ -2434,7 +2411,7 @@ pub const Effects = struct {
2434
2411
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2435
2412
  }
2436
2413
 
2437
- try options.log.transform(battle.active(player), foe_ident);
2414
+ try options.log.transform(.{ battle.active(player), foe_ident });
2438
2415
  }
2439
2416
 
2440
2417
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2464,87 +2441,87 @@ pub const Effects = struct {
2464
2441
  switch (move.effect) {
2465
2442
  .AttackUp1, .AttackUp2, .Rage => {
2466
2443
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2467
- if (boosts.atk == 6) return try log.fail(ident, .None);
2444
+ if (boosts.atk == 6) return log.fail(.{ ident, Fail.None });
2468
2445
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2469
2446
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2470
2447
  const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2471
2448
  if (stats.atk == MAX_STAT_VALUE) {
2472
2449
  boosts.atk -= 1;
2473
2450
  if (showdown) {
2474
- try log.boost(ident, reason, n);
2475
- try log.boost(ident, Boost.Attack, -1);
2451
+ try log.boost(.{ ident, reason, n });
2452
+ try log.boost(.{ ident, Boost.Attack, -1 });
2476
2453
  if (move.effect == .Rage) return;
2477
2454
  }
2478
- return try log.fail(ident, .None);
2455
+ return log.fail(.{ ident, Fail.None });
2479
2456
  }
2480
2457
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2481
2458
  const stat = unmodifiedStats(battle, side).atk;
2482
2459
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2483
- try log.boost(ident, reason, n);
2460
+ try log.boost(.{ ident, reason, n });
2484
2461
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2485
2462
  if (showdown and move.effect == .Rage) return;
2486
2463
  },
2487
2464
  .DefenseUp1, .DefenseUp2 => {
2488
2465
  assert(boosts.def >= -6 and boosts.def <= 6);
2489
- if (boosts.def == 6) return try log.fail(ident, .None);
2466
+ if (boosts.def == 6) return log.fail(.{ ident, Fail.None });
2490
2467
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2491
2468
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2492
2469
  if (stats.def == MAX_STAT_VALUE) {
2493
2470
  boosts.def -= 1;
2494
2471
  if (showdown) {
2495
- try log.boost(ident, .Defense, n);
2496
- try log.boost(ident, .Defense, -1);
2472
+ try log.boost(.{ ident, Boost.Defense, n });
2473
+ try log.boost(.{ ident, Boost.Defense, -1 });
2497
2474
  }
2498
- return try log.fail(ident, .None);
2475
+ return log.fail(.{ ident, Fail.None });
2499
2476
  }
2500
2477
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2501
2478
  const stat = unmodifiedStats(battle, side).def;
2502
2479
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2503
- try log.boost(ident, .Defense, n);
2480
+ try log.boost(.{ ident, Boost.Defense, n });
2504
2481
  },
2505
2482
  .SpeedUp2 => {
2506
2483
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2507
- if (boosts.spe == 6) return try log.fail(ident, .None);
2484
+ if (boosts.spe == 6) return log.fail(.{ ident, Fail.None });
2508
2485
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2509
2486
  if (stats.spe == MAX_STAT_VALUE) {
2510
2487
  boosts.spe -= 1;
2511
2488
  if (showdown) {
2512
- try log.boost(ident, .Speed, 2);
2513
- try log.boost(ident, .Speed, -1);
2489
+ try log.boost(.{ ident, Boost.Speed, 2 });
2490
+ try log.boost(.{ ident, Boost.Speed, -1 });
2514
2491
  }
2515
- return try log.fail(ident, .None);
2492
+ return log.fail(.{ ident, Fail.None });
2516
2493
  }
2517
2494
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2518
2495
  const stat = unmodifiedStats(battle, side).spe;
2519
2496
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2520
- try log.boost(ident, .Speed, 2);
2497
+ try log.boost(.{ ident, Boost.Speed, 2 });
2521
2498
  },
2522
2499
  .SpecialUp1, .SpecialUp2 => {
2523
2500
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2524
- if (boosts.spc == 6) return try log.fail(ident, .None);
2501
+ if (boosts.spc == 6) return log.fail(.{ ident, Fail.None });
2525
2502
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2526
2503
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2527
2504
  if (stats.spc == MAX_STAT_VALUE) {
2528
2505
  boosts.spc -= 1;
2529
2506
  if (showdown) {
2530
- try log.boost(ident, .SpecialAttack, n);
2531
- try log.boost(ident, .SpecialAttack, -1);
2532
- try log.boost(ident, .SpecialDefense, n);
2533
- try log.boost(ident, .SpecialDefense, -1);
2507
+ try log.boost(.{ ident, Boost.SpecialAttack, n });
2508
+ try log.boost(.{ ident, Boost.SpecialAttack, -1 });
2509
+ try log.boost(.{ ident, Boost.SpecialDefense, n });
2510
+ try log.boost(.{ ident, Boost.SpecialDefense, -1 });
2534
2511
  }
2535
- return try log.fail(ident, .None);
2512
+ return log.fail(.{ ident, Fail.None });
2536
2513
  }
2537
2514
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2538
2515
  const stat = unmodifiedStats(battle, side).spc;
2539
2516
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2540
- try log.boost(ident, .SpecialAttack, n);
2541
- try log.boost(ident, .SpecialDefense, n);
2517
+ try log.boost(.{ ident, Boost.SpecialAttack, n });
2518
+ try log.boost(.{ ident, Boost.SpecialDefense, n });
2542
2519
  },
2543
2520
  .EvasionUp1 => {
2544
2521
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2545
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2522
+ if (boosts.evasion == 6) return log.fail(.{ ident, Fail.None });
2546
2523
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2547
- try log.boost(ident, .Evasion, 1);
2524
+ try log.boost(.{ ident, Boost.Evasion, 1 });
2548
2525
  },
2549
2526
  else => unreachable,
2550
2527
  }
@@ -2559,7 +2536,9 @@ pub const Effects = struct {
2559
2536
  const foe_ident = battle.active(player.foe());
2560
2537
  const secondary = move.effect.isStatDownChance();
2561
2538
 
2562
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2539
+ if (foe.active.volatiles.Substitute) {
2540
+ return if (!secondary) log.fail(.{ foe_ident, Fail.None });
2541
+ }
2563
2542
 
2564
2543
  if (secondary) {
2565
2544
  const proc = try Rolls.unboost(battle, player, options);
@@ -2577,82 +2556,82 @@ pub const Effects = struct {
2577
2556
  switch (move.effect) {
2578
2557
  .AttackDown1, .AttackDownChance => {
2579
2558
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2580
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2559
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2581
2560
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2582
2561
  if (stats.atk == 1) {
2583
2562
  boosts.atk += 1;
2584
2563
  if (showdown) {
2585
- try log.boost(foe_ident, .Attack, -1);
2586
- try log.boost(foe_ident, .Attack, 1);
2564
+ try log.boost(.{ foe_ident, Boost.Attack, -1 });
2565
+ try log.boost(.{ foe_ident, Boost.Attack, 1 });
2587
2566
  }
2588
- return try log.fail(foe_ident, .None);
2567
+ return log.fail(.{ foe_ident, Fail.None });
2589
2568
  }
2590
2569
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2591
2570
  const stat = unmodifiedStats(battle, foe).atk;
2592
2571
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2593
- try log.boost(foe_ident, .Attack, -1);
2572
+ try log.boost(.{ foe_ident, Boost.Attack, -1 });
2594
2573
  },
2595
2574
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2596
2575
  assert(boosts.def >= -6 and boosts.def <= 6);
2597
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2576
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2598
2577
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2599
2578
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2600
2579
  if (stats.def == 1) {
2601
2580
  boosts.def += 1;
2602
2581
  if (showdown) {
2603
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2604
- try log.boost(foe_ident, .Defense, 1);
2582
+ try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2583
+ try log.boost(.{ foe_ident, Boost.Defense, 1 });
2605
2584
  }
2606
- return try log.fail(foe_ident, .None);
2585
+ return log.fail(.{ foe_ident, Fail.None });
2607
2586
  }
2608
2587
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2609
2588
  const stat = unmodifiedStats(battle, foe).def;
2610
2589
  stats.def = @max(1, stat * mod[0] / mod[1]);
2611
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2590
+ try log.boost(.{ foe_ident, Boost.Defense, -@as(i8, n) });
2612
2591
  },
2613
2592
  .SpeedDown1, .SpeedDownChance => {
2614
2593
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2615
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2594
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2616
2595
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2617
2596
  if (stats.spe == 1) {
2618
2597
  boosts.spe += 1;
2619
2598
  if (showdown) {
2620
- try log.boost(foe_ident, .Speed, -1);
2621
- try log.boost(foe_ident, .Speed, 1);
2599
+ try log.boost(.{ foe_ident, Boost.Speed, -1 });
2600
+ try log.boost(.{ foe_ident, Boost.Speed, 1 });
2622
2601
  }
2623
- return try log.fail(foe_ident, .None);
2602
+ return log.fail(.{ foe_ident, Fail.None });
2624
2603
  }
2625
2604
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2626
2605
  const stat = unmodifiedStats(battle, foe).spe;
2627
2606
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2628
- try log.boost(foe_ident, .Speed, -1);
2607
+ try log.boost(.{ foe_ident, Boost.Speed, -1 });
2629
2608
  assert(boosts.spe >= -6);
2630
2609
  },
2631
2610
  .SpecialDownChance => {
2632
2611
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2633
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2612
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2634
2613
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2635
2614
  if (stats.spc == 1) {
2636
2615
  boosts.spc += 1;
2637
2616
  if (showdown) {
2638
- try log.boost(foe_ident, .SpecialAttack, -1);
2639
- try log.boost(foe_ident, .SpecialAttack, 1);
2640
- try log.boost(foe_ident, .SpecialDefense, -1);
2641
- try log.boost(foe_ident, .SpecialDefense, 1);
2617
+ try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2618
+ try log.boost(.{ foe_ident, Boost.SpecialAttack, 1 });
2619
+ try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2620
+ try log.boost(.{ foe_ident, Boost.SpecialDefense, 1 });
2642
2621
  }
2643
- return try log.fail(foe_ident, .None);
2622
+ return log.fail(.{ foe_ident, Fail.None });
2644
2623
  }
2645
2624
  var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2646
2625
  const stat = unmodifiedStats(battle, foe).spc;
2647
2626
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2648
- try log.boost(foe_ident, .SpecialAttack, -1);
2649
- try log.boost(foe_ident, .SpecialDefense, -1);
2627
+ try log.boost(.{ foe_ident, Boost.SpecialAttack, -1 });
2628
+ try log.boost(.{ foe_ident, Boost.SpecialDefense, -1 });
2650
2629
  },
2651
2630
  .AccuracyDown1 => {
2652
2631
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2653
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2632
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, Fail.None });
2654
2633
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2655
- try log.boost(foe_ident, .Accuracy, -1);
2634
+ try log.boost(.{ foe_ident, Boost.Accuracy, -1 });
2656
2635
  },
2657
2636
  else => unreachable,
2658
2637
  }
@@ -2676,7 +2655,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2676
2655
  }
2677
2656
  }
2678
2657
 
2679
- inline fn isForced(active: anytype) bool {
2658
+ fn isForced(active: anytype) bool {
2680
2659
  return active.volatiles.Recharging or active.volatiles.Rage or
2681
2660
  active.volatiles.Thrashing or active.volatiles.Charging;
2682
2661
  }
@@ -2689,27 +2668,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2689
2668
 
2690
2669
  options.chance.clearDurations(who, clear);
2691
2670
 
2692
- if (volatiles.disabled_move != 0) {
2693
- volatiles.disabled_move = 0;
2694
- volatiles.disabled_duration = 0;
2695
- try log.end(ident, .DisableSilent);
2671
+ if (volatiles.disable_move != 0) {
2672
+ volatiles.disable_move = 0;
2673
+ volatiles.disable_duration = 0;
2674
+ try log.end(.{ ident, End.DisableSilent });
2696
2675
  }
2697
2676
  if (volatiles.Confusion) {
2698
2677
  // volatiles.confusion is left unchanged
2699
2678
  volatiles.Confusion = false;
2700
- try log.end(ident, .ConfusionSilent);
2679
+ try log.end(.{ ident, End.ConfusionSilent });
2701
2680
  }
2702
2681
  if (volatiles.Mist) {
2703
2682
  volatiles.Mist = false;
2704
- try log.end(ident, .MistSilent);
2683
+ try log.end(.{ ident, End.MistSilent });
2705
2684
  }
2706
2685
  if (volatiles.FocusEnergy) {
2707
2686
  volatiles.FocusEnergy = false;
2708
- try log.end(ident, .FocusEnergySilent);
2687
+ try log.end(.{ ident, End.FocusEnergySilent });
2709
2688
  }
2710
2689
  if (volatiles.LeechSeed) {
2711
2690
  volatiles.LeechSeed = false;
2712
- try log.end(ident, .LeechSeedSilent);
2691
+ try log.end(.{ ident, End.LeechSeedSilent });
2713
2692
  }
2714
2693
  if (!showdown and volatiles.Toxic) {
2715
2694
  volatiles.Toxic = false;
@@ -2717,22 +2696,22 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2717
2696
  }
2718
2697
  if (volatiles.LightScreen) {
2719
2698
  volatiles.LightScreen = false;
2720
- try log.end(ident, .LightScreenSilent);
2699
+ try log.end(.{ ident, End.LightScreenSilent });
2721
2700
  }
2722
2701
  if (volatiles.Reflect) {
2723
2702
  volatiles.Reflect = false;
2724
- try log.end(ident, .ReflectSilent);
2703
+ try log.end(.{ ident, End.ReflectSilent });
2725
2704
  }
2726
2705
  if (showdown and volatiles.Toxic) {
2727
2706
  volatiles.Toxic = false;
2728
2707
  // Pokémon Showdown erroneously clears the toxic counter
2729
2708
  volatiles.toxic = 0;
2730
- try log.end(ident, .ToxicSilent);
2709
+ try log.end(.{ ident, End.ToxicSilent });
2731
2710
  }
2732
2711
  }
2733
2712
 
2734
2713
  pub const Rolls = struct {
2735
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2714
+ fn speedTie(battle: anytype, options: anytype) !bool {
2736
2715
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2737
2716
  player == .P1
2738
2717
  else if (showdown)
@@ -2744,7 +2723,7 @@ pub const Rolls = struct {
2744
2723
  return p1;
2745
2724
  }
2746
2725
 
2747
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2726
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2748
2727
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2749
2728
  val == .true
2750
2729
  else if (showdown)
@@ -2756,7 +2735,7 @@ pub const Rolls = struct {
2756
2735
  return crit;
2757
2736
  }
2758
2737
 
2759
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2738
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2760
2739
  const roll = if (options.calc.overridden(player, .damage)) |val|
2761
2740
  val
2762
2741
  else roll: {
@@ -2772,7 +2751,7 @@ pub const Rolls = struct {
2772
2751
  return roll;
2773
2752
  }
2774
2753
 
2775
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2754
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2776
2755
  const ok = if (options.calc.overridden(player, .hit)) |val|
2777
2756
  val == .true
2778
2757
  else if (showdown)
@@ -2784,7 +2763,7 @@ pub const Rolls = struct {
2784
2763
  return ok;
2785
2764
  }
2786
2765
 
2787
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2766
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2788
2767
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2789
2768
  val == .true
2790
2769
  else if (showdown)
@@ -2796,7 +2775,7 @@ pub const Rolls = struct {
2796
2775
  return cfz;
2797
2776
  }
2798
2777
 
2799
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2778
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2800
2779
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2801
2780
  val == .true
2802
2781
  else if (showdown)
@@ -2808,7 +2787,7 @@ pub const Rolls = struct {
2808
2787
  return par;
2809
2788
  }
2810
2789
 
2811
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2790
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2812
2791
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2813
2792
  val == .true
2814
2793
  else if (showdown)
@@ -2820,7 +2799,7 @@ pub const Rolls = struct {
2820
2799
  return proc;
2821
2800
  }
2822
2801
 
2823
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2802
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2824
2803
  const rate: u8 = if (low) 26 else 77;
2825
2804
 
2826
2805
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2834,7 +2813,7 @@ pub const Rolls = struct {
2834
2813
  return proc;
2835
2814
  }
2836
2815
 
2837
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2816
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2838
2817
  const rate: u8 = if (low) 52 else 103;
2839
2818
 
2840
2819
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2848,7 +2827,7 @@ pub const Rolls = struct {
2848
2827
  return proc;
2849
2828
  }
2850
2829
 
2851
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2830
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2852
2831
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2853
2832
  val == .true
2854
2833
  else if (showdown)
@@ -2860,14 +2839,12 @@ pub const Rolls = struct {
2860
2839
  return proc;
2861
2840
  }
2862
2841
 
2863
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2842
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2864
2843
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2865
2844
  val
2866
- else if (showdown) move: {
2867
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2868
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2869
- break :move @enumFromInt(r + mod);
2870
- } else move: {
2845
+ else if (showdown)
2846
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2847
+ else move: {
2871
2848
  while (true) {
2872
2849
  const r = battle.rng.next();
2873
2850
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2880,7 +2857,7 @@ pub const Rolls = struct {
2880
2857
  return move;
2881
2858
  }
2882
2859
 
2883
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2860
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2884
2861
  const power = if (options.calc.overridden(player, .psywave)) |val|
2885
2862
  val - 1
2886
2863
  else if (showdown)
@@ -2897,11 +2874,11 @@ pub const Rolls = struct {
2897
2874
  return power;
2898
2875
  }
2899
2876
 
2900
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2877
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2901
2878
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2902
2879
  @intCast(val)
2903
2880
  else if (showdown)
2904
- @intCast(battle.rng.range(u8, 1, 8))
2881
+ battle.rng.range(u3, 1, 8)
2905
2882
  else duration: {
2906
2883
  while (true) {
2907
2884
  const r = battle.rng.next() & 7;
@@ -2914,11 +2891,11 @@ pub const Rolls = struct {
2914
2891
  return duration;
2915
2892
  }
2916
2893
 
2917
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2894
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2918
2895
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2919
2896
  val
2920
2897
  else if (showdown)
2921
- @intCast(battle.rng.range(u8, 1, 9))
2898
+ battle.rng.range(u4, 1, 9)
2922
2899
  else
2923
2900
  @intCast((battle.rng.next() & 7) + 1);
2924
2901
 
@@ -2927,11 +2904,11 @@ pub const Rolls = struct {
2927
2904
  return duration;
2928
2905
  }
2929
2906
 
2930
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2907
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2931
2908
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2909
  @intCast(val)
2933
2910
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2911
+ battle.rng.range(u3, 2, 6)
2935
2912
  else
2936
2913
  @intCast((battle.rng.next() & 3) + 2);
2937
2914
 
@@ -2940,11 +2917,11 @@ pub const Rolls = struct {
2940
2917
  return duration;
2941
2918
  }
2942
2919
 
2943
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2920
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2944
2921
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2945
2922
  @intCast(val)
2946
2923
  else if (showdown)
2947
- @intCast(battle.rng.range(u4, 2, 4))
2924
+ battle.rng.range(u3, 2, 4)
2948
2925
  else
2949
2926
  @intCast((battle.rng.next() & 1) + 2);
2950
2927
 
@@ -2953,7 +2930,9 @@ pub const Rolls = struct {
2953
2930
  return duration;
2954
2931
  }
2955
2932
 
2956
- inline fn distribution(
2933
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2934
+
2935
+ fn distribution(
2957
2936
  battle: anytype,
2958
2937
  comptime effect: Move.Effect,
2959
2938
  player: Player,
@@ -2963,7 +2942,7 @@ pub const Rolls = struct {
2963
2942
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2964
2943
  @intCast(val)
2965
2944
  else if (showdown)
2966
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2945
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2967
2946
  else n: {
2968
2947
  const r = (battle.rng.next() & 3);
2969
2948
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3010,10 +2989,9 @@ pub const Rolls = struct {
3010
2989
  while (i < moves.len and r > 0) : (i += 1) {
3011
2990
  if (moves[i].pp > 0) {
3012
2991
  r -= 1;
3013
- if (r == 0) break;
2992
+ if (r == 0) break :slot @intCast(i + 1);
3014
2993
  }
3015
2994
  }
3016
- break :slot @intCast(i + 1);
3017
2995
  }
3018
2996
  }
3019
2997
 
@@ -3129,7 +3107,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3129
3107
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3130
3108
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3131
3109
  const struggle =
3132
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3110
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3133
3111
  const s = if (struggle) 0 else slot;
3134
3112
  out[n] = .{ .type = .Move, .data = s };
3135
3113
  n += 1;
@@ -3144,7 +3122,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3144
3122
  const m = active.moves[slot - 1];
3145
3123
  if (m.id == .None) break;
3146
3124
  if (m.pp == 0) continue;
3147
- if (active.volatiles.disabled_move == slot) continue;
3125
+ if (active.volatiles.disable_move == slot) continue;
3148
3126
  out[n] = .{ .type = .Move, .data = slot };
3149
3127
  n += 1;
3150
3128
  }