@pkmn/engine 0.1.0-dev.305a932f → 0.1.0-dev.32adfe77

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/LICENSE +1 -3
  2. package/README.md +37 -14
  3. package/build/pkg/addon.js.map +1 -1
  4. package/build/pkg/data/layout.json +2 -2
  5. package/build/pkg/data.d.ts +3 -2
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js +2 -2
  8. package/build/pkg/gen1.js.map +1 -1
  9. package/build/pkg/index.d.ts +2 -1
  10. package/build/pkg/index.js.map +1 -1
  11. package/build/pkg/protocol.js +1 -1
  12. package/build/pkg/protocol.js.map +1 -1
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +3 -1
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +78 -68
  17. package/package.json +34 -23
  18. package/src/bin/install-pkmn-engine +14 -14
  19. package/src/lib/common/data.zig +8 -8
  20. package/src/lib/common/optional.zig +9 -2
  21. package/src/lib/common/protocol.zig +402 -383
  22. package/src/lib/common/rational.zig +11 -7
  23. package/src/lib/common/rng.zig +20 -11
  24. package/src/lib/gen1/README.md +28 -28
  25. package/src/lib/gen1/calc.zig +7 -7
  26. package/src/lib/gen1/chance.zig +70 -68
  27. package/src/lib/gen1/data/moves.zig +24 -10
  28. package/src/lib/gen1/data/species.zig +3 -3
  29. package/src/lib/gen1/data/types.zig +5 -5
  30. package/src/lib/gen1/data.zig +36 -36
  31. package/src/lib/gen1/helpers.zig +21 -36
  32. package/src/lib/gen1/mechanics.zig +333 -344
  33. package/src/lib/gen2/README.md +1 -2
  34. package/src/lib/gen2/calc.zig +143 -0
  35. package/src/lib/gen2/chance.zig +1314 -0
  36. package/src/lib/gen2/data/items.zig +3 -3
  37. package/src/lib/gen2/data/moves.zig +32 -15
  38. package/src/lib/gen2/data/species.zig +1 -1
  39. package/src/lib/gen2/data/types.zig +75 -28
  40. package/src/lib/gen2/data.zig +37 -25
  41. package/src/lib/gen2/generated.zig +806 -0
  42. package/src/lib/gen2/mechanics.zig +2612 -349
  43. package/src/lib/{binding/node.zig → node.zig} +9 -12
  44. package/src/lib/pkmn.zig +26 -3
  45. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
@@ -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,30 +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
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
66
63
  if (battle.turn == 0) return start(battle, options);
67
64
 
68
65
  var s1 = false;
69
66
  var s2 = false;
70
- var f1: ?Move = null;
71
- var f2: ?Move = null;
72
67
 
73
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
74
- 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;
75
70
 
76
71
  var p1 = battle.side(.P1);
77
72
  var p2 = battle.side(.P2);
78
73
 
79
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
80
- 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;
81
76
 
82
77
  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;
78
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
84
79
  } else {
85
- 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;
86
81
  }
87
82
 
