@pkmn/engine 0.1.0-dev.736f7a8b → 0.1.0-dev.77410aff

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 (45) hide show
  1. package/README.md +36 -14
  2. package/build/pkg/addon.js.map +1 -1
  3. package/build/pkg/data/ids.json +12 -12
  4. package/build/pkg/data/layout.json +2 -2
  5. package/build/pkg/data/protocol.json +69 -12
  6. package/build/pkg/data.d.ts +3 -2
  7. package/build/pkg/data.js.map +1 -1
  8. package/build/pkg/gen1.js +2 -2
  9. package/build/pkg/gen1.js.map +1 -1
  10. package/build/pkg/index.d.ts +2 -1
  11. package/build/pkg/index.js.map +1 -1
  12. package/build/pkg/protocol.js +57 -38
  13. package/build/pkg/protocol.js.map +1 -1
  14. package/build/test/display.js.map +1 -1
  15. package/build/tools/debug.js +3 -1
  16. package/build/tools/debug.js.map +1 -1
  17. package/build.zig +40 -23
  18. package/package.json +30 -17
  19. package/src/bin/install-pkmn-engine +1 -1
  20. package/src/lib/binding/node.zig +7 -11
  21. package/src/lib/common/data.zig +8 -20
  22. package/src/lib/common/optional.zig +8 -1
  23. package/src/lib/common/protocol.zig +725 -299
  24. package/src/lib/common/rational.zig +12 -8
  25. package/src/lib/common/rng.zig +15 -6
  26. package/src/lib/gen1/README.md +31 -29
  27. package/src/lib/gen1/calc.zig +9 -9
  28. package/src/lib/gen1/chance.zig +55 -39
  29. package/src/lib/gen1/data/moves.zig +192 -180
  30. package/src/lib/gen1/data/species.zig +2 -2
  31. package/src/lib/gen1/data/types.zig +7 -7
  32. package/src/lib/gen1/data.zig +31 -43
  33. package/src/lib/gen1/helpers.zig +26 -41
  34. package/src/lib/gen1/mechanics.zig +314 -365
  35. package/src/lib/gen2/README.md +18 -72
  36. package/src/lib/gen2/calc.zig +143 -0
  37. package/src/lib/gen2/chance.zig +1314 -0
  38. package/src/lib/gen2/data/items.zig +12 -18
  39. package/src/lib/gen2/data/moves.zig +649 -568
  40. package/src/lib/gen2/data/species.zig +1 -1
  41. package/src/lib/gen2/data/types.zig +134 -31
  42. package/src/lib/gen2/data.zig +80 -68
  43. package/src/lib/gen2/generated.zig +806 -0
  44. package/src/lib/gen2/helpers.zig +4 -9
  45. package/src/lib/gen2/mechanics.zig +3238 -85
@@ -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,34 +58,26 @@ 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);
82
73
 
83
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
84
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
74
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
75
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
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;
@@ -99,11 +90,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
90
  const p1 = battle.side(.P1);
100
91
  const p2 = battle.side(.P2);
101
92
 
102
- var p1_slot = findFirstAlive(p1);
93
+ const p1_slot = findFirstAlive(p1);
103
94
  assert(!showdown or p1_slot == 1);
104
95
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
96
 
106
- var p2_slot = findFirstAlive(p2);
97
+ const p2_slot = findFirstAlive(p2);
107
98
  assert(!showdown or p2_slot == 1);
108
99
  if (p2_slot == 0) return Result.Win;
109
100
 
@@ -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,16 +196,13 @@ 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
  };
217
-
218
203
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
204
  }
205
+ saveMove(battle, player, choice);
223
206
 
224
207
  return null;
225
208
  }
@@ -232,7 +215,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
215
  if (c.data == 0) {
233
216
  side.last_selected_move = .Struggle;
234
217
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
219
  const move = side.active.move(c.data);
237
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
221
  // sometimes required...), but a 0 PP move can be used automatically
@@ -273,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
273
256
 
274
257
  foe.active.volatiles.Binding = false;
275
258
 
276
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
277
260
  options.chance.switched(player, side.order[0], out);
278
261
 
279
262
  if (showdown and incoming.status == Status.TOX) {
280
263
  incoming.status = Status.init(.PSN);
281
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
265
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
267
  }
285
268
  }
286
269
 
@@ -343,12 +326,10 @@ fn doTurn(
343
326
  player_choice: Choice,
344
327
  player_rewrap: bool,
345
328
  player_skip: bool,
346
- player_from: ?Move,
347
329
  foe_player: Player,
348
330
  foe_choice: Choice,
349
331
  foe_rewrap: bool,
350
332
  foe_skip: bool,
351
- foe_from: ?Move,
352
333
  options: anytype,
353
334
  ) !?Result {
354
335
  assert(player_choice.type != .Pass);
@@ -361,7 +342,6 @@ fn doTurn(
361
342
  player_choice,
362
343
  player_rewrap,
363
344
  player_skip,
364
- player_from,
365
345
  &residual,
366
346
  options,
367
347
  )) |r| return r;
@@ -384,7 +364,6 @@ fn doTurn(
384
364
  foe_choice,
385
365
  foe_rewrap,
386
366
  foe_skip,
387
- foe_from,
388
367
  &residual,
389
368
  options,
390
369
  )) |r| return r;
