@pkmn/engine 0.1.0-dev.954f650d → 0.1.0-dev.95e81bfe

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 (47) 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/ids.json +12 -12
  5. package/build/pkg/data/layout.json +2 -2
  6. package/build/pkg/data/protocol.json +69 -12
  7. package/build/pkg/data.d.ts +3 -2
  8. package/build/pkg/data.js.map +1 -1
  9. package/build/pkg/gen1.js +2 -2
  10. package/build/pkg/gen1.js.map +1 -1
  11. package/build/pkg/index.d.ts +2 -1
  12. package/build/pkg/index.js.map +1 -1
  13. package/build/pkg/protocol.js +57 -38
  14. package/build/pkg/protocol.js.map +1 -1
  15. package/build/test/display.js.map +1 -1
  16. package/build/tools/debug.js +3 -1
  17. package/build/tools/debug.js.map +1 -1
  18. package/build.zig +80 -70
  19. package/package.json +43 -23
  20. package/src/bin/install-pkmn-engine +14 -14
  21. package/src/lib/common/data.zig +9 -9
  22. package/src/lib/common/optional.zig +9 -2
  23. package/src/lib/common/protocol.zig +734 -318
  24. package/src/lib/common/rational.zig +12 -8
  25. package/src/lib/common/rng.zig +25 -16
  26. package/src/lib/gen1/README.md +28 -28
  27. package/src/lib/gen1/calc.zig +9 -9
  28. package/src/lib/gen1/chance.zig +72 -48
  29. package/src/lib/gen1/data/moves.zig +24 -10
  30. package/src/lib/gen1/data/species.zig +3 -3
  31. package/src/lib/gen1/data/types.zig +7 -7
  32. package/src/lib/gen1/data.zig +41 -41
  33. package/src/lib/gen1/helpers.zig +33 -48
  34. package/src/lib/gen1/mechanics.zig +340 -355
  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 +69 -36
  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 +52 -44
  43. package/src/lib/gen2/generated.zig +806 -0
  44. package/src/lib/gen2/mechanics.zig +2935 -244
  45. package/src/lib/{binding/node.zig → node.zig} +10 -13
  46. package/src/lib/pkmn.zig +26 -3
  47. 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,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,7 +196,7 @@ fn selectMove(
210
196
  if (choice.data == 0) {
211
197
  const struggle = ok: {
212
198
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
199
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
200
  }
215
201
  break :ok true;
216
202
  };
@@ -229,7 +215,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
229
215
  if (c.data == 0) {
230
216
  side.last_selected_move = .Struggle;
231
217
  } else {
232
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
233
219
  const move = side.active.move(c.data);
234
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
235
221
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
256
 
271
257
  foe.active.volatiles.Binding = false;
272
258
 
273
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
274
260
  options.chance.switched(player, side.order[0], out);
275
261
 
276
262
  if (showdown and incoming.status == Status.TOX) {
277
263
  incoming.status = Status.init(.PSN);
278
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
279
265
  // rather not clutter up turnOrder just for this (incorrect) log message
280
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
281
267
  }
282
268
  }
283
269
 
@@ -340,12 +326,10 @@ fn doTurn(
340
326
  player_choice: Choice,
341
327
  player_rewrap: bool,
342
328
  player_skip: bool,
343
- player_from: ?Move,
344
329
  foe_player: Player,
345
330
  foe_choice: Choice,
346
331
  foe_rewrap: bool,
347
332
  foe_skip: bool,
348
- foe_from: ?Move,
349
333
  options: anytype,
350
334
  ) !?Result {
351
335
  assert(player_choice.type != .Pass);
@@ -358,7 +342,6 @@ fn doTurn(
358
342
  player_choice,
359
343
  player_rewrap,
360
344
  player_skip,
361
- player_from,
362
345
  &residual,
363
346
  options,
364
347
  )) |r| return r;
@@ -381,7 +364,6 @@ fn doTurn(
381
364
  foe_choice,
382
365
  foe_rewrap,
383
366
  foe_skip,
384
- foe_from,
385
367
  &residual,
386
368
  options,
387
369
  )) |r| return r;