88
83
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -95,11 +90,11 @@ fn start(battle: anytype, options: anytype) !Result {
95
90
  const p1 = battle.side(.P1);
96
91
  const p2 = battle.side(.P2);
97
92
 
98
- var p1_slot = findFirstAlive(p1);
93
+ const p1_slot = findFirstAlive(p1);
99
94
  assert(!showdown or p1_slot == 1);
100
95
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
101
96
 
102
- var p2_slot = findFirstAlive(p2);
97
+ const p2_slot = findFirstAlive(p2);
103
98
  assert(!showdown or p2_slot == 1);
104
99
  if (p2_slot == 0) return Result.Win;
105
100
 
@@ -120,7 +115,6 @@ fn selectMove(
120
115
  choice: Choice,
121
116
  foe_choice: Choice,
122
117
  skip_turn: *bool,
123
- from: *?Move,
124
118
  ) ?Result {
125
119
  if (choice.type == .Pass) return null;
126
120
 
@@ -137,7 +131,6 @@ fn selectMove(
137
131
  return null;
138
132
  }
139
133
  if (volatiles.Rage) {
140
- from.* = side.last_used_move;
141
134
  if (showdown) {
142
135
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
143
136
  saveMove(battle, player, null);
@@ -147,7 +140,6 @@ fn selectMove(
147
140
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
148
141
  if (!showdown) volatiles.Flinch = false;
149
142
  if (volatiles.Thrashing or volatiles.Charging) {
150
- from.* = side.last_selected_move;
151
143
  if (showdown) {
152
144
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
153
145
  saveMove(battle, player, null);
@@ -168,9 +160,7 @@ fn selectMove(
168
160
  return null;
169
161
  }
170
162
  if (volatiles.Binding) {
171
- from.* = side.last_used_move;
172
163
  if (showdown) {
173
- if (foe_choice.type == .Switch) from.* = null;
174
164
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
175
165
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
176
166
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -206,7 +196,7 @@ fn selectMove(
206
196
  if (choice.data == 0) {
207
197
  const struggle = ok: {
208
198
  for (side.active.moves, 0..) |move, i| {
209
- 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;
210
200
  }
211
201
  break :ok true;
212
202
  };
@@ -225,7 +215,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
225
215
  if (c.data == 0) {
226
216
  side.last_selected_move = .Struggle;
227
217
  } else {
228
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
229
219
  const move = side.active.move(c.data);
230
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
231
221
  // sometimes required...), but a 0 PP move can be used automatically
@@ -266,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
266
256
 
267
257
  foe.active.volatiles.Binding = false;
268
258
 
269
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
270
260
  options.chance.switched(player, side.order[0], out);
271
261
 
272
262
  if (showdown and incoming.status == Status.TOX) {
273
263
  incoming.status = Status.init(.PSN);
274
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
275
265
  // rather not clutter up turnOrder just for this (incorrect) log message
276
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
277
267
  }
278
268
  }
279
269
 
@@ -336,12 +326,10 @@ fn doTurn(
336
326
  player_choice: Choice,
337
327
  player_rewrap: bool,
338
328
  player_skip: bool,
339
- player_from: ?Move,
340
329
  foe_player: Player,
341
330
  foe_choice: Choice,
342
331
  foe_rewrap: bool,
343
332
  foe_skip: bool,
344
- foe_from: ?Move,
345
333
  options: anytype,
346
334
  ) !?Result {
347
335
  assert(player_choice.type != .Pass);
@@ -354,7 +342,6 @@ fn doTurn(
354
342
  player_choice,
355
343
  player_rewrap,
356
344
  player_skip,
357
- player_from,
358
345
  &residual,
359
346
  options,
360
347
  )) |r| return r;
@@ -377,7 +364,6 @@ fn doTurn(
377
364
  foe_choice,
378
365
  foe_rewrap,
379
366
  foe_skip,
380
- foe_from,
381
367
  &residual,
382
368
  options,
383
369
  )) |r| return r;
@@ -405,7 +391,6 @@ fn executeMove(
405
391
  choice: Choice,
406
392
  rewrap: bool,
407
393
  skip: bool,
408
- from: ?Move,
409
394
  residual: *bool,
410
395
  options: anytype,
411
396
  ) !?Result {
@@ -422,7 +407,7 @@ fn executeMove(
422
407
  if (side.last_selected_move == .SKIP_TURN) {
423
408
  assert(!showdown);
424
409
  if (battle.foe(player).active.volatiles.Binding) {
425
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
426
411
  }
427
412
  return null;
428
413
  }
@@ -440,8 +425,10 @@ fn executeMove(
440
425
  mslot = @intCast(battle.lastMove(player).index);
441
426
  const stored = side.stored();
442
427
  // 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);
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);
445
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
446
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
447
434
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -463,7 +450,7 @@ fn executeMove(
463
450
 
464
451
  var skip_can = false;
465
452
  var skip_pp = false;
466
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
467
454
  .done => return null,
468
455
  .skip_can => skip_can = true,
469
456
  .skip_pp => skip_pp = true,
@@ -471,9 +458,9 @@ fn executeMove(
471
458
  .err => return @as(?Result, Result.Error),
472
459
  }
473
460
 
474
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
475
- return null;
476
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
477
464
 
478
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
479
466
  }
@@ -484,7 +471,6 @@ fn beforeMove(
484
471
  battle: anytype,
485
472
  player: Player,
486
473
  skip: bool,
487
- from: ?Move,
488
474
  residual: *bool,
489
475
  options: anytype,
490
476
  ) !BeforeMove {
@@ -511,23 +497,23 @@ fn beforeMove(
511
497
  }
512
498
 
513
499
  if (duration == 0) {
514
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
515
501
  stored.status = 0; // clears EXT if present
516
502
  } else {
517
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
518
504
  }
519
505
  side.last_used_move = .None;
520
506
  return .done;
521
507
  }
522
508
 
523
509
  if (Status.is(stored.status, .FRZ)) {
524
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
525
511
  side.last_used_move = .None;
526
512
  return .done;
527
513
  }
528
514
 
529
515
  if (skip or foe.active.volatiles.Binding) {
530
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
531
517
  return .done;
532
518
  }
533
519
 
@@ -535,27 +521,27 @@ fn beforeMove(
535
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
536
522
  // Pokémon can sometimes flinch multiple times from the same original hit
537
523
  if (!showdown) volatiles.Flinch = false;
538
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
539
525
  return .done;
540
526
  }
541
527
 
542
528
  if (volatiles.Recharging) {
543
529
  volatiles.Recharging = false;
544
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
545
531
  return .done;
546
532
  }
547
533
 
548
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
549
535
  if (options.calc.modify(player, .disable)) |extend| {
550
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
551
537
  } else {
552
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
553
539
  }
554
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
555
541
 
556
- if (volatiles.disabled_duration == 0) {
557
- volatiles.disabled_move = 0;
558
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
559
545
  }
560
546
  }
561
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -580,9 +566,9 @@ fn beforeMove(
580
566
 
581
567
  if (volatiles.confusion == 0) {
582
568
  volatiles.Confusion = false;
583
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
584
570
  } else {
585
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
586
572
 
587
573
  if (try Rolls.confused(battle, player, options)) {
588
574
  assert(!volatiles.MultiHit);
@@ -633,7 +619,7 @@ fn beforeMove(
633
619
  volatiles.Binding = false;
634
620
  options.chance.clearDurations(player, null);
635
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
636
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
637
623
  return .done;
638
624
  }
639
625
 
@@ -649,8 +635,6 @@ fn beforeMove(
649
635
  volatiles.state +%= battle.last_damage;
650
636
  }
651
637
 
652
- assert(volatiles.attacks > 0);
653
-
654
638
  if (options.calc.modify(player, .attacking)) |extend| {
655
639
  if (!extend) volatiles.attacks = 0;
656
640
  } else {
@@ -659,18 +643,18 @@ fn beforeMove(
659
643
  try options.chance.attacking(player, volatiles.attacks);
660
644
 
661
645
  if (volatiles.attacks != 0) {
662
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
663
647
  return .done;
664
648
  }
665
649
 
666
650
  volatiles.Bide = false;
667
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
668
652
 
669
653
  battle.last_damage = volatiles.state *% 2;
670
654
  volatiles.state = 0;
671
655
 
672
656
  if (battle.last_damage == 0) {
673
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
674
658
  return .done;
675
659
  }
676
660
 
@@ -690,9 +674,6 @@ fn beforeMove(
690
674
  }
691
675
 
692
676
  if (volatiles.Thrashing) {
693
- assert(volatiles.attacks > 0);
694
- assert(from != null);
695
-
696
677
  if (options.calc.modify(player, .attacking)) |extend| {
697
678
  if (!extend) volatiles.attacks = 0;
698
679
  } else {
@@ -701,23 +682,21 @@ fn beforeMove(
701
682
  try options.chance.attacking(player, volatiles.attacks);
702
683
 
703
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
704
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
705
686
  }
706
- 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()) });
707
688
  if (showdown) {
708
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
709
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
710
691
  side.last_used_move = side.last_selected_move;
711
692
  if (handleThrashing(battle, active, player, options)) {
712
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
713
694
  }
714
695
  }
715
696
  return .skip_can;
716
697
  }
717
698
 
718
699
  if (volatiles.Binding) {
719
- assert(volatiles.attacks > 0);
720
-
721
700
  if (options.calc.modify(player, .binding)) |extend| {
722
701
  if (!extend) volatiles.attacks = 0;
723
702
  } else {
@@ -725,12 +704,7 @@ fn beforeMove(
725
704
  }
726
705
  try options.chance.binding(player, volatiles.attacks);
727
706
 
728
- try log.move(
729
- battle.active(player),
730
- side.last_selected_move,
731
- battle.active(player.foe()),
732
- side.last_selected_move,
733
- );
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
734
708
  if (showdown or battle.last_damage != 0) {
735
709
  const sub = showdown and foe.active.volatiles.Substitute;
736
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -750,7 +724,7 @@ fn canMove(
750
724
  mslot: u4,
751
725
  auto: bool,
752
726
  skip_pp: bool,
753
- from: ?Move,
727
+ from: Move,
754
728
  residual: *bool,
755
729
  options: anytype,
756
730
  ) !bool {
@@ -758,33 +732,21 @@ fn canMove(
758
732
  const player_ident = battle.active(player);
759
733
  const move = Move.get(side.last_selected_move);
760
734
 
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
- var skip = skip_pp;
766
735
  if (side.active.volatiles.Charging) {
767
736
  side.active.volatiles.Charging = false;
768
737
  side.active.volatiles.Invulnerable = false;
769
738
  } else if (move.effect == .Charge) {
770
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
771
740
  setCounterable(battle, player, side, move);
772
741
  try Effects.charge(battle, player, options);
773
742
  return false;
774
743
  }
775
744
 
776
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
777
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
778
747
 
779
748
  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);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
788
750
  setCounterable(battle, player, side, move);
789
751
 
790
752
  if (move.effect.onBegin()) {
@@ -822,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
822
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
823
785
  if (volatiles.Bide) return;
824
786
 
825
- assert(active.move(mslot).pp > 0 or auto);
826
- 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;
827
790
  if (volatiles.Transform) return;
828
791
 
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;
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;
831
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
832
796
  }
833
797
 
@@ -931,7 +895,7 @@ fn doMove(
931
895
  }
932
896
  }
933
897
 
934
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
935
899
  miss = if (showdown or skip)
936
900
  miss
937
901
  else
@@ -959,16 +923,16 @@ fn doMove(
959
923
  ohko = (!showdown or (!immune and !invulnerable)) and
960
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
961
925
  if (ohko) {
962
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
963
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
964
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
965
929
  } else if (mist) {
966
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
967
- 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 });
968
932
  } else {
969
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
970
- try log.lastmiss();
971
- try log.miss(battle.active(player));
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
972
936
  }
973
937
  if (move.effect == .JumpKick) {
974
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1004,15 +968,15 @@ fn doMove(
1004
968
  var hit: u4 = 0;
1005
969
  while (hit < hits) {
1006
970
  if (hit == 0) {
1007
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
1008
972
  if (effectiveness > Effectiveness.neutral) {
1009
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
1010
974
  } else if (effectiveness < Effectiveness.neutral) {
1011
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
1012
976
  }
1013
977
  }
1014
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1015
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
1016
980
  hit += 1;
1017
981
  if (foe.stored().hp == 0) break;
1018
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1029,7 +993,7 @@ fn doMove(
1029
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1030
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1031
995
  }
1032
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1033
997
  } else if (showdown) {
1034
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1035
999
  if (move.effect == .Binding) {
@@ -1039,9 +1003,9 @@ fn doMove(
1039
1003
  assert(foe.stored().hp > 0);
1040
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1041
1005
  if (sub) {
1042
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1043
1007
  } else {
1044
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1045
1009
  try buildRage(battle, player.foe(), options);
1046
1010
  }
1047
1011
  return null;
@@ -1089,7 +1053,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1089
1053
  const side = battle.side(player);
1090
1054
 
1091
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1092
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1093
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1094
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1095
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
@@ -1146,9 +1110,9 @@ fn calcDamage(
1146
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1147
1111
  }
1148
1112
 
1149
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1113
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1150
1114
 
1151
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1115
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1152
1116
 
1153
1117
  if (def == 0) return false;
1154
1118
 
@@ -1239,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1239
1203
  const foe = battle.foe(player);
1240
1204
 
1241
1205
  if (battle.last_damage == 0) {
1242
- try options.log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1243
1207
  return null;
1244
1208
  }
1245
1209
 
@@ -1257,14 +1221,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1257
1221
  foe_last_selected_move.type == .Fighting);
1258
1222
 
1259
1223
  if (!used and !selected) {
1260
- try options.log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1261
1225
  return null;
1262
1226
  }
1263
1227
 
1264
1228
  if (!used or !selected) {
1265
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1266
1230
  if (!showdown) return Result.Error;
1267
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1268
1232
  return null;
1269
1233
  }
1270
1234
 
@@ -1300,20 +1264,20 @@ fn applyDamage(
1300
1264
  subbed.active.volatiles.substitute = 0;
1301
1265
  subbed.active.volatiles.Substitute = false;
1302
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1303
- try options.log.end(battle.active(sub_player), .Substitute);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1304
1268
  options.calc.capped(target_player);
1305
1269
  return true;
1306
1270
  } else {
1307
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1308
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1309
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1310
1274
  return false;
1311
1275
  }
1312
1276
  }
1313
1277
 
1314
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1315
1279
  target.stored().hp -= battle.last_damage;
1316
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1317
1281
  return false;
1318
1282
  }
1319
1283
 
@@ -1332,7 +1296,7 @@ fn mirrorMove(
1332
1296
  side.last_selected_move = foe.last_used_move;
1333
1297
 
1334
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1335
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1336
1300
  return null;
1337
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1338
1302
  incrementPP(side, mslot);
@@ -1374,12 +1338,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1374
1338
  if (mist) {
1375
1339
  assert(!showdown);
1376
1340
  const foe_ident = battle.active(player.foe());
1377
- try options.log.activate(foe_ident, .Mist);
1378
- try options.log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1379
1343
  } else {
1380
1344
  try options.chance.commit(player, .miss);
1381
- try options.log.lastmiss();
1382
- try options.log.miss(battle.active(player));
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1383
1347
  }
1384
1348
 
1385
1349
  return false;
@@ -1431,7 +1395,7 @@ fn moveHit(
1431
1395
  const overwritten = overwrite and state > 0;
1432
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1433
1397
 
1434
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1435
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1436
1400
  accuracy = accuracy * boost[0] / boost[1];
1437
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1470,9 +1434,8 @@ fn checkFaint(
1470
1434
  const foe_fainted = foe.stored().hp == 0;
1471
1435
 
1472
1436
  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;
1437
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
+ const more = player_out or foe_out;
1476
1439
 
1477
1440
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1478
1441
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1480,14 +1443,14 @@ fn checkFaint(
1480
1443
  assert(!side.active.volatiles.MultiHit);
1481
1444
  assert(!foe.active.volatiles.MultiHit);
1482
1445
 
1483
- if (tie) {
1484
- try options.log.tie();
1446
+ if (player_out and foe_out) {
1447
+ try options.log.tie(.{});
1485
1448
  return Result.Tie;
1486
1449
  } else if (player_out) {
1487
- try options.log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1488
1451
  return if (player == .P1) Result.Lose else Result.Win;
1489
1452
  } else if (foe_out) {
1490
- try options.log.win(player);
1453
+ try options.log.win(.{player});
1491
1454
  return if (player == .P1) Result.Win else Result.Lose;
1492
1455
  }
1493
1456
 
@@ -1529,7 +1492,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1529
1492
  side.stored().stats.spe;
1530
1493
  }
1531
1494
 
1532
- try options.log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1533
1496
  options.calc.capped(player);
1534
1497
  return null;
1535
1498
  }
@@ -1551,8 +1514,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1551
1514
 
1552
1515
  if (!showdown or damage > 0) {
1553
1516
  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);
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 });
1556
1519
  }
1557
1520
  }
1558
1521
 
@@ -1577,14 +1540,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1577
1540
  const amount = @min(damage, stored.hp);
1578
1541
  stored.hp -= amount;
1579
1542
 
1580
- // As above, Pokémon Showdown uses damageOf but its not relevant
1581
- 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 });
1582
1545
 
1583
1546
  const before = foe_stored.hp;
1584
1547
  // Uncapped damage is added back to the foe
1585
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1586
1549
  // 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);
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1588
1551
  }
1589
1552
  }
1590
1553
 
@@ -1595,18 +1558,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1595
1558
  battle.turn += 1;
1596
1559
 
1597
1560
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1598
- try options.log.tie();
1561
+ try options.log.tie(.{});
1599
1562
  return Result.Tie;
1600
1563
  }
1601
1564
 
1602
1565
  if (showdown and battle.turn >= 1000) {
1603
- try options.log.tie();
1566
+ try options.log.tie(.{});
1604
1567
  return Result.Tie;
1605
1568
  } else if (battle.turn >= 65535) {
1606
1569
  return Result.Error;
1607
1570
  }
1608
1571
 
1609
- try options.log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1610
1573
 
1611
1574
  return Result.Default;
1612
1575
  }
@@ -1666,9 +1629,9 @@ fn checkEBC(battle: anytype) bool {
1666
1629
  }
1667
1630
 
1668
1631
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1669
- if (side.active.volatiles.disabled_move != 0) {
1632
+ if (side.active.volatiles.disable_move != 0) {
1670
1633
  // 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;
1634
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1672
1635
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1673
1636
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1674
1637
  m
@@ -1676,14 +1639,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1676
1639
  side.last_selected_move;
1677
1640
  if (m != .None and m == last) {
1678
1641
  side.active.volatiles.Charging = false;
1679
- try options.log.disabled(ident, last);
1642
+ try options.log.cant(.{ ident, .Disable, last });
1680
1643
  return true;
1681
1644
  }
1682
1645
  }
1683
1646
  return false;
1684
1647
  }
1685
1648
 
1686
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1687
1650
  const side = battle.side(who);
1688
1651
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1689
1652
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1701,7 +1664,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1701
1664
  return true;
1702
1665
  }
1703
1666
 
1704
- inline fn onBegin(
1667
+ fn onBegin(
1705
1668
  battle: anytype,
1706
1669
  player: Player,
1707
1670
  move: Move.Data,
@@ -1709,7 +1672,6 @@ inline fn onBegin(
1709
1672
  residual: *bool,
1710
1673
  options: anytype,
1711
1674
  ) !void {
1712
- assert(move.effect.onBegin());
1713
1675
  return switch (move.effect) {
1714
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1715
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1731,8 +1693,7 @@ inline fn onBegin(
1731
1693
  };
1732
1694
  }
1733
1695
 
1734
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1735
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1736
1697
  return switch (move.effect) {
1737
1698
  .Bide => Effects.bide(battle, player, options),
1738
1699
  // zig fmt: off
@@ -1747,14 +1708,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1747
1708
  };
1748
1709
  }
1749
1710
 
1750
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1751
1712
  battle: anytype,
1752
1713
  player: Player,
1753
1714
  move: Move.Data,
1754
1715
  residual: *bool,
1755
1716
  options: anytype,
1756
1717
  ) !void {
1757
- assert(move.effect.alwaysHappens());
1758
1718
  return switch (move.effect) {
1759
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1760
1720
  .Explode => Effects.explode(battle, player),
@@ -1766,7 +1726,7 @@ inline fn alwaysHappens(
1766
1726
  };
1767
1727
  }
1768
1728
 
1769
- 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 {
1770
1730
  return switch (move.effect) {
1771
1731
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1772
1732
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1793,7 +1753,7 @@ pub const Effects = struct {
1793
1753
  side.active.volatiles.state = 0;
1794
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1795
1755
 
1796
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1797
1757
  }
1798
1758
 
1799
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1807,11 +1767,11 @@ pub const Effects = struct {
1807
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1808
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1809
1769
  assert(move.type == .Fire);
1810
- try options.log.curestatus(
1770
+ try options.log.curestatus(.{
1811
1771
  battle.active(player.foe()),
1812
1772
  foe_stored.status,
1813
1773
  .Message,
1814
- );
1774
+ });
1815
1775
  foe_stored.status = 0;
1816
1776
  }
1817
1777
  return;
@@ -1825,7 +1785,7 @@ pub const Effects = struct {
1825
1785
  foe_stored.status = Status.init(.BRN);
1826
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1827
1787
 
1828
- 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 });
1829
1789
  }
1830
1790
 
1831
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1835,8 +1795,8 @@ pub const Effects = struct {
1835
1795
  volatiles.Charging = true;
1836
1796
  const move = side.last_selected_move;
1837
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1838
- try options.log.laststill();
1839
- try options.log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1840
1800
  }
1841
1801
 
1842
1802
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1847,17 +1807,11 @@ pub const Effects = struct {
1847
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1848
1808
  } else {
1849
1809
  if (showdown) {
1850
- if (!try checkHit(battle, player, move, options)) {
1851
- return;
1852
- } else if (sub) {
1853
- return options.log.fail(battle.active(player.foe()), .None);
1854
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1855
1812
  } else {
1856
- if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1858
- } else if (!try checkHit(battle, player, move, options)) {
1859
- return;
1860
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1861
1815
  }
1862
1816
  try options.chance.commit(player, .hit);
1863
1817
  }
@@ -1866,23 +1820,24 @@ pub const Effects = struct {
1866
1820
  foe.active.volatiles.Confusion = true;
1867
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1868
1822
 
1869
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1870
1824
  }
1871
1825
 
1872
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1873
1827
  const foe = battle.foe(player);
1874
1828
 
1875
1829
  if (foe.active.volatiles.Invulnerable) {
1876
- try options.log.lastmiss();
1877
- return options.log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1878
1832
  }
1879
1833
 
1880
1834
  battle.side(player).active.types = foe.active.types;
1881
- return options.log.typechange(
1835
+ return options.log.start(.{
1882
1836
  battle.active(player),
1837
+ .TypeChange,
1883
1838
  foe.active.types,
1884
1839
  battle.active(player.foe()),
1885
- );
1840
+ });
1886
1841
  }
1887
1842
 
1888
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1896,8 +1851,8 @@ pub const Effects = struct {
1896
1851
  try options.chance.commit(player, .hit);
1897
1852
  }
1898
1853
 
1899
- if (volatiles.disabled_move != 0) {
1900
- try options.log.fail(foe_ident, .None);
1854
+ if (volatiles.disable_move != 0) {
1855
+ try options.log.fail(.{ foe_ident, .None });
1901
1856
  return null;
1902
1857
  }
1903
1858
 
@@ -1913,7 +1868,7 @@ pub const Effects = struct {
1913
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1914
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1915
1870
  if (n == 0) {
1916
- try options.log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1917
1872
  return null;
1918
1873
  } else if (err) {
1919
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1921,18 +1876,18 @@ pub const Effects = struct {
1921
1876
  return Result.Error;
1922
1877
  }
1923
1878
 
1924
- volatiles.disabled_move =
1879
+ volatiles.disable_move =
1925
1880
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1926
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1881
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1927
1882
 
1928
- const id = foe.active.move(volatiles.disabled_move).id;
1929
- 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 });
1930
1885
  return null;
1931
1886
  }
1932
1887
 
1933
1888
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1934
- var side = battle.side(player);
1935
- var stored = side.stored();
1889
+ var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1936
1891
 
1937
1892
  if (battle.last_damage == 0) {
1938
1893
  assert(showdown);
@@ -1945,7 +1900,7 @@ pub const Effects = struct {
1945
1900
  if (stored.hp == stored.stats.hp) return;
1946
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1947
1902
 
1948
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1949
1904
  }
1950
1905
 
1951
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1977,7 +1932,7 @@ pub const Effects = struct {
1977
1932
  if (side.active.volatiles.FocusEnergy) return;
1978
1933
  side.active.volatiles.FocusEnergy = true;
1979
1934
 
1980
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1981
1936
  }
1982
1937
 
1983
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1999,7 +1954,7 @@ pub const Effects = struct {
1999
1954
  foe_stored.status = Status.init(.FRZ);
2000
1955
  // GLITCH: Hyper Beam recharging status is not cleared
2001
1956
 
2002
- try options.log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2003
1958
  }
2004
1959
 
2005
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2007,7 +1962,7 @@ pub const Effects = struct {
2007
1962
  var side = battle.side(player);
2008
1963
  var foe = battle.foe(player);
2009
1964
 
2010
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
2011
1966
  var foe_stored = foe.stored();
2012
1967
 
2013
1968
  const player_ident = battle.active(player);
@@ -2019,8 +1974,8 @@ pub const Effects = struct {
2019
1974
  side.active.stats = side_stored.stats;
2020
1975
  foe.active.stats = foe_stored.stats;
2021
1976
 
2022
- try log.activate(player_ident, .Haze);
2023
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
2024
1979
 
2025
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2026
1981
  if (showdown) {
@@ -2028,11 +1983,11 @@ pub const Effects = struct {
2028
1983
  const p: Player = @enumFromInt(i);
2029
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2030
1985
  if (p != player and Status.any(s.stored().status)) {
2031
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2032
1987
  s.stored().status = 0;
2033
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2034
1989
  s.stored().status = Status.init(.PSN);
2035
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2036
1991
  }
2037
1992
  try clearVolatiles(battle, p, p != player, options);
2038
1993
  }
@@ -2041,7 +1996,7 @@ pub const Effects = struct {
2041
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2042
1997
  foe.last_selected_move = .SKIP_TURN;
2043
1998
  }
2044
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2045
2000
  foe_stored.status = 0;
2046
2001
  }
2047
2002
  try clearVolatiles(battle, player, false, options);
@@ -2057,7 +2012,7 @@ pub const Effects = struct {
2057
2012
  // GLITCH: HP recovery move failure glitches
2058
2013
  const delta = stored.stats.hp - stored.hp;
2059
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2060
- return try options.log.fail(ident, .None);
2015
+ return options.log.fail(.{ ident, .None });
2061
2016
  }
2062
2017
 
2063
2018
  const rest = side.last_selected_move == .Rest;
@@ -2065,17 +2020,17 @@ pub const Effects = struct {
2065
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2066
2021
  if (showdown) battle.rng.advance(1);
2067
2022
  stored.status = Status.slf(2);
2068
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2069
2024
  stored.hp = stored.stats.hp;
2070
2025
  } else {
2071
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2072
2027
  }
2073
- 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 });
2074
2029
  }
2075
2030
 
2076
2031
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2077
2032
  battle.side(player).active.volatiles.Recharging = true;
2078
- try options.log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
2079
2034
  }
2080
2035
 
2081
2036
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2084,33 +2039,32 @@ pub const Effects = struct {
2084
2039
  if (showdown) {
2085
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2086
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2087
- return options.log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2088
2043
  }
2089
2044
  if (!try checkHit(battle, player, move, options)) return;
2090
2045
  if (foe.active.volatiles.LeechSeed) return;
2091
2046
  } else {
2092
2047
  if (!try checkHit(battle, player, move, options)) return;
2093
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2094
- try options.log.lastmiss();
2095
- return options.log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
2096
2051
  }
2097
2052
  }
2098
2053
 
2099
2054
  try options.chance.commit(player, .hit);
2100
2055
  foe.active.volatiles.LeechSeed = true;
2101
2056
 
2102
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2103
2058
  }
2104
2059
 
2105
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2106
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2107
2063
 
2108
- if (side.active.volatiles.LightScreen) {
2109
- return options.log.fail(battle.active(player), .None);
2110
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2111
2065
  side.active.volatiles.LightScreen = true;
2112
2066
 
2113
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2114
2068
  }
2115
2069
 
2116
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2139,7 +2093,7 @@ pub const Effects = struct {
2139
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2140
2094
  if (!try checkHit(battle, player, move, options)) return;
2141
2095
  try options.chance.commit(player, .hit);
2142
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2143
2097
  }
2144
2098
  }
2145
2099
  if (!try checkHit(battle, player, move, options)) return;
@@ -2148,7 +2102,7 @@ pub const Effects = struct {
2148
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2149
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2150
2104
 
2151
- 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 });
2152
2106
  }
2153
2107
 
2154
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2157,7 +2111,7 @@ pub const Effects = struct {
2157
2111
  if (side.active.volatiles.Mist) return;
2158
2112
  side.active.volatiles.Mist = true;
2159
2113
 
2160
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2161
2115
  }
2162
2116
 
2163
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2183,17 +2137,18 @@ pub const Effects = struct {
2183
2137
 
2184
2138
  if (showdown) {
2185
2139
  // 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;
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;
2188
2143
  }
2189
2144
  if (Status.any(foe_stored.status)) {
2190
- return log.fail(
2145
+ return log.fail(.{
2191
2146
  foe_ident,
2192
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2193
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2194
2149
  }
2195
2150
  if (!showdown) {
2196
- if (immune) return log.immune(foe_ident, .None);
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2197
2152
  if (!try checkHit(battle, player, move, options)) return;
2198
2153
  }
2199
2154
  try options.chance.commit(player, .hit);
@@ -2201,7 +2156,7 @@ pub const Effects = struct {
2201
2156
  foe_stored.status = Status.init(.PAR);
2202
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2203
2158
 
2204
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2205
2160
  }
2206
2161
 
2207
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2223,12 +2178,12 @@ pub const Effects = struct {
2223
2178
  foe_stored.status = Status.init(.PAR);
2224
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2225
2180
 
2226
- 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 });
2227
2182
  }
2228
2183
 
2229
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2230
2185
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2231
- try options.log.fieldactivate();
2186
+ try options.log.fieldactivate(.{});
2232
2187
  }
2233
2188
  }
2234
2189
 
@@ -2243,23 +2198,23 @@ pub const Effects = struct {
2243
2198
  return;
2244
2199
  } else if (foe.active.volatiles.Substitute) {
2245
2200
  if (move.effect != .Poison) return;
2246
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2247
2202
  } else if (Status.any(foe_stored.status)) {
2248
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2249
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2250
2205
  // 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))
2206
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2252
2207
  if (!showdown)
2253
- .Poison
2208
+ Fail.Poison
2254
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2255
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2256
2211
  else
2257
- .None
2212
+ Fail.None
2258
2213
  else
2259
- .None);
2214
+ Fail.None });
2260
2215
  } else if (foe.active.types.includes(.Poison)) {
2261
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2262
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2263
2218
  }
2264
2219
 
2265
2220
  if (move.effect == .Poison) {
@@ -2278,7 +2233,7 @@ pub const Effects = struct {
2278
2233
  foe.active.volatiles.toxic = 0;
2279
2234
  }
2280
2235
 
2281
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2282
2237
  }
2283
2238
 
2284
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2298,22 +2253,23 @@ pub const Effects = struct {
2298
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2299
2254
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2300
2255
 
2301
- try options.log.damageOf(
2256
+ try options.log.damage(.{
2302
2257
  battle.active(player),
2303
2258
  stored,
2304
2259
  .RecoilOf,
2305
2260
  battle.active(player.foe()),
2306
- );
2261
+ });
2307
2262
  if (showdown and stored.hp == 0) residual.* = false;
2308
2263
  }
2309
2264
 
2310
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2311
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2312
2268
 
2313
- 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 });
2314
2270
  side.active.volatiles.Reflect = true;
2315
2271
 
2316
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2317
2273
  }