@@ -412,7 +391,6 @@ fn executeMove(
412
391
  choice: Choice,
413
392
  rewrap: bool,
414
393
  skip: bool,
415
- from: ?Move,
416
394
  residual: *bool,
417
395
  options: anytype,
418
396
  ) !?Result {
@@ -429,7 +407,7 @@ fn executeMove(
429
407
  if (side.last_selected_move == .SKIP_TURN) {
430
408
  assert(!showdown);
431
409
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
433
411
  }
434
412
  return null;
435
413
  }
@@ -447,8 +425,10 @@ fn executeMove(
447
425
  mslot = @intCast(battle.lastMove(player).index);
448
426
  const stored = side.stored();
449
427
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
450
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
451
- 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);
452
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
454
434
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -470,7 +450,7 @@ fn executeMove(
470
450
 
471
451
  var skip_can = false;
472
452
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
454
  .done => return null,
475
455
  .skip_can => skip_can = true,
476
456
  .skip_pp => skip_pp = true,
@@ -478,9 +458,9 @@ fn executeMove(
478
458
  .err => return @as(?Result, Result.Error),
479
459
  }
480
460
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
484
464
 
485
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
466
  }
@@ -491,7 +471,6 @@ fn beforeMove(
491
471
  battle: anytype,
492
472
  player: Player,
493
473
  skip: bool,
494
- from: ?Move,
495
474
  residual: *bool,
496
475
  options: anytype,
497
476
  ) !BeforeMove {
@@ -518,23 +497,23 @@ fn beforeMove(
518
497
  }
519
498
 
520
499
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
522
501
  stored.status = 0; // clears EXT if present
523
502
  } else {
524
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
525
504
  }
526
505
  side.last_used_move = .None;
527
506
  return .done;
528
507
  }
529
508
 
530
509
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
532
511
  side.last_used_move = .None;
533
512
  return .done;
534
513
  }
535
514
 
536
515
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
538
517
  return .done;
539
518
  }
540
519
 
@@ -542,27 +521,27 @@ fn beforeMove(
542
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
522
  // Pokémon can sometimes flinch multiple times from the same original hit
544
523
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
546
525
  return .done;
547
526
  }
548
527
 
549
528
  if (volatiles.Recharging) {
550
529
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
552
531
  return .done;
553
532
  }
554
533
 
555
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
556
535
  if (options.calc.modify(player, .disable)) |extend| {
557
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
558
537
  } else {
559
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
560
539
  }
561
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
562
541
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
566
545
  }
567
546
  }