@@ -409,7 +391,6 @@ fn executeMove(
409
391
  choice: Choice,
410
392
  rewrap: bool,
411
393
  skip: bool,
412
- from: ?Move,
413
394
  residual: *bool,
414
395
  options: anytype,
415
396
  ) !?Result {
@@ -426,7 +407,7 @@ fn executeMove(
426
407
  if (side.last_selected_move == .SKIP_TURN) {
427
408
  assert(!showdown);
428
409
  if (battle.foe(player).active.volatiles.Binding) {
429
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
430
411
  }
431
412
  return null;
432
413
  }
@@ -444,8 +425,10 @@ fn executeMove(
444
425
  mslot = @intCast(battle.lastMove(player).index);
445
426
  const stored = side.stored();
446
427
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
447
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
448
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
428
+ auto = isForced(&side.active) or
429
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
430
+ side.last_selected_move == .HyperBeam or
431
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
449
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
450
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
451
434
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -467,7 +450,7 @@ fn executeMove(
467
450
 
468
451
  var skip_can = false;
469
452
  var skip_pp = false;
470
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
471
454
  .done => return null,
472
455
  .skip_can => skip_can = true,
473
456
  .skip_pp => skip_pp = true,
@@ -475,9 +458,9 @@ fn executeMove(
475
458
  .err => return @as(?Result, Result.Error),
476
459
  }
477
460
 
478
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
479
- return null;
480
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
481
464
 
482
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
483
466
  }
@@ -488,7 +471,6 @@ fn beforeMove(
488
471
  battle: anytype,
489
472
  player: Player,
490
473
  skip: bool,
491
- from: ?Move,
492
474
  residual: *bool,
493
475
  options: anytype,
494
476
  ) !BeforeMove {
@@ -515,23 +497,23 @@ fn beforeMove(
515
497
  }
516
498
 
517
499
  if (duration == 0) {
518
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
519
501
  stored.status = 0; // clears EXT if present
520
502
  } else {
521
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
522
504
  }
523
505
  side.last_used_move = .None;
524
506
  return .done;
525
507
  }
526
508
 
527
509
  if (Status.is(stored.status, .FRZ)) {
528
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
529
511
  side.last_used_move = .None;
530
512
  return .done;
531
513
  }
532
514
 
533
515
  if (skip or foe.active.volatiles.Binding) {
534
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
535
517
  return .done;
536
518
  }
537
519
 
@@ -539,27 +521,27 @@ fn beforeMove(
539
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
540
522
  // Pokémon can sometimes flinch multiple times from the same original hit
541
523
  if (!showdown) volatiles.Flinch = false;
542
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
543
525
  return .done;
544
526
  }
545
527
 
546
528
  if (volatiles.Recharging) {
547
529
  volatiles.Recharging = false;
548
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
549
531
  return .done;
550
532
  }
551
533
 
552
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
553
535
  if (options.calc.modify(player, .disable)) |extend| {
554
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
555
537
  } else {
556
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
557
539
  }
558
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
559
541
 
560
- if (volatiles.disabled_duration == 0) {
561
- volatiles.disabled_move = 0;
562
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
563
545
  }
564
546
  }
565
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -584,9 +566,9 @@ fn beforeMove(
584
566
 
585
567
  if (volatiles.confusion == 0) {
586
568
  volatiles.Confusion = false;
587
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
588
570
  } else {
589
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
590
572
 
591
573
  if (try Rolls.confused(battle, player, options)) {
592
574
  assert(!volatiles.MultiHit);
@@ -637,7 +619,7 @@ fn beforeMove(
637
619
  volatiles.Binding = false;
638
620
  options.chance.clearDurations(player, null);
639
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
640
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
641
623
  return .done;
642
624
  }
643
625
 
@@ -653,8 +635,6 @@ fn beforeMove(
653
635
  volatiles.state +%= battle.last_damage;
654
636
  }
655
637
 
656
- assert(volatiles.attacks > 0);
657
-
658
638
  if (options.calc.modify(player, .attacking)) |extend| {
659
639
  if (!extend) volatiles.attacks = 0;
660
640
  } else {
@@ -663,18 +643,18 @@ fn beforeMove(
663
643
  try options.chance.attacking(player, volatiles.attacks);
664
644
 
665
645
  if (volatiles.attacks != 0) {
666
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
667
647
  return .done;
668
648
  }
669
649
 
670
650
  volatiles.Bide = false;
671
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
672
652
 
673
653
  battle.last_damage = volatiles.state *% 2;
674
654
  volatiles.state = 0;
675
655
 
676
656
  if (battle.last_damage == 0) {
677
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
678
658
  return .done;
679
659
  }
680
660
 
@@ -694,9 +674,6 @@ fn beforeMove(
694
674
  }
695
675
 
696
676
  if (volatiles.Thrashing) {
697
- assert(volatiles.attacks > 0);
698
- assert(from != null);
699
-
700
677
  if (options.calc.modify(player, .attacking)) |extend| {
701
678
  if (!extend) volatiles.attacks = 0;
702
679
  } else {
@@ -705,23 +682,21 @@ fn beforeMove(
705
682
  try options.chance.attacking(player, volatiles.attacks);
706
683
 
707
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
708
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
709
686
  }
710
- 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()) });
711
688
  if (showdown) {
712
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
713
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
714
691
  side.last_used_move = side.last_selected_move;
715
692
  if (handleThrashing(battle, active, player, options)) {
716
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
717
694
  }
718
695
  }
719
696
  return .skip_can;
720
697
  }
721
698
 
722
699
  if (volatiles.Binding) {
723
- assert(volatiles.attacks > 0);
724
-
725
700
  if (options.calc.modify(player, .binding)) |extend| {
726
701
  if (!extend) volatiles.attacks = 0;
727
702
  } else {
@@ -729,12 +704,7 @@ fn beforeMove(
729
704
  }
730
705
  try options.chance.binding(player, volatiles.attacks);
731
706
 
732
- try log.move(
733
- battle.active(player),
734
- side.last_selected_move,
735
- battle.active(player.foe()),
736
- side.last_selected_move,
737
- );
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
738
708
  if (showdown or battle.last_damage != 0) {
739
709
  const sub = showdown and foe.active.volatiles.Substitute;
740
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -754,7 +724,7 @@ fn canMove(
754
724
  mslot: u4,
755
725
  auto: bool,
756
726
  skip_pp: bool,
757
- from: ?Move,
727
+ from: Move,
758
728
  residual: *bool,
759
729
  options: anytype,
760
730
  ) !bool {
@@ -762,33 +732,21 @@ fn canMove(
762
732
  const player_ident = battle.active(player);
763
733
  const move = Move.get(side.last_selected_move);
764
734
 
765
- const foe_last = battle.foe(player).last_used_move;
766
- const special = from != null and (from.? == .Metronome and
767
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
768
-
769
- var skip = skip_pp;
770
735
  if (side.active.volatiles.Charging) {
771
736
  side.active.volatiles.Charging = false;
772
737
  side.active.volatiles.Invulnerable = false;
773
738
  } else if (move.effect == .Charge) {
774
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
775
740
  setCounterable(battle, player, side, move);
776
741
  try Effects.charge(battle, player, options);
777
742
  return false;
778
743
  }
779
744
 
780
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
781
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
782
747
 
783
748
  const target = if (move.target == .Self) player else player.foe();
784
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
785
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
786
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
787
- // meaning sometimes we lose this information. from is still set because it not being null is
788
- // relevant for control flow, but we don't have anything useful to put in the log. This is
789
- // regrettable, but this information is more "nice to have" than required
790
- const f = if (from != null and from.? == .None) null else from;
791
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
792
750
  setCounterable(battle, player, side, move);
793
751
 
794
752
  if (move.effect.onBegin()) {
@@ -826,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
826
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
827
785
  if (volatiles.Bide) return;
828
786
 
829
- assert(active.move(mslot).pp > 0 or auto);
830
- 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;
831
790
  if (volatiles.Transform) return;
832
791
 
833
- assert(side.stored().move(mslot).pp > 0 or auto);
834
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
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;
835
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
836
796
  }
837
797
 
@@ -935,7 +895,7 @@ fn doMove(
935
895
  }
936
896
  }
937
897
 
938
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
939
899
  miss = if (showdown or skip)
940
900
  miss
941
901
  else
@@ -963,16 +923,16 @@ fn doMove(
963
923
  ohko = (!showdown or (!immune and !invulnerable)) and
964
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
965
925
  if (ohko) {
966
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
967
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
968
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
969
929
  } else if (mist) {
970
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
971
- 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 });
972
932
  } else {
973
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
974
- try log.lastmiss();
975
- try log.miss(battle.active(player));
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
976
936
  }
977
937
  if (move.effect == .JumpKick) {
978
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1008,15 +968,15 @@ fn doMove(
1008
968
  var hit: u4 = 0;
1009
969
  while (hit < hits) {
1010
970
  if (hit == 0) {
1011
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
1012
972
  if (effectiveness > Effectiveness.neutral) {
1013
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
1014
974
  } else if (effectiveness < Effectiveness.neutral) {
1015
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
1016
976
  }
1017
977
  }
1018
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1019
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
1020
980
  hit += 1;
1021
981
  if (foe.stored().hp == 0) break;
1022
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1033,7 +993,7 @@ fn doMove(
1033
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1034
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1035
995
  }
1036
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1037
997
  } else if (showdown) {
1038
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1039
999
  if (move.effect == .Binding) {
@@ -1043,9 +1003,9 @@ fn doMove(
1043
1003
  assert(foe.stored().hp > 0);
1044
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1045
1005
  if (sub) {
1046
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1047
1007
  } else {
1048
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1049
1009
  try buildRage(battle, player.foe(), options);
1050
1010
  }
1051
1011
  return null;
@@ -1093,7 +1053,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1093
1053
  const side = battle.side(player);
1094
1054
 
1095
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1096
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1097
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1098
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1099
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
@@ -1150,9 +1110,9 @@ fn calcDamage(
1150
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1151
1111
  }
1152
1112
 
1153
- 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);
1154
1114
 
1155
- 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;
1156
1116
 
1157
1117
  if (def == 0) return false;
1158
1118
 
@@ -1243,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1243
1203
  const foe = battle.foe(player);
1244
1204
 
1245
1205
  if (battle.last_damage == 0) {
1246
- try options.log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1247
1207
  return null;
1248
1208
  }
1249
1209
 
@@ -1261,14 +1221,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1261
1221
  foe_last_selected_move.type == .Fighting);
1262
1222
 
1263
1223
  if (!used and !selected) {
1264
- try options.log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1265
1225
  return null;
1266
1226
  }
1267
1227
 
1268
1228
  if (!used or !selected) {
1269
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1270
1230
  if (!showdown) return Result.Error;
1271
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1272
1232
  return null;
1273
1233
  }
1274
1234
 
@@ -1304,20 +1264,20 @@ fn applyDamage(
1304
1264
  subbed.active.volatiles.substitute = 0;
1305
1265
  subbed.active.volatiles.Substitute = false;
1306
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1307
- try options.log.end(battle.active(sub_player), .Substitute);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1308
1268
  options.calc.capped(target_player);
1309
1269
  return true;
1310
1270
  } else {
1311
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1312
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1313
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1314
1274
  return false;
1315
1275
  }
1316
1276
  }
1317
1277
 
1318
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1319
1279
  target.stored().hp -= battle.last_damage;
1320
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1321
1281
  return false;
1322
1282
  }
1323
1283
 
@@ -1336,7 +1296,7 @@ fn mirrorMove(
1336
1296
  side.last_selected_move = foe.last_used_move;
1337
1297
 
1338
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1339
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1340
1300
  return null;
1341
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1342
1302
  incrementPP(side, mslot);
@@ -1378,12 +1338,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1378
1338
  if (mist) {
1379
1339
  assert(!showdown);
1380
1340
  const foe_ident = battle.active(player.foe());
1381
- try options.log.activate(foe_ident, .Mist);
1382
- try options.log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1383
1343
  } else {
1384
1344
  try options.chance.commit(player, .miss);
1385
- try options.log.lastmiss();
1386
- try options.log.miss(battle.active(player));
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1387
1347
  }
1388
1348
 
1389
1349
  return false;
@@ -1435,7 +1395,7 @@ fn moveHit(
1435
1395
  const overwritten = overwrite and state > 0;
1436
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1437
1397
 
1438
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1439
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1440
1400
  accuracy = accuracy * boost[0] / boost[1];
1441
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1474,9 +1434,8 @@ fn checkFaint(
1474
1434
  const foe_fainted = foe.stored().hp == 0;
1475
1435
 
1476
1436
  const player_out = findFirstAlive(side) == 0;
1477
- const foe_out = findFirstAlive(foe) == 0;
1478
- const tie = player_out and foe_out;
1479
- const more = tie or player_out or foe_out;
1437
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
+ const more = player_out or foe_out;
1480
1439
 
1481
1440
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1482
1441
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1484,20 +1443,20 @@ fn checkFaint(
1484
1443
  assert(!side.active.volatiles.MultiHit);
1485
1444
  assert(!foe.active.volatiles.MultiHit);
1486
1445
 
1487
- if (tie) {
1488
- try options.log.tie();
1446
+ if (player_out and foe_out) {
1447
+ try options.log.tie(.{});
1489
1448
  return Result.Tie;
1490
1449
  } else if (player_out) {
1491
- try options.log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1492
1451
  return if (player == .P1) Result.Lose else Result.Win;
1493
1452
  } else if (foe_out) {
1494
- try options.log.win(player);
1453
+ try options.log.win(.{player});
1495
1454
  return if (player == .P1) Result.Win else Result.Lose;
1496
1455
  }
1497
1456
 
1498
1457
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1499
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1500
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1458
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1459
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1501
1460
  }
1502
1461
 
1503
1462
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1533,7 +1492,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1533
1492
  side.stored().stats.spe;
1534
1493
  }
1535
1494
 
1536
- try options.log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1537
1496
  options.calc.capped(player);
1538
1497
  return null;
1539
1498
  }
@@ -1555,8 +1514,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1555
1514
 
1556
1515
  if (!showdown or damage > 0) {
1557
1516
  stored.hp -= damage;
1558
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1559
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
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 });
1560
1519
  }
1561
1520
  }
1562
1521
 
@@ -1581,14 +1540,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1581
1540
  const amount = @min(damage, stored.hp);
1582
1541
  stored.hp -= amount;
1583
1542
 
1584
- // As above, Pokémon Showdown uses damageOf but its not relevant
1585
- 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 });
1586
1545
 
1587
1546
  const before = foe_stored.hp;
1588
1547
  // Uncapped damage is added back to the foe
1589
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1590
1549
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1591
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1550
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1592
1551
  }
1593
1552
  }
1594
1553
 
@@ -1599,18 +1558,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1599
1558
  battle.turn += 1;
1600
1559
 
1601
1560
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1602
- try options.log.tie();
1561
+ try options.log.tie(.{});
1603
1562
  return Result.Tie;
1604
1563
  }
1605
1564
 
1606
1565
  if (showdown and battle.turn >= 1000) {
1607
- try options.log.tie();
1566
+ try options.log.tie(.{});
1608
1567
  return Result.Tie;
1609
1568
  } else if (battle.turn >= 65535) {
1610
1569
  return Result.Error;
1611
1570
  }
1612
1571
 
1613
- try options.log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1614
1573
 
1615
1574
  return Result.Default;
1616
1575
  }
@@ -1670,9 +1629,9 @@ fn checkEBC(battle: anytype) bool {
1670
1629
  }
1671
1630
 
1672
1631
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1673
- if (side.active.volatiles.disabled_move != 0) {
1632
+ if (side.active.volatiles.disable_move != 0) {
1674
1633
  // A Pokémon that transforms after being disabled may end up with less move slots
1675
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1634
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1676
1635
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1677
1636
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1678
1637
  m
@@ -1680,14 +1639,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1680
1639
  side.last_selected_move;
1681
1640
  if (m != .None and m == last) {
1682
1641
  side.active.volatiles.Charging = false;
1683
- try options.log.disabled(ident, last);
1642
+ try options.log.cant(.{ ident, .Disable, last });
1684
1643
  return true;
1685
1644
  }
1686
1645
  }
1687
1646
  return false;
1688
1647
  }
1689
1648
 
1690
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1691
1650
  const side = battle.side(who);
1692
1651
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1693
1652
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1705,7 +1664,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1705
1664
  return true;
1706
1665
  }
1707
1666
 
1708
- inline fn onBegin(
1667
+ fn onBegin(
1709
1668
  battle: anytype,
1710
1669
  player: Player,
1711
1670
  move: Move.Data,
@@ -1713,7 +1672,6 @@ inline fn onBegin(
1713
1672
  residual: *bool,
1714
1673
  options: anytype,
1715
1674
  ) !void {
1716
- assert(move.effect.onBegin());
1717
1675
  return switch (move.effect) {
1718
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1719
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1735,8 +1693,7 @@ inline fn onBegin(
1735
1693
  };
1736
1694
  }
1737
1695
 
1738
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1739
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1740
1697
  return switch (move.effect) {
1741
1698
  .Bide => Effects.bide(battle, player, options),
1742
1699
  // zig fmt: off
@@ -1751,14 +1708,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1751
1708
  };
1752
1709
  }
1753
1710
 
1754
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1755
1712
  battle: anytype,
1756
1713
  player: Player,
1757
1714
  move: Move.Data,
1758
1715
  residual: *bool,
1759
1716
  options: anytype,
1760
1717
  ) !void {
1761
- assert(move.effect.alwaysHappens());
1762
1718
  return switch (move.effect) {
1763
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1764
1720
  .Explode => Effects.explode(battle, player),
@@ -1770,7 +1726,7 @@ inline fn alwaysHappens(
1770
1726
  };
1771
1727
  }
1772
1728
 
1773
- 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 {
1774
1730
  return switch (move.effect) {
1775
1731
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1776
1732
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1797,7 +1753,7 @@ pub const Effects = struct {
1797
1753
  side.active.volatiles.state = 0;
1798
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1799
1755
 
1800
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1801
1757
  }
1802
1758
 
1803
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1811,11 +1767,11 @@ pub const Effects = struct {
1811
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1812
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1813
1769
  assert(move.type == .Fire);
1814
- try options.log.curestatus(
1770
+ try options.log.curestatus(.{
1815
1771
  battle.active(player.foe()),
1816
1772
  foe_stored.status,
1817
1773
  .Message,
1818
- );
1774
+ });
1819
1775
  foe_stored.status = 0;
1820
1776
  }
1821
1777
  return;
@@ -1829,7 +1785,7 @@ pub const Effects = struct {
1829
1785
  foe_stored.status = Status.init(.BRN);
1830
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1831
1787
 
1832
- 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 });
1833
1789
  }
1834
1790
 
1835
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1839,8 +1795,8 @@ pub const Effects = struct {
1839
1795
  volatiles.Charging = true;
1840
1796
  const move = side.last_selected_move;
1841
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1842
- try options.log.laststill();
1843
- try options.log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1844
1800
  }
1845
1801
 
1846
1802
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1851,17 +1807,11 @@ pub const Effects = struct {
1851
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1852
1808
  } else {
1853
1809
  if (showdown) {
1854
- if (!try checkHit(battle, player, move, options)) {
1855
- return;
1856
- } else if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1858
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1859
1812
  } else {
1860
- if (sub) {
1861
- return options.log.fail(battle.active(player.foe()), .None);
1862
- } else if (!try checkHit(battle, player, move, options)) {
1863
- return;
1864
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1865
1815
  }
1866
1816
  try options.chance.commit(player, .hit);
1867
1817
  }
@@ -1870,23 +1820,24 @@ pub const Effects = struct {
1870
1820
  foe.active.volatiles.Confusion = true;
1871
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1872
1822
 
1873
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1874
1824
  }
1875
1825
 
1876
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1877
1827
  const foe = battle.foe(player);
1878
1828
 
1879
1829
  if (foe.active.volatiles.Invulnerable) {
1880
- try options.log.lastmiss();
1881
- return options.log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1882
1832
  }
1883
1833
 
1884
1834
  battle.side(player).active.types = foe.active.types;
1885
- return options.log.typechange(
1835
+ return options.log.start(.{
1886
1836
  battle.active(player),
1837
+ .TypeChange,
1887
1838
  foe.active.types,
1888
1839
  battle.active(player.foe()),
1889
- );
1840
+ });
1890
1841
  }
1891
1842
 
1892
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,8 +1851,8 @@ pub const Effects = struct {
1900
1851
  try options.chance.commit(player, .hit);
1901
1852
  }
1902
1853
 
1903
- if (volatiles.disabled_move != 0) {
1904
- try options.log.fail(foe_ident, .None);
1854
+ if (volatiles.disable_move != 0) {
1855
+ try options.log.fail(.{ foe_ident, .None });
1905
1856
  return null;
1906
1857
  }
1907
1858
 
@@ -1917,7 +1868,7 @@ pub const Effects = struct {
1917
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1918
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1919
1870
  if (n == 0) {
1920
- try options.log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1921
1872
  return null;
1922
1873
  } else if (err) {
1923
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1925,18 +1876,18 @@ pub const Effects = struct {
1925
1876
  return Result.Error;
1926
1877
  }
1927
1878
 
1928
- volatiles.disabled_move =
1879
+ volatiles.disable_move =
1929
1880
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1930
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1881
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1931
1882
 
1932
- const id = foe.active.move(volatiles.disabled_move).id;
1933
- 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 });
1934
1885
  return null;
1935
1886
  }
1936
1887
 
1937
1888
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1938
- var side = battle.side(player);
1939
- var stored = side.stored();
1889
+ var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1940
1891
 
1941
1892
  if (battle.last_damage == 0) {
1942
1893
  assert(showdown);
@@ -1949,7 +1900,7 @@ pub const Effects = struct {
1949
1900
  if (stored.hp == stored.stats.hp) return;
1950
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1951
1902
 
1952
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1953
1904
  }
1954
1905
 
1955
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -1981,7 +1932,7 @@ pub const Effects = struct {
1981
1932
  if (side.active.volatiles.FocusEnergy) return;
1982
1933
  side.active.volatiles.FocusEnergy = true;
1983
1934
 
1984
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1985
1936
  }
1986
1937
 
1987
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2003,7 +1954,7 @@ pub const Effects = struct {
2003
1954
  foe_stored.status = Status.init(.FRZ);
2004
1955
  // GLITCH: Hyper Beam recharging status is not cleared
2005
1956
 
2006
- try options.log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2007
1958
  }
2008
1959
 
2009
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2011,7 +1962,7 @@ pub const Effects = struct {
2011
1962
  var side = battle.side(player);
2012
1963
  var foe = battle.foe(player);
2013
1964
 
2014
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
2015
1966
  var foe_stored = foe.stored();
2016
1967
 
2017
1968
  const player_ident = battle.active(player);
@@ -2023,8 +1974,8 @@ pub const Effects = struct {
2023
1974
  side.active.stats = side_stored.stats;
2024
1975
  foe.active.stats = foe_stored.stats;
2025
1976
 
2026
- try log.activate(player_ident, .Haze);
2027
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
2028
1979
 
2029
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2030
1981
  if (showdown) {
@@ -2032,11 +1983,11 @@ pub const Effects = struct {
2032
1983
  const p: Player = @enumFromInt(i);
2033
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2034
1985
  if (p != player and Status.any(s.stored().status)) {
2035
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2036
1987
  s.stored().status = 0;
2037
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2038
1989
  s.stored().status = Status.init(.PSN);
2039
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2040
1991
  }
2041
1992
  try clearVolatiles(battle, p, p != player, options);
2042
1993
  }
@@ -2045,7 +1996,7 @@ pub const Effects = struct {
2045
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2046
1997
  foe.last_selected_move = .SKIP_TURN;
2047
1998
  }
2048
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2049
2000
  foe_stored.status = 0;
2050
2001
  }
2051
2002
  try clearVolatiles(battle, player, false, options);
@@ -2061,7 +2012,7 @@ pub const Effects = struct {
2061
2012
  // GLITCH: HP recovery move failure glitches
2062
2013
  const delta = stored.stats.hp - stored.hp;
2063
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2064
- return try options.log.fail(ident, .None);
2015
+ return options.log.fail(.{ ident, .None });
2065
2016
  }
2066
2017
 
2067
2018
  const rest = side.last_selected_move == .Rest;
@@ -2069,17 +2020,17 @@ pub const Effects = struct {
2069
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2070
2021
  if (showdown) battle.rng.advance(1);
2071
2022
  stored.status = Status.slf(2);
2072
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2073
2024
  stored.hp = stored.stats.hp;
2074
2025
  } else {
2075
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2076
2027
  }
2077
- 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 });
2078
2029
  }
2079
2030
 
2080
2031
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2081
2032
  battle.side(player).active.volatiles.Recharging = true;
2082
- try options.log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
2083
2034
  }
2084
2035
 
2085
2036
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2088,33 +2039,32 @@ pub const Effects = struct {
2088
2039
  if (showdown) {
2089
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2090
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2091
- return options.log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2092
2043
  }
2093
2044
  if (!try checkHit(battle, player, move, options)) return;
2094
2045
  if (foe.active.volatiles.LeechSeed) return;
2095
2046
  } else {
2096
2047
  if (!try checkHit(battle, player, move, options)) return;
2097
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2098
- try options.log.lastmiss();
2099
- return options.log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
2100
2051
  }
2101
2052
  }
2102
2053
 
2103
2054
  try options.chance.commit(player, .hit);
2104
2055
  foe.active.volatiles.LeechSeed = true;
2105
2056
 
2106
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2107
2058
  }
2108
2059
 
2109
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2110
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2111
2063
 
2112
- if (side.active.volatiles.LightScreen) {
2113
- return options.log.fail(battle.active(player), .None);
2114
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2115
2065
  side.active.volatiles.LightScreen = true;
2116
2066
 
2117
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2118
2068
  }
2119
2069
 
2120
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2143,7 +2093,7 @@ pub const Effects = struct {
2143
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2144
2094
  if (!try checkHit(battle, player, move, options)) return;
2145
2095
  try options.chance.commit(player, .hit);
2146
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2147
2097
  }
2148
2098
  }