2318
2274
 
2319
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2327,10 +2283,10 @@ pub const Effects = struct {
2327
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2328
2284
  } else {
2329
2285
  if (Status.any(foe_stored.status)) {
2330
- return options.log.fail(
2286
+ return options.log.fail(.{
2331
2287
  foe_ident,
2332
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2333
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2334
2290
  }
2335
2291
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2336
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2347,18 +2303,17 @@ pub const Effects = struct {
2347
2303
 
2348
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2349
2305
  const last = battle.side(player).last_selected_move;
2350
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2351
2307
  }
2352
2308
 
2353
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2354
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2355
2311
  }
2356
2312
 
2357
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2358
2314
  var side = battle.side(player);
2359
2315
  if (side.active.volatiles.Substitute) {
2360
- try options.log.fail(battle.active(player), .Substitute);
2361
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2362
2317
  }
2363
2318
 
2364
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2370,17 +2325,16 @@ pub const Effects = struct {
2370
2325
  // avoid having to convert to floating point)
2371
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2372
2327
  if (side.stored().hp < required_hp) {
2373
- try options.log.fail(battle.active(player), .Weak);
2374
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2375
2329
  }
2376
2330
 
2377
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2378
2332
  side.stored().hp -= hp;
2379
2333
  side.active.volatiles.substitute = hp + 1;
2380
2334
  side.active.volatiles.Substitute = true;
2381
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2382
2336
  if (hp > 0) {
2383
- try options.log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2384
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2385
2339
  }
2386
2340
  }
@@ -2394,8 +2348,8 @@ pub const Effects = struct {
2394
2348
  }
2395
2349
  battle.last_damage = 0;
2396
2350
  } else {
2397
- try options.log.fail(battle.active(player), .None);
2398
- try options.log.laststill();
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2399
2353
  }
2400
2354
  }
2401
2355
 
@@ -2434,7 +2388,7 @@ pub const Effects = struct {
2434
2388
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2435
2389
  }
2436
2390
 
2437
- try options.log.transform(battle.active(player), foe_ident);
2391
+ try options.log.transform(.{ battle.active(player), foe_ident });
2438
2392
  }