568
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -587,9 +566,9 @@ fn beforeMove(
587
566
 
588
567
  if (volatiles.confusion == 0) {
589
568
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
591
570
  } else {
592
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
593
572
 
594
573
  if (try Rolls.confused(battle, player, options)) {
595
574
  assert(!volatiles.MultiHit);
@@ -640,7 +619,7 @@ fn beforeMove(
640
619
  volatiles.Binding = false;
641
620
  options.chance.clearDurations(player, null);
642
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
644
623
  return .done;
645
624
  }
646
625
 
@@ -656,8 +635,6 @@ fn beforeMove(
656
635
  volatiles.state +%= battle.last_damage;
657
636
  }
658
637
 
659
- assert(volatiles.attacks > 0);
660
-
661
638
  if (options.calc.modify(player, .attacking)) |extend| {
662
639
  if (!extend) volatiles.attacks = 0;
663
640
  } else {
@@ -666,18 +643,18 @@ fn beforeMove(
666
643
  try options.chance.attacking(player, volatiles.attacks);
667
644
 
668
645
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
670
647
  return .done;
671
648
  }
672
649
 
673
650
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
675
652
 
676
653
  battle.last_damage = volatiles.state *% 2;
677
654
  volatiles.state = 0;
678
655
 
679
656
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
681
658
  return .done;
682
659
  }
683
660
 
@@ -697,9 +674,6 @@ fn beforeMove(
697
674
  }
698
675
 
699
676
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
702
-
703
677
  if (options.calc.modify(player, .attacking)) |extend| {
704
678
  if (!extend) volatiles.attacks = 0;
705
679
  } else {
@@ -708,23 +682,21 @@ fn beforeMove(
708
682
  try options.chance.attacking(player, volatiles.attacks);
709
683
 
710
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
711
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
712
686
  }
713
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
687
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
688
  if (showdown) {
715
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
716
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
717
691
  side.last_used_move = side.last_selected_move;
718
692
  if (handleThrashing(battle, active, player, options)) {
719
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
720
694
  }
721
695
  }
722
696
  return .skip_can;
723
697
  }
724
698
 
725
699
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
727
-
728
700
  if (options.calc.modify(player, .binding)) |extend| {
729
701
  if (!extend) volatiles.attacks = 0;
730
702
  } else {
@@ -732,12 +704,7 @@ fn beforeMove(
732
704
  }
733
705
  try options.chance.binding(player, volatiles.attacks);
734
706
 
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
708
  if (showdown or battle.last_damage != 0) {
742
709
  const sub = showdown and foe.active.volatiles.Substitute;
743
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +724,7 @@ fn canMove(
757
724
  mslot: u4,
758
725
  auto: bool,
759
726
  skip_pp: bool,
760
- from: ?Move,
727
+ from: Move,
761
728
  residual: *bool,
762
729
  options: anytype,
763
730
  ) !bool {
@@ -765,33 +732,21 @@ fn canMove(
765
732
  const player_ident = battle.active(player);
766
733
  const move = Move.get(side.last_selected_move);
767
734
 
768
- const foe_last = battle.foe(player).last_used_move;
769
- const special = from != null and (from.? == .Metronome and
770
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
771
-
772
- var skip = skip_pp;
773
735
  if (side.active.volatiles.Charging) {
774
736
  side.active.volatiles.Charging = false;
775
737
  side.active.volatiles.Invulnerable = false;
776
738
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
740
  setCounterable(battle, player, side, move);
779
741
  try Effects.charge(battle, player, options);
780
742
  return false;
781
743
  }
782
744
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
747
 
786
748
  const target = if (move.target == .Self) player else player.foe();
787
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
788
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
789
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
790
- // meaning sometimes we lose this information. from is still set because it not being null is
791
- // relevant for control flow, but we don't have anything useful to put in the log. This is
792
- // regrettable, but this information is more "nice to have" than required
793
- const f = if (from != null and from.? == .None) null else from;
794
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
750
  setCounterable(battle, player, side, move);
796
751
 
797
752
  if (move.effect.onBegin()) {
@@ -829,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
785
  if (volatiles.Bide) return;
831
786
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
787
+ var move_slot = active.move(mslot);
788
+ assert(move_slot.pp > 0 or auto);
789
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
790
  if (volatiles.Transform) return;
835
791
 
836
- assert(side.stored().move(mslot).pp > 0 or auto);
837
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
792
+ move_slot = side.stored().move(mslot);
793
+ assert(move_slot.pp > 0 or auto);
794
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
796
  }
840
797
 
@@ -938,7 +895,7 @@ fn doMove(
938
895
  }
939
896
  }
940
897
 
941
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
942
899
  miss = if (showdown or skip)
943
900
  miss
944
901
  else
@@ -966,16 +923,16 @@ fn doMove(
966
923
  ohko = (!showdown or (!immune and !invulnerable)) and
967
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
925
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
970
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
972
929
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- try log.fail(foe_ident, .None);
930
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
+ try log.fail(.{ foe_ident, .None });
975
932
  } else {
976
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- try log.miss(battle.active(player));
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
979
936
  }
980
937
  if (move.effect == .JumpKick) {
981
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1011,15 +968,15 @@ fn doMove(
1011
968
  var hit: u4 = 0;
1012
969
  while (hit < hits) {
1013
970
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
972
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
1017
974
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
1019
976
  }
1020
977
  }
1021
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
980
  hit += 1;
1024
981
  if (foe.stored().hp == 0) break;
1025
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,7 +993,7 @@ fn doMove(
1036
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
995
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1040
997
  } else if (showdown) {
1041
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
999
  if (move.effect == .Binding) {
@@ -1046,9 +1003,9 @@ fn doMove(
1046
1003
  assert(foe.stored().hp > 0);
1047
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
1005
  if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1050
1007
  } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1052
1009
  try buildRage(battle, player.foe(), options);
1053
1010
  }
1054
1011
  return null;
@@ -1246,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1203
  const foe = battle.foe(player);
1247
1204
 
1248
1205
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1250
1207
  return null;
1251
1208
  }
1252
1209
 
@@ -1264,14 +1221,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1221
  foe_last_selected_move.type == .Fighting);
1265
1222
 
1266
1223
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1268
1225
  return null;
1269
1226
  }
1270
1227
 
1271
1228
  if (!used or !selected) {
1272
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1230
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1275
1232
  return null;
1276
1233
  }
1277
1234
 
@@ -1307,20 +1264,20 @@ fn applyDamage(
1307
1264
  subbed.active.volatiles.substitute = 0;
1308
1265
  subbed.active.volatiles.Substitute = false;
1309
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1310
- try options.log.end(battle.active(sub_player), .Substitute);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1311
1268
  options.calc.capped(target_player);
1312
1269
  return true;
1313
1270
  } else {
1314
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1274
  return false;
1318
1275
  }
1319
1276
  }
1320
1277
 
1321
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1279
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1281
  return false;
1325
1282
  }
1326
1283
 
@@ -1339,7 +1296,7 @@ fn mirrorMove(
1339
1296
  side.last_selected_move = foe.last_used_move;
1340
1297
 
1341
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1343
1300
  return null;
1344
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
1302
  incrementPP(side, mslot);
@@ -1381,12 +1338,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1381
1338
  if (mist) {
1382
1339
  assert(!showdown);
1383
1340
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1386
1343
  } else {
1387
1344
  try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1390
1347
  }
1391
1348
 
1392
1349
  return false;
@@ -1438,7 +1395,7 @@ fn moveHit(
1438
1395
  const overwritten = overwrite and state > 0;
1439
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1397
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1398
+ var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1442
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1400
  accuracy = accuracy * boost[0] / boost[1];
1444
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1477,9 +1434,8 @@ fn checkFaint(
1477
1434
  const foe_fainted = foe.stored().hp == 0;
1478
1435
 
1479
1436
  const player_out = findFirstAlive(side) == 0;
1480
- const foe_out = findFirstAlive(foe) == 0;
1481
- const tie = player_out and foe_out;
1482
- const more = tie or player_out or foe_out;
1437
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
+ const more = player_out or foe_out;
1483
1439
 
1484
1440
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1485
1441
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1487,20 +1443,20 @@ fn checkFaint(
1487
1443
  assert(!side.active.volatiles.MultiHit);
1488
1444
  assert(!foe.active.volatiles.MultiHit);
1489
1445
 
1490
- if (tie) {
1491
- try options.log.tie();
1446
+ if (player_out and foe_out) {
1447
+ try options.log.tie(.{});
1492
1448
  return Result.Tie;
1493
1449
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1495
1451
  return if (player == .P1) Result.Lose else Result.Win;
1496
1452
  } else if (foe_out) {
1497
- try options.log.win(player);
1453
+ try options.log.win(.{player});
1498
1454
  return if (player == .P1) Result.Win else Result.Lose;
1499
1455
  }
1500
1456
 
1501
1457
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1502
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1503
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1458
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1459
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1460
  }
1505
1461
 
1506
1462
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1536,7 +1492,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1492
  side.stored().stats.spe;
1537
1493
  }
1538
1494
 
1539
- try options.log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1540
1496
  options.calc.capped(player);
1541
1497
  return null;
1542
1498
  }
@@ -1548,21 +1504,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1504
  var volatiles = &side.active.volatiles;
1549
1505
 
1550
1506
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1507
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1508
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1509
  if (volatiles.Toxic) {
1555
1510
  volatiles.toxic += 1;
1556
1511
  damage *= volatiles.toxic;
1557
1512
  }
1513
+ damage = @min(damage, stored.hp);
1558
1514
 
1559
- const amount = @min(damage, stored.hp);
1560
- if (showdown and amount == 0) break :blk;
1561
-
1562
- stored.hp -= amount;
1563
-
1564
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1565
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1515
+ if (!showdown or damage > 0) {
1516
+ stored.hp -= damage;
1517
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1518
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1519
+ }
1566
1520
  }
1567
1521
 
1568
1522
  if (volatiles.LeechSeed) {
@@ -1586,14 +1540,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1540
  const amount = @min(damage, stored.hp);
1587
1541
  stored.hp -= amount;
1588
1542
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1543
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1544
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1545
 
1592
1546
  const before = foe_stored.hp;
1593
1547
  // Uncapped damage is added back to the foe
1594
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1549
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1596
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1551
  }
1598
1552
  }
1599
1553
 
@@ -1604,18 +1558,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1558
  battle.turn += 1;
1605
1559
 
1606
1560
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1561
+ try options.log.tie(.{});
1608
1562
  return Result.Tie;
1609
1563
  }
1610
1564
 
1611
1565
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1566
+ try options.log.tie(.{});
1613
1567
  return Result.Tie;
1614
1568
  } else if (battle.turn >= 65535) {
1615
1569
  return Result.Error;
1616
1570
  }
1617
1571
 
1618
- try options.log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1619
1573
 
1620
1574
  return Result.Default;
1621
1575
  }
@@ -1675,9 +1629,9 @@ fn checkEBC(battle: anytype) bool {
1675
1629
  }
1676
1630
 
1677
1631
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1632
+ if (side.active.volatiles.disable_move != 0) {
1679
1633
  // A Pokémon that transforms after being disabled may end up with less move slots
1680
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1634
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1635
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1636
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1637
  m
@@ -1685,14 +1639,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1639
  side.last_selected_move;
1686
1640
  if (m != .None and m == last) {
1687
1641
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1642
+ try options.log.cant(.{ ident, .Disable, last });
1689
1643
  return true;
1690
1644
  }
1691
1645
  }
1692
1646
  return false;
1693
1647
  }