2149
2099
  if (!try checkHit(battle, player, move, options)) return;
@@ -2152,7 +2102,7 @@ pub const Effects = struct {
2152
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2153
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2154
2104
 
2155
- 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 });
2156
2106
  }
2157
2107
 
2158
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2161,7 +2111,7 @@ pub const Effects = struct {
2161
2111
  if (side.active.volatiles.Mist) return;
2162
2112
  side.active.volatiles.Mist = true;
2163
2113
 
2164
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2165
2115
  }
2166
2116
 
2167
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2187,17 +2137,18 @@ pub const Effects = struct {
2187
2137
 
2188
2138
  if (showdown) {
2189
2139
  // Invulnerability trumps type immunity on Pokémon Showdown
2190
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2191
- if (!try checkHit(battle, player, move, options)) return;
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;
2192
2143
  }
2193
2144
  if (Status.any(foe_stored.status)) {
2194
- return log.fail(
2145
+ return log.fail(.{
2195
2146
  foe_ident,
2196
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2197
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2198
2149
  }
2199
2150
  if (!showdown) {
2200
- if (immune) return log.immune(foe_ident, .None);
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2201
2152
  if (!try checkHit(battle, player, move, options)) return;
2202
2153
  }
2203
2154
  try options.chance.commit(player, .hit);
@@ -2205,7 +2156,7 @@ pub const Effects = struct {
2205
2156
  foe_stored.status = Status.init(.PAR);
2206
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2207
2158
 
2208
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2209
2160
  }
2210
2161
 
2211
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2227,12 +2178,12 @@ pub const Effects = struct {
2227
2178
  foe_stored.status = Status.init(.PAR);
2228
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2229
2180
 
2230
- 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 });
2231
2182
  }
2232
2183
 
2233
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2234
2185
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2235
- try options.log.fieldactivate();
2186
+ try options.log.fieldactivate(.{});
2236
2187
  }
2237
2188
  }