2439
2393
 
2440
2394
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2464,87 +2418,87 @@ pub const Effects = struct {
2464
2418
  switch (move.effect) {
2465
2419
  .AttackUp1, .AttackUp2, .Rage => {
2466
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2467
- if (boosts.atk == 6) return try log.fail(ident, .None);
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2468
2422
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2469
2423
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2470
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2471
2425
  if (stats.atk == MAX_STAT_VALUE) {
2472
2426
  boosts.atk -= 1;
2473
2427
  if (showdown) {
2474
- try log.boost(ident, reason, n);
2475
- try log.boost(ident, Boost.Attack, -1);
2428
+ try log.boost(.{ ident, reason, n });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2476
2430
  if (move.effect == .Rage) return;
2477
2431
  }
2478
- return try log.fail(ident, .None);
2432
+ return log.fail(.{ ident, .None });
2479
2433
  }
2480
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2481
2435
  const stat = unmodifiedStats(battle, side).atk;
2482
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2483
- try log.boost(ident, reason, n);
2437
+ try log.boost(.{ ident, reason, n });
2484
2438
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2485
2439
  if (showdown and move.effect == .Rage) return;
2486
2440
  },
2487
2441
  .DefenseUp1, .DefenseUp2 => {
2488
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2489
- if (boosts.def == 6) return try log.fail(ident, .None);
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2490
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2491
2445
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2492
2446
  if (stats.def == MAX_STAT_VALUE) {
2493
2447
  boosts.def -= 1;
2494
2448
  if (showdown) {
2495
- try log.boost(ident, .Defense, n);
2496
- try log.boost(ident, .Defense, -1);
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2497
2451
  }
2498
- return try log.fail(ident, .None);
2452
+ return log.fail(.{ ident, .None });
2499
2453
  }
2500
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2501
2455
  const stat = unmodifiedStats(battle, side).def;
2502
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2503
- try log.boost(ident, .Defense, n);
2457
+ try log.boost(.{ ident, .Defense, n });
2504
2458
  },
2505
2459
  .SpeedUp2 => {
2506
2460
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2507
- if (boosts.spe == 6) return try log.fail(ident, .None);
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2508
2462
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2509
2463
  if (stats.spe == MAX_STAT_VALUE) {
2510
2464
  boosts.spe -= 1;
2511
2465
  if (showdown) {
2512
- try log.boost(ident, .Speed, 2);
2513
- try log.boost(ident, .Speed, -1);
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2514
2468
  }
2515
- return try log.fail(ident, .None);
2469
+ return log.fail(.{ ident, .None });
2516
2470
  }
2517
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2518
2472
  const stat = unmodifiedStats(battle, side).spe;
2519
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2520
- try log.boost(ident, .Speed, 2);
2474
+ try log.boost(.{ ident, .Speed, 2 });
2521
2475
  },
2522
2476
  .SpecialUp1, .SpecialUp2 => {
2523
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2524
- if (boosts.spc == 6) return try log.fail(ident, .None);
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2525
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2526
2480
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2527
2481
  if (stats.spc == MAX_STAT_VALUE) {
2528
2482
  boosts.spc -= 1;
2529
2483
  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);
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 });
2534
2488
  }