1694
1648
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1650
  const side = battle.side(who);
1697
1651
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1652
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1710,7 +1664,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1710
1664
  return true;
1711
1665
  }
1712
1666
 
1713
- inline fn onBegin(
1667
+ fn onBegin(
1714
1668
  battle: anytype,
1715
1669
  player: Player,
1716
1670
  move: Move.Data,
@@ -1718,7 +1672,6 @@ inline fn onBegin(
1718
1672
  residual: *bool,
1719
1673
  options: anytype,
1720
1674
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1675
  return switch (move.effect) {
1723
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1693,7 @@ inline fn onBegin(
1740
1693
  };
1741
1694
  }
1742
1695
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1697
  return switch (move.effect) {
1746
1698
  .Bide => Effects.bide(battle, player, options),
1747
1699
  // zig fmt: off
@@ -1756,14 +1708,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1708
  };
1757
1709
  }
1758
1710
 
1759
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1760
1712
  battle: anytype,
1761
1713
  player: Player,
1762
1714
  move: Move.Data,
1763
1715
  residual: *bool,
1764
1716
  options: anytype,
1765
1717
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1718
  return switch (move.effect) {
1768
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1720
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1726,7 @@ inline fn alwaysHappens(
1775
1726
  };
1776
1727
  }
1777
1728
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1729
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1730
  return switch (move.effect) {
1780
1731
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1732
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1802,7 +1753,7 @@ pub const Effects = struct {
1802
1753
  side.active.volatiles.state = 0;
1803
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1804
1755
 
1805
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1757
  }
1807
1758
 
1808
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1767,11 @@ pub const Effects = struct {
1816
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1769
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1770
+ try options.log.curestatus(.{
1820
1771
  battle.active(player.foe()),
1821
1772
  foe_stored.status,
1822
1773
  .Message,
1823
- );
1774
+ });
1824
1775
  foe_stored.status = 0;
1825
1776
  }
1826
1777
  return;
@@ -1834,7 +1785,7 @@ pub const Effects = struct {
1834
1785
  foe_stored.status = Status.init(.BRN);
1835
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1787
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1788
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1789
  }
1839
1790
 
1840
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1795,8 @@ pub const Effects = struct {
1844
1795
  volatiles.Charging = true;
1845
1796
  const move = side.last_selected_move;
1846
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1849
1800
  }
1850
1801
 
1851
1802
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,17 +1807,11 @@ pub const Effects = struct {
1856
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1808
  } else {
1858
1809
  if (showdown) {
1859
- if (!try checkHit(battle, player, move, options)) {
1860
- return;
1861
- } else if (sub) {
1862
- return options.log.fail(battle.active(player.foe()), .None);
1863
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1812
  } else {
1865
- if (sub) {
1866
- return options.log.fail(battle.active(player.foe()), .None);
1867
- } else if (!try checkHit(battle, player, move, options)) {
1868
- return;
1869
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1870
1815
  }
1871
1816
  try options.chance.commit(player, .hit);
1872
1817
  }
@@ -1875,23 +1820,24 @@ pub const Effects = struct {
1875
1820
  foe.active.volatiles.Confusion = true;
1876
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1822
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1824
  }
1880
1825
 
1881
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1827
  const foe = battle.foe(player);
1883
1828
 
1884
1829
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1887
1832
  }
1888
1833
 
1889
1834
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1835
+ return options.log.start(.{
1891
1836
  battle.active(player),
1837
+ .TypeChange,
1892
1838
  foe.active.types,
1893
1839
  battle.active(player.foe()),
1894
- );
1840
+ });
1895
1841
  }
1896
1842
 
1897
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1905,8 +1851,8 @@ pub const Effects = struct {
1905
1851
  try options.chance.commit(player, .hit);
1906
1852
  }
1907
1853
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1854
+ if (volatiles.disable_move != 0) {
1855
+ try options.log.fail(.{ foe_ident, .None });
1910
1856
  return null;
1911
1857
  }
1912
1858
 
@@ -1922,7 +1868,7 @@ pub const Effects = struct {
1922
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1870
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1926
1872
  return null;
1927
1873
  } else if (err) {
1928
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1876,18 @@ pub const Effects = struct {
1930
1876
  return Result.Error;
1931
1877
  }
1932
1878
 
1933
- volatiles.disabled_move =
1879
+ volatiles.disable_move =
1934
1880
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1881
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1936
1882
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1883
+ const id = foe.active.move(volatiles.disable_move).id;
1884
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1885
  return null;
1940
1886
  }
1941
1887
 
1942
1888
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1889
+ var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1945
1891
 
1946
1892
  if (battle.last_damage == 0) {
1947
1893
  assert(showdown);
@@ -1954,7 +1900,7 @@ pub const Effects = struct {
1954
1900
  if (stored.hp == stored.stats.hp) return;
1955
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1902
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1904
  }
1959
1905
 
1960
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1932,7 @@ pub const Effects = struct {
1986
1932
  if (side.active.volatiles.FocusEnergy) return;
1987
1933
  side.active.volatiles.FocusEnergy = true;
1988
1934
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1936
  }
1991
1937
 
1992
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +1954,7 @@ pub const Effects = struct {
2008
1954
  foe_stored.status = Status.init(.FRZ);
2009
1955
  // GLITCH: Hyper Beam recharging status is not cleared
2010
1956
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
1958
  }
2013
1959
 
2014
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +1962,7 @@ pub const Effects = struct {
2016
1962
  var side = battle.side(player);
2017
1963
  var foe = battle.foe(player);
2018
1964
 
2019
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
2020
1966
  var foe_stored = foe.stored();
2021
1967
 
2022
1968
  const player_ident = battle.active(player);
@@ -2028,8 +1974,8 @@ pub const Effects = struct {
2028
1974
  side.active.stats = side_stored.stats;
2029
1975
  foe.active.stats = foe_stored.stats;
2030
1976
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
2033
1979
 
2034
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
1981
  if (showdown) {
@@ -2037,11 +1983,11 @@ pub const Effects = struct {
2037
1983
  const p: Player = @enumFromInt(i);
2038
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
1985
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
1987
  s.stored().status = 0;
2042
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2043
1989
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
1991
  }
2046
1992
  try clearVolatiles(battle, p, p != player, options);
2047
1993
  }
@@ -2050,7 +1996,7 @@ pub const Effects = struct {
2050
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
1997
  foe.last_selected_move = .SKIP_TURN;
2052
1998
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2000
  foe_stored.status = 0;
2055
2001
  }
2056
2002
  try clearVolatiles(battle, player, false, options);
@@ -2066,7 +2012,7 @@ pub const Effects = struct {
2066
2012
  // GLITCH: HP recovery move failure glitches
2067
2013
  const delta = stored.stats.hp - stored.hp;
2068
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2015
+ return options.log.fail(.{ ident, .None });
2070
2016
  }
2071
2017
 
2072
2018
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2020,17 @@ pub const Effects = struct {
2074
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2021
  if (showdown) battle.rng.advance(1);
2076
2022
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2024
  stored.hp = stored.stats.hp;
2079
2025
  } else {
2080
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2027
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2028
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2029
  }
2084
2030
 
2085
2031
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2032
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2034
  }
2089
2035
 
2090
2036
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2039,32 @@ pub const Effects = struct {
2093
2039
  if (showdown) {
2094
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2043
  }
2098
2044
  if (!try checkHit(battle, player, move, options)) return;
2099
2045
  if (foe.active.volatiles.LeechSeed) return;
2100
2046
  } else {
2101
2047
  if (!try checkHit(battle, player, move, options)) return;
2102
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
2105
2051
  }
2106
2052
  }
2107
2053
 
2108
2054
  try options.chance.commit(player, .hit);
2109
2055
  foe.active.volatiles.LeechSeed = true;
2110
2056
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2058
  }
2113
2059
 
2114
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2116
2063
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2065
  side.active.volatiles.LightScreen = true;
2121
2066
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2123
2068
  }