2238
2189
 
@@ -2247,23 +2198,23 @@ pub const Effects = struct {
2247
2198
  return;
2248
2199
  } else if (foe.active.volatiles.Substitute) {
2249
2200
  if (move.effect != .Poison) return;
2250
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2251
2202
  } else if (Status.any(foe_stored.status)) {
2252
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2253
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2254
2205
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2255
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2206
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2256
2207
  if (!showdown)
2257
- .Poison
2208
+ Fail.Poison
2258
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2259
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2260
2211
  else
2261
- .None
2212
+ Fail.None
2262
2213
  else
2263
- .None);
2214
+ Fail.None });
2264
2215
  } else if (foe.active.types.includes(.Poison)) {
2265
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2266
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2267
2218
  }
2268
2219
 
2269
2220
  if (move.effect == .Poison) {
@@ -2282,7 +2233,7 @@ pub const Effects = struct {
2282
2233
  foe.active.volatiles.toxic = 0;
2283
2234
  }
2284
2235
 
2285
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2286
2237
  }
2287
2238
 
2288
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2302,22 +2253,23 @@ pub const Effects = struct {
2302
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2303
2254
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2304
2255
 
2305
- try options.log.damageOf(
2256
+ try options.log.damage(.{
2306
2257
  battle.active(player),
2307
2258
  stored,
2308
2259
  .RecoilOf,
2309
2260
  battle.active(player.foe()),
2310
- );
2261
+ });
2311
2262
  if (showdown and stored.hp == 0) residual.* = false;
2312
2263
  }