2535
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2536
2490
  }
2537
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2538
2492
  const stat = unmodifiedStats(battle, side).spc;
2539
2493
  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);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2542
2496
  },
2543
2497
  .EvasionUp1 => {
2544
2498
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2545
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2546
2500
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2547
- try log.boost(ident, .Evasion, 1);
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2548
2502
  },
2549
2503
  else => unreachable,
2550
2504
  }
@@ -2559,7 +2513,7 @@ pub const Effects = struct {
2559
2513
  const foe_ident = battle.active(player.foe());
2560
2514
  const secondary = move.effect.isStatDownChance();
2561
2515
 
2562
- 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 });
2563
2517
 
2564
2518
  if (secondary) {
2565
2519
  const proc = try Rolls.unboost(battle, player, options);
@@ -2577,82 +2531,82 @@ pub const Effects = struct {
2577
2531
  switch (move.effect) {
2578
2532
  .AttackDown1, .AttackDownChance => {
2579
2533
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2580
- 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 });
2581
2535
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2582
2536
  if (stats.atk == 1) {
2583
2537
  boosts.atk += 1;
2584
2538
  if (showdown) {
2585
- try log.boost(foe_ident, .Attack, -1);
2586
- try log.boost(foe_ident, .Attack, 1);
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2587
2541
  }
2588
- return try log.fail(foe_ident, .None);
2542
+ return log.fail(.{ foe_ident, .None });
2589
2543
  }
2590
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2591
2545
  const stat = unmodifiedStats(battle, foe).atk;
2592
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2593
- try log.boost(foe_ident, .Attack, -1);
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2594
2548
  },
2595
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2596
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2597
- 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 });
2598
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2599
2553
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2600
2554
  if (stats.def == 1) {
2601
2555
  boosts.def += 1;
2602
2556
  if (showdown) {
2603
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2604
- 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 });
2605
2559
  }