2124
2069
 
2125
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2148,7 +2093,7 @@ pub const Effects = struct {
2148
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2094
  if (!try checkHit(battle, player, move, options)) return;
2150
2095
  try options.chance.commit(player, .hit);
2151
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2097
  }
2153
2098
  }
2154
2099
  if (!try checkHit(battle, player, move, options)) return;
@@ -2157,7 +2102,7 @@ pub const Effects = struct {
2157
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2104
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2105
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2106
  }
2162
2107
 
2163
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2111,7 @@ pub const Effects = struct {
2166
2111
  if (side.active.volatiles.Mist) return;
2167
2112
  side.active.volatiles.Mist = true;
2168
2113
 
2169
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2115
  }
2171
2116
 
2172
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,17 +2137,18 @@ pub const Effects = struct {
2192
2137
 
2193
2138
  if (showdown) {
2194
2139
  // Invulnerability trumps type immunity on Pokémon Showdown
2195
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2196
- if (!try checkHit(battle, player, move, options)) return;
2140
+ if (immune and !foe.active.volatiles.Invulnerable) {
2141
+ return log.immune(.{ foe_ident, .None });
2142
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2143
  }
2198
2144
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2145
+ return log.fail(.{
2200
2146
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2203
2149
  }
2204
2150
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2152
  if (!try checkHit(battle, player, move, options)) return;
2207
2153
  }
2208
2154
  try options.chance.commit(player, .hit);
@@ -2210,7 +2156,7 @@ pub const Effects = struct {
2210
2156
  foe_stored.status = Status.init(.PAR);
2211
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2158
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2160
  }
2215
2161
 
2216
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2178,12 @@ pub const Effects = struct {
2232
2178
  foe_stored.status = Status.init(.PAR);
2233
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2180
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2181
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2182
  }
2237
2183
 
2238
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2185
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2186
+ try options.log.fieldactivate(.{});
2241
2187
  }
2242
2188
  }