2313
2264
 
2314
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2315
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2316
2268
 
2317
- 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 });
2318
2270
  side.active.volatiles.Reflect = true;
2319
2271
 
2320
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2321
2273
  }
2322
2274
 
2323
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2331,10 +2283,10 @@ pub const Effects = struct {
2331
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2332
2284
  } else {
2333
2285
  if (Status.any(foe_stored.status)) {
2334
- return options.log.fail(
2286
+ return options.log.fail(.{
2335
2287
  foe_ident,
2336
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2337
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2338
2290
  }
2339
2291
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2340
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2351,18 +2303,17 @@ pub const Effects = struct {
2351
2303
 
2352
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2353
2305
  const last = battle.side(player).last_selected_move;
2354
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2355
2307
  }
2356
2308
 
2357
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2358
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2359
2311
  }
2360
2312
 
2361
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2362
2314
  var side = battle.side(player);
2363
2315
  if (side.active.volatiles.Substitute) {
2364
- try options.log.fail(battle.active(player), .Substitute);
2365
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2366
2317
  }
2367
2318
 
2368
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2374,17 +2325,16 @@ pub const Effects = struct {
2374
2325
  // avoid having to convert to floating point)
2375
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2376
2327
  if (side.stored().hp < required_hp) {
2377
- try options.log.fail(battle.active(player), .Weak);
2378
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2379
2329
  }
2380
2330
 
2381
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2382
2332
  side.stored().hp -= hp;
2383
2333
  side.active.volatiles.substitute = hp + 1;
2384
2334
  side.active.volatiles.Substitute = true;
2385
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2386
2336
  if (hp > 0) {
2387
- try options.log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2388
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2389
2339
  }
2390
2340
  }
@@ -2398,8 +2348,8 @@ pub const Effects = struct {
2398
2348
  }
2399
2349
  battle.last_damage = 0;
2400
2350
  } else {
2401
- try options.log.fail(battle.active(player), .None);
2402
- try options.log.laststill();
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2403
2353
  }
2404
2354
  }
2405
2355
 
@@ -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,24 +2671,26 @@ 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
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2742
2694
  else if (showdown)
2743
2695
  battle.rng.range(u8, 0, 2) == 0
2744
2696
  else
@@ -2748,9 +2700,11 @@ pub const Rolls = struct {
2748
2700
  return p1;
2749
2701
  }
2750
2702
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2703
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2752
2704
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2705
  val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2754
2708
  else if (showdown)
2755
2709
  battle.rng.chance(u8, rate, 256)
2756
2710
  else
@@ -2760,9 +2714,11 @@ pub const Rolls = struct {
2760
2714
  return crit;
2761
2715
  }
2762
2716
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2717
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2718
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2719
  val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2766
2722
  else roll: {
2767
2723
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2724
  while (true) {
@@ -2776,9 +2732,11 @@ pub const Rolls = struct {
2776
2732
  return roll;
2777
2733
  }
2778
2734
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2780
2736
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2737
  val == .true
2738
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
+ battle.rng.hit(player, accuracy)
2782
2740
  else if (showdown)
2783
2741
  battle.rng.chance(u8, accuracy, 256)
2784
2742
  else
@@ -2788,9 +2746,11 @@ pub const Rolls = struct {
2788
2746
  return ok;
2789
2747
  }
2790
2748
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2749
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2750
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2751
  val == .true
2752
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2753
+ battle.rng.confused(player)
2794
2754
  else if (showdown)
2795
2755
  !battle.rng.chance(u8, 128, 256)
2796
2756
  else
@@ -2800,9 +2760,11 @@ pub const Rolls = struct {
2800
2760
  return cfz;
2801
2761
  }
2802
2762
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2765
  val == .true
2766
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2767
+ battle.rng.paralyzed(player)
2806
2768
  else if (showdown)
2807
2769
  battle.rng.chance(u8, 63, 256)
2808
2770
  else
@@ -2812,9 +2774,11 @@ pub const Rolls = struct {
2812
2774
  return par;
2813
2775
  }
2814
2776
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2777
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2778
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2779
  val == .true
2780
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2781
+ battle.rng.secondaryChance(player, 25)
2818
2782
  else if (showdown)
2819
2783
  battle.rng.chance(u8, 25, 256)
2820
2784
  else
@@ -2824,11 +2788,13 @@ pub const Rolls = struct {
2824
2788
  return proc;
2825
2789
  }
2826
2790
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2791
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2792
  const rate: u8 = if (low) 26 else 77;
2829
2793
 
2830
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2797
+ battle.rng.secondaryChance(player, rate)
2832
2798
  else if (showdown)
2833
2799
  battle.rng.chance(u8, rate, 256)
2834
2800
  else
@@ -2838,11 +2804,13 @@ pub const Rolls = struct {
2838
2804
  return proc;
2839
2805
  }
2840
2806
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2807
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2808
  const rate: u8 = if (low) 52 else 103;
2843
2809
 
2844
2810
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2813
+ battle.rng.secondaryChance(player, rate)
2846
2814
  else if (showdown)
2847
2815
  battle.rng.chance(u8, rate, 256)
2848
2816
  else
@@ -2852,9 +2820,11 @@ pub const Rolls = struct {
2852
2820
  return proc;
2853
2821
  }
2854
2822
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2823
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2824
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2827
+ battle.rng.secondaryChance(player, 85)
2858
2828
  else if (showdown)
2859
2829
  battle.rng.chance(u8, 85, 256)
2860
2830
  else
@@ -2864,14 +2834,14 @@ pub const Rolls = struct {
2864
2834
  return proc;
2865
2835
  }
2866
2836
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2837
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2838
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2839
  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: {
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: {
2875
2845
  while (true) {
2876
2846
  const r = battle.rng.next();
2877
2847
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2854,11 @@ pub const Rolls = struct {
2884
2854
  return move;
2885
2855
  }
2886
2856
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2857
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2858
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2859
  val - 1
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2861
+ battle.rng.psywave(player)
2890
2862
  else if (showdown)
2891
2863
  battle.rng.range(u8, 0, max)
2892
2864
  else power: {
@@ -2901,11 +2873,13 @@ pub const Rolls = struct {
2901
2873
  return power;
2902
2874
  }
2903
2875
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2876
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
2877
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
2878
  @intCast(val)
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
+ battle.rng.sleepDuration(player)
2907
2881
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2882
+ battle.rng.range(u3, 1, 8)
2909
2883
  else duration: {
2910
2884
  while (true) {
2911
2885
  const r = battle.rng.next() & 7;
@@ -2918,11 +2892,13 @@ pub const Rolls = struct {
2918
2892
  return duration;
2919
2893
  }
2920
2894
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
2896
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
2897
  val
2898
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
+ battle.rng.disableDuration(player)
2924
2900
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2901
+ battle.rng.range(u4, 1, 9)
2926
2902
  else
2927
2903
  @intCast((battle.rng.next() & 7) + 1);
2928
2904
 
@@ -2931,11 +2907,13 @@ pub const Rolls = struct {
2931
2907
  return duration;
2932
2908
  }
2933
2909
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2910
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2935
2911
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
2912
  @intCast(val)
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
+ battle.rng.confusionDuration(player, self)
2937
2915
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2916
+ battle.rng.range(u3, 2, 6)
2939
2917
  else
2940
2918
  @intCast((battle.rng.next() & 3) + 2);
2941
2919
 
@@ -2944,11 +2922,13 @@ pub const Rolls = struct {
2944
2922
  return duration;
2945
2923
  }
2946
2924
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2925
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
2926
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
2927
  @intCast(val)
2928
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
+ battle.rng.attackingDuration(player)
2950
2930
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
2931
+ battle.rng.range(u3, 2, 4)
2952
2932
  else
2953
2933
  @intCast((battle.rng.next() & 1) + 2);
2954
2934
 
@@ -2957,7 +2937,9 @@ pub const Rolls = struct {
2957
2937
  return duration;
2958
2938
  }
2959
2939
 
2960
- inline fn distribution(
2940
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2941
+
2942
+ fn distribution(
2961
2943
  battle: anytype,
2962
2944
  comptime effect: Move.Effect,
2963
2945
  player: Player,
@@ -2966,8 +2948,10 @@ pub const Rolls = struct {
2966
2948
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
2949
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
2950
  @intCast(val)
2951
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
+ battle.rng.distribution(player)
2969
2953
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2954
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
2955
  else n: {
2972
2956
  const r = (battle.rng.next() & 3);
2973
2957
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2998,7 +2982,9 @@ pub const Rolls = struct {
2998
2982
  else
2999
2983
  null;
3000
2984
 
3001
- 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: {
3002
2988
  if (showdown) {
3003
2989
  if (check_pp == 0) {
3004
2990
  var i: usize = moves.len;
@@ -3014,10 +3000,9 @@ pub const Rolls = struct {
3014
3000
  while (i < moves.len and r > 0) : (i += 1) {
3015
3001
  if (moves[i].pp > 0) {
3016
3002
  r -= 1;
3017
- if (r == 0) break;
3003
+ if (r == 0) break :slot @intCast(i + 1);
3018
3004
  }
3019
3005
  }
3020
- break :slot @intCast(i + 1);
3021
3006
  }
3022
3007
  }
3023
3008
 
@@ -3039,11 +3024,11 @@ test "RNG agreement" {
3039
3024
  expected[i] = @intCast(i * 0x1000000);
3040
3025
  }
3041
3026
 
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 };
3027
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3028
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3029
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3030
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3031
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3032
 
3048
3033
  for (0..expected.len) |i| {
3049
3034
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,7 +3064,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3064
  const side = battle.side(player);
3080
3065
  const foe = battle.foe(player);
3081
3066
 
3082
- var active = &side.active;
3067
+ const active = &side.active;
3083
3068
  const stored = side.stored();
3084
3069
 
3085
3070
  // While players are not given any input options on the cartridge in these cases,
@@ -3133,7 +3118,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3118
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3119
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3120
  const struggle =
3136
- 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);
3137
3122
  const s = if (struggle) 0 else slot;
3138
3123
  out[n] = .{ .type = .Move, .data = s };
3139
3124
  n += 1;
@@ -3148,7 +3133,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3133
  const m = active.moves[slot - 1];
3149
3134
  if (m.id == .None) break;
3150
3135
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3136
+ if (active.volatiles.disable_move == slot) continue;
3152
3137
  out[n] = .{ .type = .Move, .data = slot };
3153
3138
  n += 1;
3154
3139
  }