2606
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2607
2561
  }
2608
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2609
2563
  const stat = unmodifiedStats(battle, foe).def;
2610
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2611
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2612
2566
  },
2613
2567
  .SpeedDown1, .SpeedDownChance => {
2614
2568
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2615
- 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 });
2616
2570
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2617
2571
  if (stats.spe == 1) {
2618
2572
  boosts.spe += 1;
2619
2573
  if (showdown) {
2620
- try log.boost(foe_ident, .Speed, -1);
2621
- try log.boost(foe_ident, .Speed, 1);
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2622
2576
  }
2623
- return try log.fail(foe_ident, .None);
2577
+ return log.fail(.{ foe_ident, .None });
2624
2578
  }
2625
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2626
2580
  const stat = unmodifiedStats(battle, foe).spe;
2627
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2628
- try log.boost(foe_ident, .Speed, -1);
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2629
2583
  assert(boosts.spe >= -6);
2630
2584
  },
2631
2585
  .SpecialDownChance => {
2632
2586
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2633
- 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 });
2634
2588
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2635
2589
  if (stats.spc == 1) {
2636
2590
  boosts.spc += 1;
2637
2591
  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);
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 });
2642
2596
  }
2643
- return try log.fail(foe_ident, .None);
2597
+ return log.fail(.{ foe_ident, .None });
2644
2598
  }