2243
2189
 
@@ -2252,23 +2198,23 @@ pub const Effects = struct {
2252
2198
  return;
2253
2199
  } else if (foe.active.volatiles.Substitute) {
2254
2200
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2256
2202
  } else if (Status.any(foe_stored.status)) {
2257
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2205
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2260
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2206
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2207
  if (!showdown)
2262
- .Poison
2208
+ Fail.Poison
2263
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2211
  else
2266
- .None
2212
+ Fail.None
2267
2213
  else
2268
- .None);
2214
+ Fail.None });
2269
2215
  } else if (foe.active.types.includes(.Poison)) {
2270
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2272
2218
  }
2273
2219
 
2274
2220
  if (move.effect == .Poison) {
@@ -2287,7 +2233,7 @@ pub const Effects = struct {
2287
2233
  foe.active.volatiles.toxic = 0;
2288
2234
  }
2289
2235
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2237
  }
2292
2238
 
2293
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2253,23 @@ pub const Effects = struct {
2307
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2254
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2255
 
2310
- try options.log.damageOf(
2256
+ try options.log.damage(.{
2311
2257
  battle.active(player),
2312
2258
  stored,
2313
2259
  .RecoilOf,
2314
2260
  battle.active(player.foe()),
2315
- );
2261
+ });
2316
2262
  if (showdown and stored.hp == 0) residual.* = false;
2317
2263
  }
2318
2264
 
2319
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2321
2268
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2269
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2270
  side.active.volatiles.Reflect = true;
2324
2271
 
2325
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2326
2273
  }
2327
2274
 
2328
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,10 +2283,10 @@ pub const Effects = struct {
2336
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2284
  } else {
2338
2285
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2286
+ return options.log.fail(.{
2340
2287
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2343
2290
  }
2344
2291
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2356,18 +2303,17 @@ pub const Effects = struct {
2356
2303
 
2357
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2358
2305
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2307
  }
2361
2308
 
2362
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2311
  }
2365
2312
 
2366
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2314
  var side = battle.side(player);
2368
2315
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2317
  }
2372
2318
 
2373
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2325,32 @@ pub const Effects = struct {
2379
2325
  // avoid having to convert to floating point)
2380
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2327
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2329
  }
2385
2330
 
2386
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2332
  side.stored().hp -= hp;
2388
2333
  side.active.volatiles.substitute = hp + 1;
2389
2334
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2336
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2339
  }
2395
2340
  }
2396
2341
 
2397
2342
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2398
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2399
-
2400
2343
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2401
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2402
-
2403
- battle.last_damage = 0;
2344
+ if (showdown) {
2345
+ if (battle.side(player).last_selected_move == .Teleport) return;
2346
+ if (try checkHit(battle, player, move, options)) {
2347
+ try options.chance.commit(player, .hit);
2348
+ }
2349
+ battle.last_damage = 0;
2350
+ } else {
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2353
+ }
2404
2354
  }
2405
2355
 
2406
2356
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2438,7 +2388,7 @@ pub const Effects = struct {
2438
2388
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2389
  }
2440
2390
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2391
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2392
  }
2443
2393
 
2444
2394
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2468,87 +2418,87 @@ pub const Effects = struct {
2468
2418
  switch (move.effect) {
2469
2419
  .AttackUp1, .AttackUp2, .Rage => {
2470
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2422
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2423
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2425
  if (stats.atk == MAX_STAT_VALUE) {
2476
2426
  boosts.atk -= 1;
2477
2427
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2428
+ try log.boost(.{ ident, reason, n });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2480
2430
  if (move.effect == .Rage) return;
2481
2431
  }
2482
- return try log.fail(ident, .None);
2432
+ return log.fail(.{ ident, .None });
2483
2433
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2435
  const stat = unmodifiedStats(battle, side).atk;
2486
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2437
+ try log.boost(.{ ident, reason, n });
2488
2438
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2439
  if (showdown and move.effect == .Rage) return;
2490
2440
  },
2491
2441
  .DefenseUp1, .DefenseUp2 => {
2492
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2445
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2446
  if (stats.def == MAX_STAT_VALUE) {
2497
2447
  boosts.def -= 1;
2498
2448
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2501
2451
  }
2502
- return try log.fail(ident, .None);
2452
+ return log.fail(.{ ident, .None });
2503
2453
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2455
  const stat = unmodifiedStats(battle, side).def;
2506
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2457
+ try log.boost(.{ ident, .Defense, n });
2508
2458
  },
2509
2459
  .SpeedUp2 => {
2510
2460
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2462
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2463
  if (stats.spe == MAX_STAT_VALUE) {
2514
2464
  boosts.spe -= 1;
2515
2465
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2518
2468
  }
2519
- return try log.fail(ident, .None);
2469
+ return log.fail(.{ ident, .None });
2520
2470
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2472
  const stat = unmodifiedStats(battle, side).spe;
2523
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2474
+ try log.boost(.{ ident, .Speed, 2 });
2525
2475
  },
2526
2476
  .SpecialUp1, .SpecialUp2 => {
2527
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2480
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2481
  if (stats.spc == MAX_STAT_VALUE) {
2532
2482
  boosts.spc -= 1;
2533
2483
  if (showdown) {
2534
- try log.boost(ident, .SpecialAttack, n);
2535
- try log.boost(ident, .SpecialAttack, -1);
2536
- try log.boost(ident, .SpecialDefense, n);
2537
- try log.boost(ident, .SpecialDefense, -1);
2484
+ try log.boost(.{ ident, .SpecialAttack, n });
2485
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2486
+ try log.boost(.{ ident, .SpecialDefense, n });
2487
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2538
2488
  }
2539
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2540
2490
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2492
  const stat = unmodifiedStats(battle, side).spc;
2543
2493
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2544
- try log.boost(ident, .SpecialAttack, n);
2545
- try log.boost(ident, .SpecialDefense, n);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2496
  },
2547
2497
  .EvasionUp1 => {
2548
2498
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2500
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2502
  },
2553
2503
  else => unreachable,
2554
2504
  }
@@ -2563,7 +2513,7 @@ pub const Effects = struct {
2563
2513
  const foe_ident = battle.active(player.foe());
2564
2514
  const secondary = move.effect.isStatDownChance();
2565
2515
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2516
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2517
 
2568
2518
  if (secondary) {
2569
2519
  const proc = try Rolls.unboost(battle, player, options);
@@ -2581,82 +2531,82 @@ pub const Effects = struct {
2581
2531
  switch (move.effect) {
2582
2532
  .AttackDown1, .AttackDownChance => {
2583
2533
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2534
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2535
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2536
  if (stats.atk == 1) {
2587
2537
  boosts.atk += 1;
2588
2538
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2541
  }
2592
- return try log.fail(foe_ident, .None);
2542
+ return log.fail(.{ foe_ident, .None });
2593
2543
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2545
  const stat = unmodifiedStats(battle, foe).atk;
2596
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2548
  },
2599
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2551
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2553
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2554
  if (stats.def == 1) {
2605
2555
  boosts.def += 1;
2606
2556
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2557
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2558
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2559
  }
2610
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2611
2561
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2563
  const stat = unmodifiedStats(battle, foe).def;
2614
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2566
  },
2617
2567
  .SpeedDown1, .SpeedDownChance => {
2618
2568
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2569
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2570
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2571
  if (stats.spe == 1) {
2622
2572
  boosts.spe += 1;
2623
2573
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2576
  }
2627
- return try log.fail(foe_ident, .None);
2577
+ return log.fail(.{ foe_ident, .None });
2628
2578
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2580
  const stat = unmodifiedStats(battle, foe).spe;
2631
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2583
  assert(boosts.spe >= -6);
2634
2584
  },
2635
2585
  .SpecialDownChance => {
2636
2586
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2587
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2588
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2589
  if (stats.spc == 1) {
2640
2590
  boosts.spc += 1;
2641
2591
  if (showdown) {
2642
- try log.boost(foe_ident, .SpecialAttack, -1);
2643
- try log.boost(foe_ident, .SpecialAttack, 1);
2644
- try log.boost(foe_ident, .SpecialDefense, -1);
2645
- try log.boost(foe_ident, .SpecialDefense, 1);
2592
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2593
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2594
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2595
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2596
  }
2647
- return try log.fail(foe_ident, .None);
2597
+ return log.fail(.{ foe_ident, .None });
2648
2598
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2600
  const stat = unmodifiedStats(battle, foe).spc;
2651
2601
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2652
- try log.boost(foe_ident, .SpecialAttack, -1);
2653
- try log.boost(foe_ident, .SpecialDefense, -1);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2604
  },
2655
2605
  .AccuracyDown1 => {
2656
2606
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2607
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2608
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2610
  },
2661
2611
  else => unreachable,
2662
2612
  }
@@ -2680,7 +2630,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2630
  }
2681
2631
  }
2682
2632
 
2683
- inline fn isForced(active: anytype) bool {
2633
+ fn isForced(active: anytype) bool {
2684
2634
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2635
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2636
  }
@@ -2693,27 +2643,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2693
2643
 
2694
2644
  options.chance.clearDurations(who, clear);
2695
2645
 
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2699
- try log.end(ident, .DisableSilent);
2646
+ if (volatiles.disable_move != 0) {
2647
+ volatiles.disable_move = 0;
2648
+ volatiles.disable_duration = 0;
2649
+ try log.end(.{ ident, .DisableSilent });
2700
2650
  }
2701
2651
  if (volatiles.Confusion) {
2702
2652
  // volatiles.confusion is left unchanged
2703
2653
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2654
+ try log.end(.{ ident, .ConfusionSilent });
2705
2655
  }
2706
2656
  if (volatiles.Mist) {
2707
2657
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2658
+ try log.end(.{ ident, .MistSilent });
2709
2659
  }
2710
2660
  if (volatiles.FocusEnergy) {
2711
2661
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2663
  }
2714
2664
  if (volatiles.LeechSeed) {
2715
2665
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2667
  }
2718
2668
  if (!showdown and volatiles.Toxic) {
2719
2669
  volatiles.Toxic = false;
@@ -2721,22 +2671,22 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2671
  }
2722
2672
  if (volatiles.LightScreen) {
2723
2673
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2674
+ try log.end(.{ ident, .LightScreenSilent });
2725
2675
  }
2726
2676
  if (volatiles.Reflect) {
2727
2677
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2678
+ try log.end(.{ ident, .ReflectSilent });
2729
2679
  }
2730
2680
  if (showdown and volatiles.Toxic) {
2731
2681
  volatiles.Toxic = false;
2732
2682
  // Pokémon Showdown erroneously clears the toxic counter
2733
2683
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2684
+ try log.end(.{ ident, .ToxicSilent });
2735
2685
  }
2736
2686
  }
2737
2687
 
2738
2688
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2689
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2690
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2691
  player == .P1
2742
2692
  else if (showdown)
@@ -2748,7 +2698,7 @@ pub const Rolls = struct {
2748
2698
  return p1;
2749
2699
  }
2750
2700
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2701
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2752
2702
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2703
  val == .true
2754
2704
  else if (showdown)
@@ -2760,7 +2710,7 @@ pub const Rolls = struct {
2760
2710
  return crit;
2761
2711
  }
2762
2712
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2713
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2714
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2715
  val
2766
2716
  else roll: {
@@ -2776,7 +2726,7 @@ pub const Rolls = struct {
2776
2726
  return roll;
2777
2727
  }
2778
2728
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2729
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2780
2730
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2731
  val == .true
2782
2732
  else if (showdown)
@@ -2788,7 +2738,7 @@ pub const Rolls = struct {
2788
2738
  return ok;
2789
2739
  }
2790
2740
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2741
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2742
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2743
  val == .true
2794
2744
  else if (showdown)
@@ -2800,7 +2750,7 @@ pub const Rolls = struct {
2800
2750
  return cfz;
2801
2751
  }
2802
2752
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2753
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2754
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2755
  val == .true
2806
2756
  else if (showdown)
@@ -2812,7 +2762,7 @@ pub const Rolls = struct {
2812
2762
  return par;
2813
2763
  }
2814
2764
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2765
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2766
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2767
  val == .true
2818
2768
  else if (showdown)
@@ -2824,7 +2774,7 @@ pub const Rolls = struct {
2824
2774
  return proc;
2825
2775
  }
2826
2776
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2777
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2778
  const rate: u8 = if (low) 26 else 77;
2829
2779
 
2830
2780
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2838,7 +2788,7 @@ pub const Rolls = struct {
2838
2788
  return proc;
2839
2789
  }
2840
2790
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2791
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2792
  const rate: u8 = if (low) 52 else 103;
2843
2793
 
2844
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
@@ -2852,7 +2802,7 @@ pub const Rolls = struct {
2852
2802
  return proc;
2853
2803
  }
2854
2804
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2805
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2806
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2807
  val == .true
2858
2808
  else if (showdown)
@@ -2864,14 +2814,12 @@ pub const Rolls = struct {
2864
2814
  return proc;
2865
2815
  }
2866
2816
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2817
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2818
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2819
  val
2870
- else if (showdown) move: {
2871
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2872
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2873
- break :move @enumFromInt(r + mod);
2874
- } else move: {
2820
+ else if (showdown)
2821
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2822
+ else move: {
2875
2823
  while (true) {
2876
2824
  const r = battle.rng.next();
2877
2825
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,7 +2832,7 @@ pub const Rolls = struct {
2884
2832
  return move;
2885
2833
  }
2886
2834
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2835
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2836
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2837
  val - 1
2890
2838
  else if (showdown)
@@ -2901,11 +2849,11 @@ pub const Rolls = struct {
2901
2849
  return power;
2902
2850
  }
2903
2851
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2852
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2853
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2854
  @intCast(val)
2907
2855
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2856
+ battle.rng.range(u3, 1, 8)
2909
2857
  else duration: {
2910
2858
  while (true) {
2911
2859
  const r = battle.rng.next() & 7;
@@ -2918,11 +2866,11 @@ pub const Rolls = struct {
2918
2866
  return duration;
2919
2867
  }
2920
2868
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2869
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
2870
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2871
  val
2924
2872
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2873
+ battle.rng.range(u4, 1, 9)
2926
2874
  else
2927
2875
  @intCast((battle.rng.next() & 7) + 1);
2928
2876
 
@@ -2931,11 +2879,11 @@ pub const Rolls = struct {
2931
2879
  return duration;
2932
2880
  }
2933
2881
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2882
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2935
2883
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2884
  @intCast(val)
2937
2885
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2886
+ battle.rng.range(u3, 2, 6)
2939
2887
  else
2940
2888
  @intCast((battle.rng.next() & 3) + 2);
2941
2889
 
@@ -2944,11 +2892,11 @@ pub const Rolls = struct {
2944
2892
  return duration;
2945
2893
  }
2946
2894
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2895
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
2896
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
2897
  @intCast(val)
2950
2898
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
2899
+ battle.rng.range(u3, 2, 4)
2952
2900
  else
2953
2901
  @intCast((battle.rng.next() & 1) + 2);
2954
2902
 
@@ -2957,7 +2905,9 @@ pub const Rolls = struct {
2957
2905
  return duration;
2958
2906
  }
2959
2907
 
2960
- inline fn distribution(
2908
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2909
+
2910
+ fn distribution(
2961
2911
  battle: anytype,
2962
2912
  comptime effect: Move.Effect,
2963
2913
  player: Player,
@@ -2967,7 +2917,7 @@ pub const Rolls = struct {
2967
2917
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
2918
  @intCast(val)
2969
2919
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2920
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
2921
  else n: {
2972
2922
  const r = (battle.rng.next() & 3);
2973
2923
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3014,10 +2964,9 @@ pub const Rolls = struct {
3014
2964
  while (i < moves.len and r > 0) : (i += 1) {
3015
2965
  if (moves[i].pp > 0) {
3016
2966
  r -= 1;
3017
- if (r == 0) break;
2967
+ if (r == 0) break :slot @intCast(i + 1);
3018
2968
  }
3019
2969
  }
3020
- break :slot @intCast(i + 1);
3021
2970
  }
3022
2971
  }
3023
2972
 
@@ -3039,11 +2988,11 @@ test "RNG agreement" {
3039
2988
  expected[i] = @intCast(i * 0x1000000);
3040
2989
  }
3041
2990
 
3042
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3043
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3044
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3045
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3046
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
2991
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2992
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2993
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2994
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
2995
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
2996
 
3048
2997
  for (0..expected.len) |i| {
3049
2998
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,7 +3028,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3028
  const side = battle.side(player);
3080
3029
  const foe = battle.foe(player);
3081
3030
 
3082
- var active = &side.active;
3031
+ const active = &side.active;
3083
3032
  const stored = side.stored();
3084
3033
 
3085
3034
  // While players are not given any input options on the cartridge in these cases,
@@ -3133,7 +3082,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3082
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3083
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3084
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3085
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3086
  const s = if (struggle) 0 else slot;
3138
3087
  out[n] = .{ .type = .Move, .data = s };
3139
3088
  n += 1;
@@ -3148,7 +3097,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3097
  const m = active.moves[slot - 1];
3149
3098
  if (m.id == .None) break;
3150
3099
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3100
+ if (active.volatiles.disable_move == slot) continue;
3152
3101
  out[n] = .{ .type = .Move, .data = slot };
3153
3102
  n += 1;
3154
3103
  }