2645
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2646
2600
  const stat = unmodifiedStats(battle, foe).spc;
2647
2601
  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);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2650
2604
  },
2651
2605
  .AccuracyDown1 => {
2652
2606
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2653
- 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 });
2654
2608
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2655
- try log.boost(foe_ident, .Accuracy, -1);
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2656
2610
  },
2657
2611
  else => unreachable,
2658
2612
  }
@@ -2676,7 +2630,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2676
2630
  }
2677
2631
  }
2678
2632
 
2679
- inline fn isForced(active: anytype) bool {
2633
+ fn isForced(active: anytype) bool {
2680
2634
  return active.volatiles.Recharging or active.volatiles.Rage or
2681
2635
  active.volatiles.Thrashing or active.volatiles.Charging;
2682
2636
  }
@@ -2689,27 +2643,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2689
2643
 
2690
2644
  options.chance.clearDurations(who, clear);
2691
2645
 
2692
- if (volatiles.disabled_move != 0) {
2693
- volatiles.disabled_move = 0;
2694
- volatiles.disabled_duration = 0;
2695
- 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 });
2696
2650
  }
2697
2651
  if (volatiles.Confusion) {
2698
2652
  // volatiles.confusion is left unchanged
2699
2653
  volatiles.Confusion = false;
2700
- try log.end(ident, .ConfusionSilent);
2654
+ try log.end(.{ ident, .ConfusionSilent });
2701
2655
  }
2702
2656
  if (volatiles.Mist) {
2703
2657
  volatiles.Mist = false;
2704
- try log.end(ident, .MistSilent);
2658
+ try log.end(.{ ident, .MistSilent });
2705
2659
  }
2706
2660
  if (volatiles.FocusEnergy) {
2707
2661
  volatiles.FocusEnergy = false;
2708
- try log.end(ident, .FocusEnergySilent);
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2709
2663
  }
2710
2664
  if (volatiles.LeechSeed) {
2711
2665
  volatiles.LeechSeed = false;
2712
- try log.end(ident, .LeechSeedSilent);
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2713
2667
  }
2714
2668
  if (!showdown and volatiles.Toxic) {
2715
2669
  volatiles.Toxic = false;
@@ -2717,24 +2671,26 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2717
2671
  }
2718
2672
  if (volatiles.LightScreen) {
2719
2673
  volatiles.LightScreen = false;
2720
- try log.end(ident, .LightScreenSilent);
2674
+ try log.end(.{ ident, .LightScreenSilent });
2721
2675
  }
2722
2676
  if (volatiles.Reflect) {
2723
2677
  volatiles.Reflect = false;
2724
- try log.end(ident, .ReflectSilent);
2678
+ try log.end(.{ ident, .ReflectSilent });
2725
2679
  }
2726
2680
  if (showdown and volatiles.Toxic) {
2727
2681
  volatiles.Toxic = false;
2728
2682
  // Pokémon Showdown erroneously clears the toxic counter
2729
2683
  volatiles.toxic = 0;
2730
- try log.end(ident, .ToxicSilent);
2684
+ try log.end(.{ ident, .ToxicSilent });
2731
2685
  }
2732
2686
  }
2733
2687
 
2734
2688
  pub const Rolls = struct {
2735
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2689
+ fn speedTie(battle: anytype, options: anytype) !bool {
2736
2690
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2737
2691
  player == .P1
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2738
2694
  else if (showdown)
2739
2695
  battle.rng.range(u8, 0, 2) == 0
2740
2696
  else
@@ -2744,9 +2700,11 @@ pub const Rolls = struct {
2744
2700
  return p1;
2745
2701
  }
2746
2702
 
2747
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2703
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2748
2704
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2749
2705
  val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2750
2708
  else if (showdown)
2751
2709
  battle.rng.chance(u8, rate, 256)
2752
2710
  else
@@ -2756,9 +2714,11 @@ pub const Rolls = struct {
2756
2714
  return crit;
2757
2715
  }
2758
2716
 
2759
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2717
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2760
2718
  const roll = if (options.calc.overridden(player, .damage)) |val|
2761
2719
  val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2762
2722
  else roll: {
2763
2723
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2764
2724
  while (true) {
@@ -2772,9 +2732,11 @@ pub const Rolls = struct {
2772
2732
  return roll;
2773
2733
  }
2774
2734
 
2775
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2776
2736
  const ok = if (options.calc.overridden(player, .hit)) |val|
2777
2737
  val == .true
2738
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
+ battle.rng.hit(player, accuracy)
2778
2740
  else if (showdown)
2779
2741
  battle.rng.chance(u8, accuracy, 256)
2780
2742
  else
@@ -2784,9 +2746,11 @@ pub const Rolls = struct {
2784
2746
  return ok;
2785
2747
  }
2786
2748
 
2787
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2749
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2788
2750
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2789
2751
  val == .true
2752
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2753
+ battle.rng.confused(player)
2790
2754
  else if (showdown)
2791
2755
  !battle.rng.chance(u8, 128, 256)
2792
2756
  else
@@ -2796,9 +2760,11 @@ pub const Rolls = struct {
2796
2760
  return cfz;
2797
2761
  }
2798
2762
 
2799
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2800
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2801
2765
  val == .true
2766
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2767
+ battle.rng.paralyzed(player)
2802
2768
  else if (showdown)
2803
2769
  battle.rng.chance(u8, 63, 256)
2804
2770
  else
@@ -2808,9 +2774,11 @@ pub const Rolls = struct {
2808
2774
  return par;
2809
2775
  }
2810
2776
 
2811
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2777
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2812
2778
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2813
2779
  val == .true
2780
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2781
+ battle.rng.secondaryChance(player, 25)
2814
2782
  else if (showdown)
2815
2783
  battle.rng.chance(u8, 25, 256)
2816
2784
  else
@@ -2820,11 +2788,13 @@ pub const Rolls = struct {
2820
2788
  return proc;
2821
2789
  }
2822
2790
 
2823
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2791
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2824
2792
  const rate: u8 = if (low) 26 else 77;
2825
2793
 
2826
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2827
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2797
+ battle.rng.secondaryChance(player, rate)
2828
2798
  else if (showdown)
2829
2799
  battle.rng.chance(u8, rate, 256)
2830
2800
  else
@@ -2834,11 +2804,13 @@ pub const Rolls = struct {
2834
2804
  return proc;
2835
2805
  }
2836
2806
 
2837
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2807
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2838
2808
  const rate: u8 = if (low) 52 else 103;
2839
2809
 
2840
2810
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2841
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2813
+ battle.rng.secondaryChance(player, rate)
2842
2814
  else if (showdown)
2843
2815
  battle.rng.chance(u8, rate, 256)
2844
2816
  else
@@ -2848,9 +2820,11 @@ pub const Rolls = struct {
2848
2820
  return proc;
2849
2821
  }
2850
2822
 
2851
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2823
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2852
2824
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2853
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2827
+ battle.rng.secondaryChance(player, 85)
2854
2828
  else if (showdown)
2855
2829
  battle.rng.chance(u8, 85, 256)
2856
2830
  else
@@ -2860,14 +2834,14 @@ pub const Rolls = struct {
2860
2834
  return proc;
2861
2835
  }
2862
2836
 
2863
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2837
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2864
2838
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2865
2839
  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: {
2840
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2841
+ battle.rng.metronome(player)
2842
+ else if (showdown)
2843
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2844
+ else move: {
2871
2845
  while (true) {
2872
2846
  const r = battle.rng.next();
2873
2847
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2880,9 +2854,11 @@ pub const Rolls = struct {
2880
2854
  return move;
2881
2855
  }
2882
2856
 
2883
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2857
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2884
2858
  const power = if (options.calc.overridden(player, .psywave)) |val|
2885
2859
  val - 1
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2861
+ battle.rng.psywave(player)
2886
2862
  else if (showdown)
2887
2863
  battle.rng.range(u8, 0, max)
2888
2864
  else power: {
@@ -2897,11 +2873,13 @@ pub const Rolls = struct {
2897
2873
  return power;
2898
2874
  }
2899
2875
 
2900
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2876
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2901
2877
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2902
2878
  @intCast(val)
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
+ battle.rng.sleepDuration(player)
2903
2881
  else if (showdown)
2904
- @intCast(battle.rng.range(u8, 1, 8))
2882
+ battle.rng.range(u3, 1, 8)
2905
2883
  else duration: {
2906
2884
  while (true) {
2907
2885
  const r = battle.rng.next() & 7;
@@ -2914,11 +2892,13 @@ pub const Rolls = struct {
2914
2892
  return duration;
2915
2893
  }
2916
2894
 
2917
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2918
2896
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2919
2897
  val
2898
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
+ battle.rng.disableDuration(player)
2920
2900
  else if (showdown)
2921
- @intCast(battle.rng.range(u8, 1, 9))
2901
+ battle.rng.range(u4, 1, 9)
2922
2902
  else
2923
2903
  @intCast((battle.rng.next() & 7) + 1);
2924
2904
 
@@ -2927,11 +2907,13 @@ pub const Rolls = struct {
2927
2907
  return duration;
2928
2908
  }
2929
2909
 
2930
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2910
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2931
2911
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2912
  @intCast(val)
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
+ battle.rng.confusionDuration(player, self)
2933
2915
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2916
+ battle.rng.range(u3, 2, 6)
2935
2917
  else
2936
2918
  @intCast((battle.rng.next() & 3) + 2);
2937
2919
 
@@ -2940,11 +2922,13 @@ pub const Rolls = struct {
2940
2922
  return duration;
2941
2923
  }
2942
2924
 
2943
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2925
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2944
2926
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2945
2927
  @intCast(val)
2928
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
+ battle.rng.attackingDuration(player)
2946
2930
  else if (showdown)
2947
- @intCast(battle.rng.range(u4, 2, 4))
2931
+ battle.rng.range(u3, 2, 4)
2948
2932
  else
2949
2933
  @intCast((battle.rng.next() & 1) + 2);
2950
2934
 
@@ -2953,7 +2937,9 @@ pub const Rolls = struct {
2953
2937
  return duration;
2954
2938
  }
2955
2939
 
2956
- inline fn distribution(
2940
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2941
+
2942
+ fn distribution(
2957
2943
  battle: anytype,
2958
2944
  comptime effect: Move.Effect,
2959
2945
  player: Player,
@@ -2962,8 +2948,10 @@ pub const Rolls = struct {
2962
2948
  const roll = if (effect == .Binding) .duration else .multi_hit;
2963
2949
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2964
2950
  @intCast(val)
2951
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
+ battle.rng.distribution(player)
2965
2953
  else if (showdown)
2966
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2954
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2967
2955
  else n: {
2968
2956
  const r = (battle.rng.next() & 3);
2969
2957
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2994,7 +2982,9 @@ pub const Rolls = struct {
2994
2982
  else
2995
2983
  null;
2996
2984
 
2997
- const slot: u4 = overridden orelse slot: {
2985
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
+ battle.rng.moveSlot(player, moves, check_pp)
2987
+ else slot: {
2998
2988
  if (showdown) {
2999
2989
  if (check_pp == 0) {
3000
2990
  var i: usize = moves.len;
@@ -3010,10 +3000,9 @@ pub const Rolls = struct {
3010
3000
  while (i < moves.len and r > 0) : (i += 1) {
3011
3001
  if (moves[i].pp > 0) {
3012
3002
  r -= 1;
3013
- if (r == 0) break;
3003
+ if (r == 0) break :slot @intCast(i + 1);
3014
3004
  }
3015
3005
  }
3016
- break :slot @intCast(i + 1);
3017
3006
  }
3018
3007
  }
3019
3008
 
@@ -3075,7 +3064,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3075
3064
  const side = battle.side(player);
3076
3065
  const foe = battle.foe(player);
3077
3066
 
3078
- var active = &side.active;
3067
+ const active = &side.active;
3079
3068
  const stored = side.stored();
3080
3069
 
3081
3070
  // While players are not given any input options on the cartridge in these cases,
@@ -3129,7 +3118,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3129
3118
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3130
3119
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3131
3120
  const struggle =
3132
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3121
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3133
3122
  const s = if (struggle) 0 else slot;
3134
3123
  out[n] = .{ .type = .Move, .data = s };
3135
3124
  n += 1;
@@ -3144,7 +3133,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3144
3133
  const m = active.moves[slot - 1];
3145
3134
  if (m.id == .None) break;
3146
3135
  if (m.pp == 0) continue;
3147
- if (active.volatiles.disabled_move == slot) continue;
3136
+ if (active.volatiles.disable_move == slot) continue;
3148
3137
  out[n] = .{ .type = .Move, .data = slot };
3149
3138
  n += 1;
3150
3139
  }