@pkmn/engine 0.1.0-dev.90d6c14c → 0.1.0-dev.9257876b

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 (48) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +52 -21
  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 +125 -102
  19. package/package.json +43 -23
  20. package/src/bin/install-pkmn-engine +27 -19
  21. package/src/lib/common/data.zig +12 -24
  22. package/src/lib/common/optional.zig +9 -2
  23. package/src/lib/common/protocol.zig +805 -327
  24. package/src/lib/common/rational.zig +12 -8
  25. package/src/lib/common/rng.zig +26 -17
  26. package/src/lib/gen1/calc.zig +9 -10
  27. package/src/lib/gen1/chance.zig +83 -66
  28. package/src/lib/gen1/data/moves.zig +192 -180
  29. package/src/lib/gen1/data/species.zig +3 -3
  30. package/src/lib/gen1/data/types.zig +7 -7
  31. package/src/lib/gen1/data.zig +60 -67
  32. package/src/lib/gen1/helpers.zig +261 -123
  33. package/src/lib/gen1/mechanics.zig +370 -403
  34. package/src/lib/gen2/calc.zig +143 -0
  35. package/src/lib/gen2/chance.zig +1315 -0
  36. package/src/lib/gen2/data/items.zig +12 -18
  37. package/src/lib/gen2/data/moves.zig +663 -569
  38. package/src/lib/gen2/data/species.zig +1 -1
  39. package/src/lib/gen2/data/types.zig +134 -31
  40. package/src/lib/gen2/data.zig +106 -79
  41. package/src/lib/gen2/generated.zig +806 -0
  42. package/src/lib/gen2/helpers.zig +8 -12
  43. package/src/lib/gen2/mechanics.zig +3587 -9
  44. package/src/lib/{binding/node.zig → node.zig} +10 -13
  45. package/src/lib/pkmn.zig +26 -3
  46. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  47. package/src/lib/gen1/README.md +0 -812
  48. package/src/lib/gen2/README.md +0 -308
@@ -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
@@ -186,10 +172,7 @@ fn selectMove(
186
172
  assert(choice.data == 0);
187
173
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
188
174
  if (foe_choice.type == .Switch) {
189
- const last = side.active.move(if (player == .P1)
190
- battle.last_moves.p1_index
191
- else
192
- battle.last_moves.p2_index);
175
+ const last = side.active.move(battle.lastMove(player).index);
193
176
  if (last.id == .Metronome) side.last_selected_move = last.id;
194
177
  if (last.id == .MirrorMove) return Result.Error;
195
178
  }
@@ -213,16 +196,13 @@ fn selectMove(
213
196
  if (choice.data == 0) {
214
197
  const struggle = ok: {
215
198
  for (side.active.moves, 0..) |move, i| {
216
- 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;
217
200
  }
218
201
  break :ok true;
219
202
  };
220
-
221
203
  assert(struggle);
222
- saveMove(battle, player, choice);
223
- } else {
224
- saveMove(battle, player, choice);
225
204
  }
205
+ saveMove(battle, player, choice);
226
206
 
227
207
  return null;
228
208
  }
@@ -235,18 +215,14 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
235
215
  if (c.data == 0) {
236
216
  side.last_selected_move = .Struggle;
237
217
  } else {
238
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
239
219
  const move = side.active.move(c.data);
240
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
241
221
  // sometimes required...), but a 0 PP move can be used automatically
242
222
  assert(showdown or move.pp != 0);
243
223
 
244
224
  side.last_selected_move = move.id;
245
- if (player == .P1) {
246
- battle.last_moves.p1_index = @intCast(c.data);
247
- } else {
248
- battle.last_moves.p2_index = @intCast(c.data);
249
- }
225
+ battle.lastMove(player).index = @intCast(c.data);
250
226
  }
251
227
  }
252
228
  }
@@ -264,11 +240,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
264
240
  side.order[0] = side.order[slot - 1];
265
241
  side.order[slot - 1] = out;
266
242
 
267
- if (player == .P1) {
268
- battle.last_moves.p1_index = 1;
269
- } else {
270
- battle.last_moves.p2_index = 1;
271
- }
243
+ battle.lastMove(player).index = 1;
272
244
 
273
245
  side.last_used_move = .None;
274
246
  foe.last_used_move = .None;
@@ -284,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
284
256
 
285
257
  foe.active.volatiles.Binding = false;
286
258
 
287
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
288
260
  options.chance.switched(player, side.order[0], out);
289
261
 
290
262
  if (showdown and incoming.status == Status.TOX) {
291
263
  incoming.status = Status.init(.PSN);
292
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
293
265
  // rather not clutter up turnOrder just for this (incorrect) log message
294
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
295
267
  }
296
268
  }
297
269
 
@@ -354,12 +326,10 @@ fn doTurn(
354
326
  player_choice: Choice,
355
327
  player_rewrap: bool,
356
328
  player_skip: bool,
357
- player_from: ?Move,
358
329
  foe_player: Player,
359
330
  foe_choice: Choice,
360
331
  foe_rewrap: bool,
361
332
  foe_skip: bool,
362
- foe_from: ?Move,
363
333
  options: anytype,
364
334
  ) !?Result {
365
335
  assert(player_choice.type != .Pass);
@@ -372,7 +342,6 @@ fn doTurn(
372
342
  player_choice,
373
343
  player_rewrap,
374
344
  player_skip,
375
- player_from,
376
345
  &residual,
377
346
  options,
378
347
  )) |r| return r;
@@ -395,7 +364,6 @@ fn doTurn(
395
364
  foe_choice,
396
365
  foe_rewrap,
397
366
  foe_skip,
398
- foe_from,
399
367
  &residual,
400
368
  options,
401
369
  )) |r| return r;
@@ -423,7 +391,6 @@ fn executeMove(
423
391
  choice: Choice,
424
392
  rewrap: bool,
425
393
  skip: bool,
426
- from: ?Move,
427
394
  residual: *bool,
428
395
  options: anytype,
429
396
  ) !?Result {
@@ -440,7 +407,7 @@ fn executeMove(
440
407
  if (side.last_selected_move == .SKIP_TURN) {
441
408
  assert(!showdown);
442
409
  if (battle.foe(player).active.volatiles.Binding) {
443
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
444
411
  }
445
412
  return null;
446
413
  }
@@ -455,14 +422,13 @@ fn executeMove(
455
422
  if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
456
423
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
457
424
  assert(!showdown);
458
- mslot = @intCast(if (player == .P1)
459
- battle.last_moves.p1_index
460
- else
461
- battle.last_moves.p2_index);
425
+ mslot = @intCast(battle.lastMove(player).index);
462
426
  const stored = side.stored();
463
427
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
464
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
465
- 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);
466
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
467
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
468
434
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -479,15 +445,12 @@ fn executeMove(
479
445
  } else if (showdown and side.active.volatiles.Charging) {
480
446
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
481
447
  assert(mslot == 1);
482
- mslot = @intCast(if (player == .P1)
483
- battle.last_moves.p1_index
484
- else
485
- battle.last_moves.p2_index);
448
+ mslot = @intCast(battle.lastMove(player).index);
486
449
  }
487
450
 
488
451
  var skip_can = false;
489
452
  var skip_pp = false;
490
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
491
454
  .done => return null,
492
455
  .skip_can => skip_can = true,
493
456
  .skip_pp => skip_pp = true,
@@ -495,9 +458,9 @@ fn executeMove(
495
458
  .err => return @as(?Result, Result.Error),
496
459
  }
497
460
 
498
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
499
- return null;
500
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
501
464
 
502
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
503
466
  }
@@ -508,7 +471,6 @@ fn beforeMove(
508
471
  battle: anytype,
509
472
  player: Player,
510
473
  skip: bool,
511
- from: ?Move,
512
474
  residual: *bool,
513
475
  options: anytype,
514
476
  ) !BeforeMove {
@@ -535,23 +497,23 @@ fn beforeMove(
535
497
  }
536
498
 
537
499
  if (duration == 0) {
538
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
539
501
  stored.status = 0; // clears EXT if present
540
502
  } else {
541
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
542
504
  }
543
505
  side.last_used_move = .None;
544
506
  return .done;
545
507
  }
546
508
 
547
509
  if (Status.is(stored.status, .FRZ)) {
548
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
549
511
  side.last_used_move = .None;
550
512
  return .done;
551
513
  }
552
514
 
553
515
  if (skip or foe.active.volatiles.Binding) {
554
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
555
517
  return .done;
556
518
  }
557
519
 
@@ -559,27 +521,27 @@ fn beforeMove(
559
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
560
522
  // Pokémon can sometimes flinch multiple times from the same original hit
561
523
  if (!showdown) volatiles.Flinch = false;
562
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
563
525
  return .done;
564
526
  }
565
527
 
566
528
  if (volatiles.Recharging) {
567
529
  volatiles.Recharging = false;
568
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
569
531
  return .done;
570
532
  }
571
533
 
572
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
573
535
  if (options.calc.modify(player, .disable)) |extend| {
574
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
575
537
  } else {
576
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
577
539
  }
578
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
579
541
 
580
- if (volatiles.disabled_duration == 0) {
581
- volatiles.disabled_move = 0;
582
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
583
545
  }
584
546
  }
585
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -604,9 +566,9 @@ fn beforeMove(
604
566
 
605
567
  if (volatiles.confusion == 0) {
606
568
  volatiles.Confusion = false;
607
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
608
570
  } else {
609
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
610
572
 
611
573
  if (try Rolls.confused(battle, player, options)) {
612
574
  assert(!volatiles.MultiHit);
@@ -657,7 +619,7 @@ fn beforeMove(
657
619
  volatiles.Binding = false;
658
620
  options.chance.clearDurations(player, null);
659
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
660
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
661
623
  return .done;
662
624
  }
663
625
 
@@ -673,8 +635,6 @@ fn beforeMove(
673
635
  volatiles.state +%= battle.last_damage;
674
636
  }
675
637
 
676
- assert(volatiles.attacks > 0);
677
-
678
638
  if (options.calc.modify(player, .attacking)) |extend| {
679
639
  if (!extend) volatiles.attacks = 0;
680
640
  } else {
@@ -683,18 +643,18 @@ fn beforeMove(
683
643
  try options.chance.attacking(player, volatiles.attacks);
684
644
 
685
645
  if (volatiles.attacks != 0) {
686
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
687
647
  return .done;
688
648
  }
689
649
 
690
650
  volatiles.Bide = false;
691
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
692
652
 
693
653
  battle.last_damage = volatiles.state *% 2;
694
654
  volatiles.state = 0;
695
655
 
696
656
  if (battle.last_damage == 0) {
697
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
698
658
  return .done;
699
659
  }
700
660
 
@@ -714,9 +674,6 @@ fn beforeMove(
714
674
  }
715
675
 
716
676
  if (volatiles.Thrashing) {
717
- assert(volatiles.attacks > 0);
718
- assert(from != null);
719
-
720
677
  if (options.calc.modify(player, .attacking)) |extend| {
721
678
  if (!extend) volatiles.attacks = 0;
722
679
  } else {
@@ -725,23 +682,21 @@ fn beforeMove(
725
682
  try options.chance.attacking(player, volatiles.attacks);
726
683
 
727
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
728
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
729
686
  }
730
- 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()) });
731
688
  if (showdown) {
732
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
733
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
734
691
  side.last_used_move = side.last_selected_move;
735
692
  if (handleThrashing(battle, active, player, options)) {
736
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
737
694
  }
738
695
  }
739
696
  return .skip_can;
740
697
  }
741
698
 
742
699
  if (volatiles.Binding) {
743
- assert(volatiles.attacks > 0);
744
-
745
700
  if (options.calc.modify(player, .binding)) |extend| {
746
701
  if (!extend) volatiles.attacks = 0;
747
702
  } else {
@@ -749,12 +704,7 @@ fn beforeMove(
749
704
  }
750
705
  try options.chance.binding(player, volatiles.attacks);
751
706
 
752
- try log.move(
753
- battle.active(player),
754
- side.last_selected_move,
755
- battle.active(player.foe()),
756
- side.last_selected_move,
757
- );
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
758
708
  if (showdown or battle.last_damage != 0) {
759
709
  const sub = showdown and foe.active.volatiles.Substitute;
760
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -774,7 +724,7 @@ fn canMove(
774
724
  mslot: u4,
775
725
  auto: bool,
776
726
  skip_pp: bool,
777
- from: ?Move,
727
+ from: Move,
778
728
  residual: *bool,
779
729
  options: anytype,
780
730
  ) !bool {
@@ -782,33 +732,21 @@ fn canMove(
782
732
  const player_ident = battle.active(player);
783
733
  const move = Move.get(side.last_selected_move);
784
734
 
785
- const foe_last = battle.foe(player).last_used_move;
786
- const special = from != null and (from.? == .Metronome and
787
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
788
-
789
- var skip = skip_pp;
790
735
  if (side.active.volatiles.Charging) {
791
736
  side.active.volatiles.Charging = false;
792
737
  side.active.volatiles.Invulnerable = false;
793
738
  } else if (move.effect == .Charge) {
794
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
795
740
  setCounterable(battle, player, side, move);
796
741
  try Effects.charge(battle, player, options);
797
742
  return false;
798
743
  }
799
744
 
800
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
801
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
802
747
 
803
748
  const target = if (move.target == .Self) player else player.foe();
804
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
805
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
806
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
807
- // meaning sometimes we lose this information. from is still set because it not being null is
808
- // relevant for control flow, but we don't have anything useful to put in the log. This is
809
- // regrettable, but this information is more "nice to have" than required
810
- const f = if (from != null and from.? == .None) null else from;
811
- 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 });
812
750
  setCounterable(battle, player, side, move);
813
751
 
814
752
  if (move.effect.onBegin()) {
@@ -834,11 +772,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
834
772
  // as the desync results from actually having two separate battle states that subtly disagree
835
773
  const counterable = side.last_selected_move != .Counter and move.bp > 0 and
836
774
  (move.type == .Normal or move.type == .Fighting);
837
- if (player == .P1) {
838
- battle.last_moves.p1_counterable = @intFromBool(counterable);
839
- } else {
840
- battle.last_moves.p2_counterable = @intFromBool(counterable);
841
- }
775
+ battle.lastMove(player).counterable = @intFromBool(counterable);
842
776
  }
843
777
 
844
778
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
@@ -850,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
850
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
851
785
  if (volatiles.Bide) return;
852
786
 
853
- assert(active.move(mslot).pp > 0 or auto);
854
- 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;
855
790
  if (volatiles.Transform) return;
856
791
 
857
- assert(side.stored().move(mslot).pp > 0 or auto);
858
- 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;
859
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
860
796
  }
861
797
 
@@ -959,7 +895,7 @@ fn doMove(
959
895
  }
960
896
  }
961
897
 
962
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
963
899
  miss = if (showdown or skip)
964
900
  miss
965
901
  else
@@ -987,16 +923,16 @@ fn doMove(
987
923
  ohko = (!showdown or (!immune and !invulnerable)) and
988
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
989
925
  if (ohko) {
990
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
991
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
992
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
993
929
  } else if (mist) {
994
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
995
- 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 });
996
932
  } else {
997
933
  if (showdown or !zero) try options.chance.commit(player, .miss);
998
- try log.lastmiss();
999
- try log.miss(battle.active(player));
934
+ try log.lastmiss(.{});
935
+ try log.miss(.{battle.active(player)});
1000
936
  }
1001
937
  if (move.effect == .JumpKick) {
1002
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1032,15 +968,15 @@ fn doMove(
1032
968
  var hit: u4 = 0;
1033
969
  while (hit < hits) {
1034
970
  if (hit == 0) {
1035
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
1036
972
  if (effectiveness > Effectiveness.neutral) {
1037
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
1038
974
  } else if (effectiveness < Effectiveness.neutral) {
1039
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
1040
976
  }
1041
977
  }
1042
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1043
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
1044
980
  hit += 1;
1045
981
  if (foe.stored().hp == 0) break;
1046
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1057,7 +993,7 @@ fn doMove(
1057
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1058
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1059
995
  }
1060
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1061
997
  } else if (showdown) {
1062
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1063
999
  if (move.effect == .Binding) {
@@ -1067,9 +1003,9 @@ fn doMove(
1067
1003
  assert(foe.stored().hp > 0);
1068
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1069
1005
  if (sub) {
1070
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1071
1007
  } else {
1072
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1073
1009
  try buildRage(battle, player.foe(), options);
1074
1010
  }
1075
1011
  return null;
@@ -1117,7 +1053,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1117
1053
  const side = battle.side(player);
1118
1054
 
1119
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1120
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1121
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1122
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1123
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
@@ -1174,9 +1110,9 @@ fn calcDamage(
1174
1110
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1175
1111
  }
1176
1112
 
1177
- 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);
1178
1114
 
1179
- 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;
1180
1116
 
1181
1117
  if (def == 0) return false;
1182
1118
 
@@ -1267,7 +1203,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1267
1203
  const foe = battle.foe(player);
1268
1204
 
1269
1205
  if (battle.last_damage == 0) {
1270
- try options.log.fail(battle.active(player), .None);
1206
+ try options.log.fail(.{ battle.active(player), .None });
1271
1207
  return null;
1272
1208
  }
1273
1209
 
@@ -1278,25 +1214,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1278
1214
  else
1279
1215
  foe.last_selected_move);
1280
1216
 
1281
- const used = (if (player.foe() == .P1)
1282
- battle.last_moves.p1_counterable
1283
- else
1284
- battle.last_moves.p2_counterable) != 0;
1285
-
1217
+ const used = battle.lastMove(player.foe()).counterable != 0;
1286
1218
  const selected = foe_last_selected_move.bp > 0 and
1287
1219
  foe.last_selected_move != .Counter and
1288
1220
  (foe_last_selected_move.type == .Normal or
1289
1221
  foe_last_selected_move.type == .Fighting);
1290
1222
 
1291
1223
  if (!used and !selected) {
1292
- try options.log.fail(battle.active(player), .None);
1224
+ try options.log.fail(.{ battle.active(player), .None });
1293
1225
  return null;
1294
1226
  }
1295
1227
 
1296
1228
  if (!used or !selected) {
1297
1229
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1298
1230
  if (!showdown) return Result.Error;
1299
- try options.log.fail(battle.active(player), .None);
1231
+ try options.log.fail(.{ battle.active(player), .None });
1300
1232
  return null;
1301
1233
  }
1302
1234
 
@@ -1332,20 +1264,20 @@ fn applyDamage(
1332
1264
  subbed.active.volatiles.substitute = 0;
1333
1265
  subbed.active.volatiles.Substitute = false;
1334
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1335
- try options.log.end(battle.active(sub_player), .Substitute);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1336
1268
  options.calc.capped(target_player);
1337
1269
  return true;
1338
1270
  } else {
1339
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1340
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1341
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1342
1274
  return false;
1343
1275
  }
1344
1276
  }
1345
1277
 
1346
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1347
1279
  target.stored().hp -= battle.last_damage;
1348
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1349
1281
  return false;
1350
1282
  }
1351
1283
 
@@ -1357,14 +1289,14 @@ fn mirrorMove(
1357
1289
  auto: bool,
1358
1290
  residual: *bool,
1359
1291
  options: anytype,
1360
- ) !?Result {
1292
+ ) anyerror!?Result {
1361
1293
  var side = battle.side(player);
1362
1294
  const foe = battle.foe(player);
1363
1295
 
1364
1296
  side.last_selected_move = foe.last_used_move;
1365
1297
 
1366
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1367
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1368
1300
  return null;
1369
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1370
1302
  incrementPP(side, mslot);
@@ -1384,7 +1316,7 @@ fn metronome(
1384
1316
  auto: bool,
1385
1317
  residual: *bool,
1386
1318
  options: anytype,
1387
- ) !?Result {
1319
+ ) anyerror!?Result {
1388
1320
  var side = battle.side(player);
1389
1321
 
1390
1322
  side.last_selected_move = try Rolls.metronome(battle, player, options);
@@ -1406,12 +1338,12 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1406
1338
  if (mist) {
1407
1339
  assert(!showdown);
1408
1340
  const foe_ident = battle.active(player.foe());
1409
- try options.log.activate(foe_ident, .Mist);
1410
- try options.log.fail(foe_ident, .None);
1341
+ try options.log.activate(.{ foe_ident, .Mist });
1342
+ try options.log.fail(.{ foe_ident, .None });
1411
1343
  } else {
1412
1344
  try options.chance.commit(player, .miss);
1413
- try options.log.lastmiss();
1414
- try options.log.miss(battle.active(player));
1345
+ try options.log.lastmiss(.{});
1346
+ try options.log.miss(.{battle.active(player)});
1415
1347
  }
1416
1348
 
1417
1349
  return false;
@@ -1463,7 +1395,7 @@ fn moveHit(
1463
1395
  const overwritten = overwrite and state > 0;
1464
1396
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1465
1397
 
1466
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1398
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1467
1399
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1468
1400
  accuracy = accuracy * boost[0] / boost[1];
1469
1401
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1502,9 +1434,8 @@ fn checkFaint(
1502
1434
  const foe_fainted = foe.stored().hp == 0;
1503
1435
 
1504
1436
  const player_out = findFirstAlive(side) == 0;
1505
- const foe_out = findFirstAlive(foe) == 0;
1506
- const tie = player_out and foe_out;
1507
- 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;
1508
1439
 
1509
1440
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1510
1441
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1512,20 +1443,20 @@ fn checkFaint(
1512
1443
  assert(!side.active.volatiles.MultiHit);
1513
1444
  assert(!foe.active.volatiles.MultiHit);
1514
1445
 
1515
- if (tie) {
1516
- try options.log.tie();
1446
+ if (player_out and foe_out) {
1447
+ try options.log.tie(.{});
1517
1448
  return Result.Tie;
1518
1449
  } else if (player_out) {
1519
- try options.log.win(player.foe());
1450
+ try options.log.win(.{player.foe()});
1520
1451
  return if (player == .P1) Result.Lose else Result.Win;
1521
1452
  } else if (foe_out) {
1522
- try options.log.win(player);
1453
+ try options.log.win(.{player});
1523
1454
  return if (player == .P1) Result.Win else Result.Lose;
1524
1455
  }
1525
1456
 
1526
1457
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1527
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1528
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1458
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1459
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1529
1460
  }
1530
1461
 
1531
1462
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1561,7 +1492,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1561
1492
  side.stored().stats.spe;
1562
1493
  }
1563
1494
 
1564
- try options.log.faint(battle.active(player), done);
1495
+ try options.log.faint(.{ battle.active(player), done });
1565
1496
  options.calc.capped(player);
1566
1497
  return null;
1567
1498
  }
@@ -1573,21 +1504,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1573
1504
  var volatiles = &side.active.volatiles;
1574
1505
 
1575
1506
  const brn = Status.is(stored.status, .BRN);
1576
- if (brn or Status.is(stored.status, .PSN)) blk: {
1507
+ if (brn or Status.is(stored.status, .PSN)) {
1577
1508
  var damage = @max(stored.stats.hp / 16, 1);
1578
-
1579
1509
  if (volatiles.Toxic) {
1580
1510
  volatiles.toxic += 1;
1581
1511
  damage *= volatiles.toxic;
1582
1512
  }
1513
+ damage = @min(damage, stored.hp);
1583
1514
 
1584
- const amount = @min(damage, stored.hp);
1585
- if (showdown and amount == 0) break :blk;
1586
-
1587
- stored.hp -= amount;
1588
-
1589
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1590
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1515
+ if (!showdown or damage > 0) {
1516
+ stored.hp -= damage;
1517
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1518
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1519
+ }
1591
1520
  }
1592
1521
 
1593
1522
  if (volatiles.LeechSeed) {
@@ -1611,14 +1540,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1611
1540
  const amount = @min(damage, stored.hp);
1612
1541
  stored.hp -= amount;
1613
1542
 
1614
- // As above, Pokémon Showdown uses damageOf but its not relevant
1615
- 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 });
1616
1545
 
1617
1546
  const before = foe_stored.hp;
1618
1547
  // Uncapped damage is added back to the foe
1619
1548
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1620
1549
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1621
- 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 });
1622
1551
  }
1623
1552
  }
1624
1553
 
@@ -1629,18 +1558,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1629
1558
  battle.turn += 1;
1630
1559
 
1631
1560
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1632
- try options.log.tie();
1561
+ try options.log.tie(.{});
1633
1562
  return Result.Tie;
1634
1563
  }
1635
1564
 
1636
1565
  if (showdown and battle.turn >= 1000) {
1637
- try options.log.tie();
1566
+ try options.log.tie(.{});
1638
1567
  return Result.Tie;
1639
1568
  } else if (battle.turn >= 65535) {
1640
1569
  return Result.Error;
1641
1570
  }
1642
1571
 
1643
- try options.log.turn(battle.turn);
1572
+ try options.log.turn(.{battle.turn});
1644
1573
 
1645
1574
  return Result.Default;
1646
1575
  }
@@ -1700,9 +1629,9 @@ fn checkEBC(battle: anytype) bool {
1700
1629
  }
1701
1630
 
1702
1631
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1703
- if (side.active.volatiles.disabled_move != 0) {
1632
+ if (side.active.volatiles.disable_move != 0) {
1704
1633
  // A Pokémon that transforms after being disabled may end up with less move slots
1705
- 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;
1706
1635
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1707
1636
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1708
1637
  m
@@ -1710,14 +1639,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1710
1639
  side.last_selected_move;
1711
1640
  if (m != .None and m == last) {
1712
1641
  side.active.volatiles.Charging = false;
1713
- try options.log.disabled(ident, last);
1642
+ try options.log.cant(.{ ident, .Disable, last });
1714
1643
  return true;
1715
1644
  }
1716
1645
  }
1717
1646
  return false;
1718
1647
  }
1719
1648
 
1720
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1649
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1721
1650
  const side = battle.side(who);
1722
1651
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1723
1652
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1735,7 +1664,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1735
1664
  return true;
1736
1665
  }
1737
1666
 
1738
- inline fn onBegin(
1667
+ fn onBegin(
1739
1668
  battle: anytype,
1740
1669
  player: Player,
1741
1670
  move: Move.Data,
@@ -1743,7 +1672,6 @@ inline fn onBegin(
1743
1672
  residual: *bool,
1744
1673
  options: anytype,
1745
1674
  ) !void {
1746
- assert(move.effect.onBegin());
1747
1675
  return switch (move.effect) {
1748
1676
  .Confusion => Effects.confusion(battle, player, move, options),
1749
1677
  .Conversion => Effects.conversion(battle, player, options),
@@ -1765,8 +1693,7 @@ inline fn onBegin(
1765
1693
  };
1766
1694
  }
1767
1695
 
1768
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1769
- assert(move.effect.onEnd());
1696
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1770
1697
  return switch (move.effect) {
1771
1698
  .Bide => Effects.bide(battle, player, options),
1772
1699
  // zig fmt: off
@@ -1781,14 +1708,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1781
1708
  };
1782
1709
  }
1783
1710
 
1784
- inline fn alwaysHappens(
1711
+ fn alwaysHappens(
1785
1712
  battle: anytype,
1786
1713
  player: Player,
1787
1714
  move: Move.Data,
1788
1715
  residual: *bool,
1789
1716
  options: anytype,
1790
1717
  ) !void {
1791
- assert(move.effect.alwaysHappens());
1792
1718
  return switch (move.effect) {
1793
1719
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1794
1720
  .Explode => Effects.explode(battle, player),
@@ -1800,7 +1726,7 @@ inline fn alwaysHappens(
1800
1726
  };
1801
1727
  }
1802
1728
 
1803
- 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 {
1804
1730
  return switch (move.effect) {
1805
1731
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1806
1732
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1827,7 +1753,7 @@ pub const Effects = struct {
1827
1753
  side.active.volatiles.state = 0;
1828
1754
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1829
1755
 
1830
- try options.log.start(battle.active(player), .Bide);
1756
+ try options.log.start(.{ battle.active(player), .Bide });
1831
1757
  }
1832
1758
 
1833
1759
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1841,11 +1767,11 @@ pub const Effects = struct {
1841
1767
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1842
1768
  if (Status.is(foe_stored.status, .FRZ)) {
1843
1769
  assert(move.type == .Fire);
1844
- try options.log.curestatus(
1770
+ try options.log.curestatus(.{
1845
1771
  battle.active(player.foe()),
1846
1772
  foe_stored.status,
1847
1773
  .Message,
1848
- );
1774
+ });
1849
1775
  foe_stored.status = 0;
1850
1776
  }
1851
1777
  return;
@@ -1859,7 +1785,7 @@ pub const Effects = struct {
1859
1785
  foe_stored.status = Status.init(.BRN);
1860
1786
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1861
1787
 
1862
- 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 });
1863
1789
  }
1864
1790
 
1865
1791
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1869,8 +1795,8 @@ pub const Effects = struct {
1869
1795
  volatiles.Charging = true;
1870
1796
  const move = side.last_selected_move;
1871
1797
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1872
- try options.log.laststill();
1873
- try options.log.prepare(battle.active(player), move);
1798
+ try options.log.laststill(.{});
1799
+ try options.log.prepare(.{ battle.active(player), move });
1874
1800
  }
1875
1801
 
1876
1802
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1881,17 +1807,11 @@ pub const Effects = struct {
1881
1807
  if (!try Rolls.confusionChance(battle, player, options)) return;
1882
1808
  } else {
1883
1809
  if (showdown) {
1884
- if (!try checkHit(battle, player, move, options)) {
1885
- return;
1886
- } else if (sub) {
1887
- return options.log.fail(battle.active(player.foe()), .None);
1888
- }
1810
+ if (!try checkHit(battle, player, move, options)) return;
1811
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1889
1812
  } else {
1890
- if (sub) {
1891
- return options.log.fail(battle.active(player.foe()), .None);
1892
- } else if (!try checkHit(battle, player, move, options)) {
1893
- return;
1894
- }
1813
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
+ if (!try checkHit(battle, player, move, options)) return;
1895
1815
  }
1896
1816
  try options.chance.commit(player, .hit);
1897
1817
  }
@@ -1900,23 +1820,24 @@ pub const Effects = struct {
1900
1820
  foe.active.volatiles.Confusion = true;
1901
1821
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1902
1822
 
1903
- try options.log.start(battle.active(player.foe()), .Confusion);
1823
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1904
1824
  }
1905
1825
 
1906
1826
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1907
1827
  const foe = battle.foe(player);
1908
1828
 
1909
1829
  if (foe.active.volatiles.Invulnerable) {
1910
- try options.log.lastmiss();
1911
- return options.log.miss(battle.active(player));
1830
+ try options.log.lastmiss(.{});
1831
+ return options.log.miss(.{battle.active(player)});
1912
1832
  }
1913
1833
 
1914
1834
  battle.side(player).active.types = foe.active.types;
1915
- return options.log.typechange(
1835
+ return options.log.start(.{
1916
1836
  battle.active(player),
1837
+ .TypeChange,
1917
1838
  foe.active.types,
1918
1839
  battle.active(player.foe()),
1919
- );
1840
+ });
1920
1841
  }
1921
1842
 
1922
1843
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1930,8 +1851,8 @@ pub const Effects = struct {
1930
1851
  try options.chance.commit(player, .hit);
1931
1852
  }
1932
1853
 
1933
- if (volatiles.disabled_move != 0) {
1934
- try options.log.fail(foe_ident, .None);
1854
+ if (volatiles.disable_move != 0) {
1855
+ try options.log.fail(.{ foe_ident, .None });
1935
1856
  return null;
1936
1857
  }
1937
1858
 
@@ -1947,7 +1868,7 @@ pub const Effects = struct {
1947
1868
  // Technically this is still considered simply a "miss" on the cartridge,
1948
1869
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1949
1870
  if (n == 0) {
1950
- try options.log.fail(foe_ident, .None);
1871
+ try options.log.fail(.{ foe_ident, .None });
1951
1872
  return null;
1952
1873
  } else if (err) {
1953
1874
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1955,18 +1876,18 @@ pub const Effects = struct {
1955
1876
  return Result.Error;
1956
1877
  }
1957
1878
 
1958
- volatiles.disabled_move =
1879
+ volatiles.disable_move =
1959
1880
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1960
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1881
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1961
1882
 
1962
- const id = foe.active.move(volatiles.disabled_move).id;
1963
- 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 });
1964
1885
  return null;
1965
1886
  }
1966
1887
 
1967
1888
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1968
- var side = battle.side(player);
1969
- var stored = side.stored();
1889
+ var stored = battle.side(player).stored();
1890
+ const ident = battle.active(player);
1970
1891
 
1971
1892
  if (battle.last_damage == 0) {
1972
1893
  assert(showdown);
@@ -1979,7 +1900,7 @@ pub const Effects = struct {
1979
1900
  if (stored.hp == stored.stats.hp) return;
1980
1901
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1981
1902
 
1982
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1903
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1983
1904
  }
1984
1905
 
1985
1906
  fn explode(battle: anytype, player: Player) !void {
@@ -2011,7 +1932,7 @@ pub const Effects = struct {
2011
1932
  if (side.active.volatiles.FocusEnergy) return;
2012
1933
  side.active.volatiles.FocusEnergy = true;
2013
1934
 
2014
- try options.log.start(battle.active(player), .FocusEnergy);
1935
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
2015
1936
  }
2016
1937
 
2017
1938
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2033,7 +1954,7 @@ pub const Effects = struct {
2033
1954
  foe_stored.status = Status.init(.FRZ);
2034
1955
  // GLITCH: Hyper Beam recharging status is not cleared
2035
1956
 
2036
- try options.log.status(foe_ident, foe_stored.status, .None);
1957
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2037
1958
  }
2038
1959
 
2039
1960
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2041,7 +1962,7 @@ pub const Effects = struct {
2041
1962
  var side = battle.side(player);
2042
1963
  var foe = battle.foe(player);
2043
1964
 
2044
- var side_stored = side.stored();
1965
+ const side_stored = side.stored();
2045
1966
  var foe_stored = foe.stored();
2046
1967
 
2047
1968
  const player_ident = battle.active(player);
@@ -2053,8 +1974,8 @@ pub const Effects = struct {
2053
1974
  side.active.stats = side_stored.stats;
2054
1975
  foe.active.stats = foe_stored.stats;
2055
1976
 
2056
- try log.activate(player_ident, .Haze);
2057
- try log.clearallboost();
1977
+ try log.activate(.{ player_ident, .Haze });
1978
+ try log.clearallboost(.{});
2058
1979
 
2059
1980
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2060
1981
  if (showdown) {
@@ -2062,11 +1983,11 @@ pub const Effects = struct {
2062
1983
  const p: Player = @enumFromInt(i);
2063
1984
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2064
1985
  if (p != player and Status.any(s.stored().status)) {
2065
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1986
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2066
1987
  s.stored().status = 0;
2067
1988
  } else if (showdown and s.stored().status == Status.TOX) {
2068
1989
  s.stored().status = Status.init(.PSN);
2069
- try log.status(battle.active(p), s.stored().status, .None);
1990
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2070
1991
  }
2071
1992
  try clearVolatiles(battle, p, p != player, options);
2072
1993
  }
@@ -2075,7 +1996,7 @@ pub const Effects = struct {
2075
1996
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2076
1997
  foe.last_selected_move = .SKIP_TURN;
2077
1998
  }
2078
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1999
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2079
2000
  foe_stored.status = 0;
2080
2001
  }
2081
2002
  try clearVolatiles(battle, player, false, options);
@@ -2091,7 +2012,7 @@ pub const Effects = struct {
2091
2012
  // GLITCH: HP recovery move failure glitches
2092
2013
  const delta = stored.stats.hp - stored.hp;
2093
2014
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2094
- return try options.log.fail(ident, .None);
2015
+ return options.log.fail(.{ ident, .None });
2095
2016
  }
2096
2017
 
2097
2018
  const rest = side.last_selected_move == .Rest;
@@ -2099,17 +2020,17 @@ pub const Effects = struct {
2099
2020
  // Adding the sleep status runs the sleep condition handler to roll duration
2100
2021
  if (showdown) battle.rng.advance(1);
2101
2022
  stored.status = Status.slf(2);
2102
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2023
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2103
2024
  stored.hp = stored.stats.hp;
2104
2025
  } else {
2105
2026
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2106
2027
  }
2107
- 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 });
2108
2029
  }
2109
2030
 
2110
2031
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2111
2032
  battle.side(player).active.volatiles.Recharging = true;
2112
- try options.log.mustrecharge(battle.active(player));
2033
+ try options.log.mustrecharge(.{battle.active(player)});
2113
2034
  }
2114
2035
 
2115
2036
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2118,33 +2039,32 @@ pub const Effects = struct {
2118
2039
  if (showdown) {
2119
2040
  // Invulnerability trumps type immunity on Pokémon Showdown
2120
2041
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2121
- return options.log.immune(battle.active(player.foe()), .None);
2042
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2122
2043
  }
2123
2044
  if (!try checkHit(battle, player, move, options)) return;
2124
2045
  if (foe.active.volatiles.LeechSeed) return;
2125
2046
  } else {
2126
2047
  if (!try checkHit(battle, player, move, options)) return;
2127
2048
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2128
- try options.log.lastmiss();
2129
- return options.log.miss(battle.active(player));
2049
+ try options.log.lastmiss(.{});
2050
+ return options.log.miss(.{battle.active(player)});
2130
2051
  }
2131
2052
  }
2132
2053
 
2133
2054
  try options.chance.commit(player, .hit);
2134
2055
  foe.active.volatiles.LeechSeed = true;
2135
2056
 
2136
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2057
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2137
2058
  }
2138
2059
 
2139
2060
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2140
2061
  var side = battle.side(player);
2062
+ const ident = battle.active(player);
2141
2063
 
2142
- if (side.active.volatiles.LightScreen) {
2143
- return options.log.fail(battle.active(player), .None);
2144
- }
2064
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2145
2065
  side.active.volatiles.LightScreen = true;
2146
2066
 
2147
- try options.log.start(battle.active(player), .LightScreen);
2067
+ try options.log.start(.{ ident, .LightScreen });
2148
2068
  }
2149
2069
 
2150
2070
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2173,7 +2093,7 @@ pub const Effects = struct {
2173
2093
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2174
2094
  if (!try checkHit(battle, player, move, options)) return;
2175
2095
  try options.chance.commit(player, .hit);
2176
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2177
2097
  }
2178
2098
  }
2179
2099
  if (!try checkHit(battle, player, move, options)) return;
@@ -2182,7 +2102,7 @@ pub const Effects = struct {
2182
2102
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2183
2103
  side.active.move(oslot).id = foe.active.move(rslot).id;
2184
2104
 
2185
- 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 });
2186
2106
  }
2187
2107
 
2188
2108
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2191,7 +2111,7 @@ pub const Effects = struct {
2191
2111
  if (side.active.volatiles.Mist) return;
2192
2112
  side.active.volatiles.Mist = true;
2193
2113
 
2194
- try options.log.start(battle.active(player), .Mist);
2114
+ try options.log.start(.{ battle.active(player), .Mist });
2195
2115
  }
2196
2116
 
2197
2117
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2217,17 +2137,18 @@ pub const Effects = struct {
2217
2137
 
2218
2138
  if (showdown) {
2219
2139
  // Invulnerability trumps type immunity on Pokémon Showdown
2220
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2221
- 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;
2222
2143
  }
2223
2144
  if (Status.any(foe_stored.status)) {
2224
- return log.fail(
2145
+ return log.fail(.{
2225
2146
  foe_ident,
2226
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2227
- );
2147
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2148
+ });
2228
2149
  }
2229
2150
  if (!showdown) {
2230
- if (immune) return log.immune(foe_ident, .None);
2151
+ if (immune) return log.immune(.{ foe_ident, .None });
2231
2152
  if (!try checkHit(battle, player, move, options)) return;
2232
2153
  }
2233
2154
  try options.chance.commit(player, .hit);
@@ -2235,7 +2156,7 @@ pub const Effects = struct {
2235
2156
  foe_stored.status = Status.init(.PAR);
2236
2157
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2237
2158
 
2238
- try log.status(foe_ident, foe_stored.status, .None);
2159
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2239
2160
  }
2240
2161
 
2241
2162
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2257,12 +2178,12 @@ pub const Effects = struct {
2257
2178
  foe_stored.status = Status.init(.PAR);
2258
2179
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2259
2180
 
2260
- 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 });
2261
2182
  }
2262
2183
 
2263
2184
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2264
2185
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2265
- try options.log.fieldactivate();
2186
+ try options.log.fieldactivate(.{});
2266
2187
  }
2267
2188
  }
2268
2189
 
@@ -2277,23 +2198,23 @@ pub const Effects = struct {
2277
2198
  return;
2278
2199
  } else if (foe.active.volatiles.Substitute) {
2279
2200
  if (move.effect != .Poison) return;
2280
- return log.fail(foe_ident, .None);
2201
+ return log.fail(.{ foe_ident, .None });
2281
2202
  } else if (Status.any(foe_stored.status)) {
2282
2203
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2283
2204
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2284
2205
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2285
- 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))
2286
2207
  if (!showdown)
2287
- .Poison
2208
+ Fail.Poison
2288
2209
  else if (toxic == (foe_stored.status == Status.TOX))
2289
- if (toxic) .Toxic else .Poison
2210
+ if (toxic) Fail.Toxic else Fail.Poison
2290
2211
  else
2291
- .None
2212
+ Fail.None
2292
2213
  else
2293
- .None);
2214
+ Fail.None });
2294
2215
  } else if (foe.active.types.includes(.Poison)) {
2295
2216
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2296
- return log.immune(foe_ident, .None);
2217
+ return log.immune(.{ foe_ident, .None });
2297
2218
  }
2298
2219
 
2299
2220
  if (move.effect == .Poison) {
@@ -2312,7 +2233,7 @@ pub const Effects = struct {
2312
2233
  foe.active.volatiles.toxic = 0;
2313
2234
  }
2314
2235
 
2315
- try log.status(foe_ident, foe_stored.status, .None);
2236
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2316
2237
  }
2317
2238
 
2318
2239
  fn rage(battle: anytype, player: Player) !void {
@@ -2332,22 +2253,23 @@ pub const Effects = struct {
2332
2253
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2333
2254
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2334
2255
 
2335
- try options.log.damageOf(
2256
+ try options.log.damage(.{
2336
2257
  battle.active(player),
2337
2258
  stored,
2338
2259
  .RecoilOf,
2339
2260
  battle.active(player.foe()),
2340
- );
2261
+ });
2341
2262
  if (showdown and stored.hp == 0) residual.* = false;
2342
2263
  }
2343
2264
 
2344
2265
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2345
2266
  var side = battle.side(player);
2267
+ const ident = battle.active(player);
2346
2268
 
2347
- 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 });
2348
2270
  side.active.volatiles.Reflect = true;
2349
2271
 
2350
- try options.log.start(battle.active(player), .Reflect);
2272
+ try options.log.start(.{ ident, .Reflect });
2351
2273
  }
2352
2274
 
2353
2275
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2361,10 +2283,10 @@ pub const Effects = struct {
2361
2283
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2362
2284
  } else {
2363
2285
  if (Status.any(foe_stored.status)) {
2364
- return options.log.fail(
2286
+ return options.log.fail(.{
2365
2287
  foe_ident,
2366
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2367
- );
2288
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
+ });
2368
2290
  }
2369
2291
  // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2370
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
@@ -2381,18 +2303,17 @@ pub const Effects = struct {
2381
2303
 
2382
2304
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2383
2305
  const last = battle.side(player).last_selected_move;
2384
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2306
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2385
2307
  }
2386
2308
 
2387
2309
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2388
- try options.log.activate(battle.active(player), .Splash);
2310
+ try options.log.activate(.{ battle.active(player), .Splash });
2389
2311
  }
2390
2312
 
2391
2313
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2392
2314
  var side = battle.side(player);
2393
2315
  if (side.active.volatiles.Substitute) {
2394
- try options.log.fail(battle.active(player), .Substitute);
2395
- return;
2316
+ return options.log.fail(.{ battle.active(player), .Substitute });
2396
2317
  }
2397
2318
 
2398
2319
  assert(side.stored().stats.hp <= 1023);
@@ -2404,28 +2325,32 @@ pub const Effects = struct {
2404
2325
  // avoid having to convert to floating point)
2405
2326
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2406
2327
  if (side.stored().hp < required_hp) {
2407
- try options.log.fail(battle.active(player), .Weak);
2408
- return;
2328
+ return options.log.fail(.{ battle.active(player), .Weak });
2409
2329
  }
2410
2330
 
2411
2331
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2412
2332
  side.stored().hp -= hp;
2413
2333
  side.active.volatiles.substitute = hp + 1;
2414
2334
  side.active.volatiles.Substitute = true;
2415
- try options.log.start(battle.active(player), .Substitute);
2335
+ try options.log.start(.{ battle.active(player), .Substitute });
2416
2336
  if (hp > 0) {
2417
- try options.log.damage(battle.active(player), side.stored(), .None);
2337
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2418
2338
  if (showdown and side.stored().hp == 0) residual.* = false;
2419
2339
  }
2420
2340
  }
2421
2341
 
2422
2342
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2423
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2424
-
2425
2343
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2426
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2427
-
2428
- battle.last_damage = 0;
2344
+ if (showdown) {
2345
+ if (battle.side(player).last_selected_move == .Teleport) return;
2346
+ if (try checkHit(battle, player, move, options)) {
2347
+ try options.chance.commit(player, .hit);
2348
+ }
2349
+ battle.last_damage = 0;
2350
+ } else {
2351
+ try options.log.fail(.{ battle.active(player), .None });
2352
+ try options.log.laststill(.{});
2353
+ }
2429
2354
  }
2430
2355
 
2431
2356
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2463,7 +2388,7 @@ pub const Effects = struct {
2463
2388
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2464
2389
  }
2465
2390
 
2466
- try options.log.transform(battle.active(player), foe_ident);
2391
+ try options.log.transform(.{ battle.active(player), foe_ident });
2467
2392
  }
2468
2393
 
2469
2394
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2493,87 +2418,87 @@ pub const Effects = struct {
2493
2418
  switch (move.effect) {
2494
2419
  .AttackUp1, .AttackUp2, .Rage => {
2495
2420
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2496
- if (boosts.atk == 6) return try log.fail(ident, .None);
2421
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2497
2422
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2498
2423
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2499
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2424
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2500
2425
  if (stats.atk == MAX_STAT_VALUE) {
2501
2426
  boosts.atk -= 1;
2502
2427
  if (showdown) {
2503
- try log.boost(ident, reason, n);
2504
- try log.boost(ident, Boost.Attack, -1);
2428
+ try log.boost(.{ ident, reason, n });
2429
+ try log.boost(.{ ident, .Attack, -1 });
2505
2430
  if (move.effect == .Rage) return;
2506
2431
  }
2507
- return try log.fail(ident, .None);
2432
+ return log.fail(.{ ident, .None });
2508
2433
  }
2509
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2434
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2510
2435
  const stat = unmodifiedStats(battle, side).atk;
2511
2436
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2512
- try log.boost(ident, reason, n);
2437
+ try log.boost(.{ ident, reason, n });
2513
2438
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2514
2439
  if (showdown and move.effect == .Rage) return;
2515
2440
  },
2516
2441
  .DefenseUp1, .DefenseUp2 => {
2517
2442
  assert(boosts.def >= -6 and boosts.def <= 6);
2518
- if (boosts.def == 6) return try log.fail(ident, .None);
2443
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2519
2444
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2520
2445
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2521
2446
  if (stats.def == MAX_STAT_VALUE) {
2522
2447
  boosts.def -= 1;
2523
2448
  if (showdown) {
2524
- try log.boost(ident, .Defense, n);
2525
- try log.boost(ident, .Defense, -1);
2449
+ try log.boost(.{ ident, .Defense, n });
2450
+ try log.boost(.{ ident, .Defense, -1 });
2526
2451
  }
2527
- return try log.fail(ident, .None);
2452
+ return log.fail(.{ ident, .None });
2528
2453
  }
2529
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2454
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2530
2455
  const stat = unmodifiedStats(battle, side).def;
2531
2456
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2532
- try log.boost(ident, .Defense, n);
2457
+ try log.boost(.{ ident, .Defense, n });
2533
2458
  },
2534
2459
  .SpeedUp2 => {
2535
2460
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2536
- if (boosts.spe == 6) return try log.fail(ident, .None);
2461
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2537
2462
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2538
2463
  if (stats.spe == MAX_STAT_VALUE) {
2539
2464
  boosts.spe -= 1;
2540
2465
  if (showdown) {
2541
- try log.boost(ident, .Speed, 2);
2542
- try log.boost(ident, .Speed, -1);
2466
+ try log.boost(.{ ident, .Speed, 2 });
2467
+ try log.boost(.{ ident, .Speed, -1 });
2543
2468
  }
2544
- return try log.fail(ident, .None);
2469
+ return log.fail(.{ ident, .None });
2545
2470
  }
2546
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2471
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2547
2472
  const stat = unmodifiedStats(battle, side).spe;
2548
2473
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2549
- try log.boost(ident, .Speed, 2);
2474
+ try log.boost(.{ ident, .Speed, 2 });
2550
2475
  },
2551
2476
  .SpecialUp1, .SpecialUp2 => {
2552
2477
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2553
- if (boosts.spc == 6) return try log.fail(ident, .None);
2478
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2554
2479
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2555
2480
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2556
2481
  if (stats.spc == MAX_STAT_VALUE) {
2557
2482
  boosts.spc -= 1;
2558
2483
  if (showdown) {
2559
- try log.boost(ident, .SpecialAttack, n);
2560
- try log.boost(ident, .SpecialAttack, -1);
2561
- try log.boost(ident, .SpecialDefense, n);
2562
- 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 });
2563
2488
  }
2564
- return try log.fail(ident, .None);
2489
+ return log.fail(.{ ident, .None });
2565
2490
  }
2566
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2491
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2567
2492
  const stat = unmodifiedStats(battle, side).spc;
2568
2493
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2569
- try log.boost(ident, .SpecialAttack, n);
2570
- try log.boost(ident, .SpecialDefense, n);
2494
+ try log.boost(.{ ident, .SpecialAttack, n });
2495
+ try log.boost(.{ ident, .SpecialDefense, n });
2571
2496
  },
2572
2497
  .EvasionUp1 => {
2573
2498
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2574
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2499
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2575
2500
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2576
- try log.boost(ident, .Evasion, 1);
2501
+ try log.boost(.{ ident, .Evasion, 1 });
2577
2502
  },
2578
2503
  else => unreachable,
2579
2504
  }
@@ -2588,7 +2513,7 @@ pub const Effects = struct {
2588
2513
  const foe_ident = battle.active(player.foe());
2589
2514
  const secondary = move.effect.isStatDownChance();
2590
2515
 
2591
- 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 });
2592
2517
 
2593
2518
  if (secondary) {
2594
2519
  const proc = try Rolls.unboost(battle, player, options);
@@ -2606,82 +2531,82 @@ pub const Effects = struct {
2606
2531
  switch (move.effect) {
2607
2532
  .AttackDown1, .AttackDownChance => {
2608
2533
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2609
- 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 });
2610
2535
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2611
2536
  if (stats.atk == 1) {
2612
2537
  boosts.atk += 1;
2613
2538
  if (showdown) {
2614
- try log.boost(foe_ident, .Attack, -1);
2615
- try log.boost(foe_ident, .Attack, 1);
2539
+ try log.boost(.{ foe_ident, .Attack, -1 });
2540
+ try log.boost(.{ foe_ident, .Attack, 1 });
2616
2541
  }
2617
- return try log.fail(foe_ident, .None);
2542
+ return log.fail(.{ foe_ident, .None });
2618
2543
  }
2619
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2544
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2620
2545
  const stat = unmodifiedStats(battle, foe).atk;
2621
2546
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2622
- try log.boost(foe_ident, .Attack, -1);
2547
+ try log.boost(.{ foe_ident, .Attack, -1 });
2623
2548
  },
2624
2549
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2625
2550
  assert(boosts.def >= -6 and boosts.def <= 6);
2626
- 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 });
2627
2552
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2628
2553
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2629
2554
  if (stats.def == 1) {
2630
2555
  boosts.def += 1;
2631
2556
  if (showdown) {
2632
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2633
- 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 });
2634
2559
  }
2635
- return try log.fail(foe_ident, .None);
2560
+ return log.fail(.{ foe_ident, .None });
2636
2561
  }
2637
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2562
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2638
2563
  const stat = unmodifiedStats(battle, foe).def;
2639
2564
  stats.def = @max(1, stat * mod[0] / mod[1]);
2640
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2565
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2641
2566
  },
2642
2567
  .SpeedDown1, .SpeedDownChance => {
2643
2568
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2644
- 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 });
2645
2570
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2646
2571
  if (stats.spe == 1) {
2647
2572
  boosts.spe += 1;
2648
2573
  if (showdown) {
2649
- try log.boost(foe_ident, .Speed, -1);
2650
- try log.boost(foe_ident, .Speed, 1);
2574
+ try log.boost(.{ foe_ident, .Speed, -1 });
2575
+ try log.boost(.{ foe_ident, .Speed, 1 });
2651
2576
  }
2652
- return try log.fail(foe_ident, .None);
2577
+ return log.fail(.{ foe_ident, .None });
2653
2578
  }
2654
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2579
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2655
2580
  const stat = unmodifiedStats(battle, foe).spe;
2656
2581
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2657
- try log.boost(foe_ident, .Speed, -1);
2582
+ try log.boost(.{ foe_ident, .Speed, -1 });
2658
2583
  assert(boosts.spe >= -6);
2659
2584
  },
2660
2585
  .SpecialDownChance => {
2661
2586
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2662
- 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 });
2663
2588
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2664
2589
  if (stats.spc == 1) {
2665
2590
  boosts.spc += 1;
2666
2591
  if (showdown) {
2667
- try log.boost(foe_ident, .SpecialAttack, -1);
2668
- try log.boost(foe_ident, .SpecialAttack, 1);
2669
- try log.boost(foe_ident, .SpecialDefense, -1);
2670
- 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 });
2671
2596
  }
2672
- return try log.fail(foe_ident, .None);
2597
+ return log.fail(.{ foe_ident, .None });
2673
2598
  }
2674
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2599
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2675
2600
  const stat = unmodifiedStats(battle, foe).spc;
2676
2601
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2677
- try log.boost(foe_ident, .SpecialAttack, -1);
2678
- try log.boost(foe_ident, .SpecialDefense, -1);
2602
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2603
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2679
2604
  },
2680
2605
  .AccuracyDown1 => {
2681
2606
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2682
- 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 });
2683
2608
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2684
- try log.boost(foe_ident, .Accuracy, -1);
2609
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2685
2610
  },
2686
2611
  else => unreachable,
2687
2612
  }
@@ -2705,7 +2630,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2705
2630
  }
2706
2631
  }
2707
2632
 
2708
- inline fn isForced(active: ActivePokemon) bool {
2633
+ fn isForced(active: anytype) bool {
2709
2634
  return active.volatiles.Recharging or active.volatiles.Rage or
2710
2635
  active.volatiles.Thrashing or active.volatiles.Charging;
2711
2636
  }
@@ -2718,27 +2643,27 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2718
2643
 
2719
2644
  options.chance.clearDurations(who, clear);
2720
2645
 
2721
- if (volatiles.disabled_move != 0) {
2722
- volatiles.disabled_move = 0;
2723
- volatiles.disabled_duration = 0;
2724
- 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 });
2725
2650
  }
2726
2651
  if (volatiles.Confusion) {
2727
2652
  // volatiles.confusion is left unchanged
2728
2653
  volatiles.Confusion = false;
2729
- try log.end(ident, .ConfusionSilent);
2654
+ try log.end(.{ ident, .ConfusionSilent });
2730
2655
  }
2731
2656
  if (volatiles.Mist) {
2732
2657
  volatiles.Mist = false;
2733
- try log.end(ident, .Mist);
2658
+ try log.end(.{ ident, .MistSilent });
2734
2659
  }
2735
2660
  if (volatiles.FocusEnergy) {
2736
2661
  volatiles.FocusEnergy = false;
2737
- try log.end(ident, .FocusEnergy);
2662
+ try log.end(.{ ident, .FocusEnergySilent });
2738
2663
  }
2739
2664
  if (volatiles.LeechSeed) {
2740
2665
  volatiles.LeechSeed = false;
2741
- try log.end(ident, .LeechSeed);
2666
+ try log.end(.{ ident, .LeechSeedSilent });
2742
2667
  }
2743
2668
  if (!showdown and volatiles.Toxic) {
2744
2669
  volatiles.Toxic = false;
@@ -2746,24 +2671,26 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2746
2671
  }
2747
2672
  if (volatiles.LightScreen) {
2748
2673
  volatiles.LightScreen = false;
2749
- try log.end(ident, .LightScreen);
2674
+ try log.end(.{ ident, .LightScreenSilent });
2750
2675
  }
2751
2676
  if (volatiles.Reflect) {
2752
2677
  volatiles.Reflect = false;
2753
- try log.end(ident, .Reflect);
2678
+ try log.end(.{ ident, .ReflectSilent });
2754
2679
  }
2755
2680
  if (showdown and volatiles.Toxic) {
2756
2681
  volatiles.Toxic = false;
2757
2682
  // Pokémon Showdown erroneously clears the toxic counter
2758
2683
  volatiles.toxic = 0;
2759
- try log.end(ident, .Toxic);
2684
+ try log.end(.{ ident, .ToxicSilent });
2760
2685
  }
2761
2686
  }
2762
2687
 
2763
2688
  pub const Rolls = struct {
2764
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2689
+ fn speedTie(battle: anytype, options: anytype) !bool {
2765
2690
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2766
2691
  player == .P1
2692
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2693
+ battle.rng.speedTie()
2767
2694
  else if (showdown)
2768
2695
  battle.rng.range(u8, 0, 2) == 0
2769
2696
  else
@@ -2773,9 +2700,11 @@ pub const Rolls = struct {
2773
2700
  return p1;
2774
2701
  }
2775
2702
 
2776
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2703
+ fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2777
2704
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2778
2705
  val == .true
2706
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2707
+ battle.rng.criticalHit(player, rate)
2779
2708
  else if (showdown)
2780
2709
  battle.rng.chance(u8, rate, 256)
2781
2710
  else
@@ -2785,9 +2714,11 @@ pub const Rolls = struct {
2785
2714
  return crit;
2786
2715
  }
2787
2716
 
2788
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2717
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2789
2718
  const roll = if (options.calc.overridden(player, .damage)) |val|
2790
2719
  val
2720
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2721
+ battle.rng.damage(player)
2791
2722
  else roll: {
2792
2723
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2793
2724
  while (true) {
@@ -2796,13 +2727,16 @@ pub const Rolls = struct {
2796
2727
  }
2797
2728
  };
2798
2729
 
2730
+ assert(roll >= 217 and roll <= 255);
2799
2731
  try options.chance.damage(player, roll);
2800
2732
  return roll;
2801
2733
  }
2802
2734
 
2803
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2804
2736
  const ok = if (options.calc.overridden(player, .hit)) |val|
2805
2737
  val == .true
2738
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
+ battle.rng.hit(player, accuracy)
2806
2740
  else if (showdown)
2807
2741
  battle.rng.chance(u8, accuracy, 256)
2808
2742
  else
@@ -2812,9 +2746,11 @@ pub const Rolls = struct {
2812
2746
  return ok;
2813
2747
  }
2814
2748
 
2815
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2749
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2816
2750
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2817
2751
  val == .true
2752
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2753
+ battle.rng.confused(player)
2818
2754
  else if (showdown)
2819
2755
  !battle.rng.chance(u8, 128, 256)
2820
2756
  else
@@ -2824,9 +2760,11 @@ pub const Rolls = struct {
2824
2760
  return cfz;
2825
2761
  }
2826
2762
 
2827
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2763
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2828
2764
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2829
2765
  val == .true
2766
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2767
+ battle.rng.paralyzed(player)
2830
2768
  else if (showdown)
2831
2769
  battle.rng.chance(u8, 63, 256)
2832
2770
  else
@@ -2836,9 +2774,11 @@ pub const Rolls = struct {
2836
2774
  return par;
2837
2775
  }
2838
2776
 
2839
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2777
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2840
2778
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2841
2779
  val == .true
2780
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2781
+ battle.rng.secondaryChance(player, 25)
2842
2782
  else if (showdown)
2843
2783
  battle.rng.chance(u8, 25, 256)
2844
2784
  else
@@ -2848,11 +2788,13 @@ pub const Rolls = struct {
2848
2788
  return proc;
2849
2789
  }
2850
2790
 
2851
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2791
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2852
2792
  const rate: u8 = if (low) 26 else 77;
2853
2793
 
2854
2794
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2855
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2797
+ battle.rng.secondaryChance(player, rate)
2856
2798
  else if (showdown)
2857
2799
  battle.rng.chance(u8, rate, 256)
2858
2800
  else
@@ -2862,11 +2804,13 @@ pub const Rolls = struct {
2862
2804
  return proc;
2863
2805
  }
2864
2806
 
2865
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2807
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2866
2808
  const rate: u8 = if (low) 52 else 103;
2867
2809
 
2868
2810
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2869
2811
  val == .true
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2813
+ battle.rng.secondaryChance(player, rate)
2870
2814
  else if (showdown)
2871
2815
  battle.rng.chance(u8, rate, 256)
2872
2816
  else
@@ -2876,9 +2820,11 @@ pub const Rolls = struct {
2876
2820
  return proc;
2877
2821
  }
2878
2822
 
2879
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2823
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2880
2824
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2881
2825
  val == .true
2826
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2827
+ battle.rng.secondaryChance(player, 85)
2882
2828
  else if (showdown)
2883
2829
  battle.rng.chance(u8, 85, 256)
2884
2830
  else
@@ -2888,14 +2834,14 @@ pub const Rolls = struct {
2888
2834
  return proc;
2889
2835
  }
2890
2836
 
2891
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2837
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2892
2838
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2893
2839
  val
2894
- else if (showdown) move: {
2895
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2896
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2897
- break :move @enumFromInt(r + mod);
2898
- } 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: {
2899
2845
  while (true) {
2900
2846
  const r = battle.rng.next();
2901
2847
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2908,9 +2854,11 @@ pub const Rolls = struct {
2908
2854
  return move;
2909
2855
  }
2910
2856
 
2911
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2857
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2912
2858
  const power = if (options.calc.overridden(player, .psywave)) |val|
2913
2859
  val - 1
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2861
+ battle.rng.psywave(player)
2914
2862
  else if (showdown)
2915
2863
  battle.rng.range(u8, 0, max)
2916
2864
  else power: {
@@ -2920,15 +2868,18 @@ pub const Rolls = struct {
2920
2868
  }
2921
2869
  };
2922
2870
 
2871
+ assert(power < max);
2923
2872
  try options.chance.psywave(player, power, max);
2924
2873
  return power;
2925
2874
  }
2926
2875
 
2927
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2876
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2928
2877
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2929
2878
  @intCast(val)
2879
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
+ battle.rng.sleepDuration(player)
2930
2881
  else if (showdown)
2931
- @intCast(battle.rng.range(u8, 1, 8))
2882
+ battle.rng.range(u3, 1, 8)
2932
2883
  else duration: {
2933
2884
  while (true) {
2934
2885
  const r = battle.rng.next() & 7;
@@ -2936,47 +2887,59 @@ pub const Rolls = struct {
2936
2887
  }
2937
2888
  };
2938
2889
 
2890
+ assert(duration >= 1 and duration <= 7);
2939
2891
  options.chance.duration(.sleep, player, player.foe(), duration);
2940
2892
  return duration;
2941
2893
  }
2942
2894
 
2943
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2895
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2944
2896
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2945
2897
  val
2898
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
+ battle.rng.disableDuration(player)
2946
2900
  else if (showdown)
2947
- @intCast(battle.rng.range(u8, 1, 9))
2901
+ battle.rng.range(u4, 1, 9)
2948
2902
  else
2949
2903
  @intCast((battle.rng.next() & 7) + 1);
2950
2904
 
2905
+ assert(duration >= 1 and duration <= 8);
2951
2906
  options.chance.duration(.disable, player, player.foe(), duration);
2952
2907
  return duration;
2953
2908
  }
2954
2909
 
2955
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2910
+ fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2956
2911
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2957
2912
  @intCast(val)
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
+ battle.rng.confusionDuration(player, self)
2958
2915
  else if (showdown)
2959
- @intCast(battle.rng.range(u8, 2, 6))
2916
+ battle.rng.range(u3, 2, 6)
2960
2917
  else
2961
2918
  @intCast((battle.rng.next() & 3) + 2);
2962
2919
 
2920
+ assert(duration >= 2 and duration <= 5);
2963
2921
  options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2964
2922
  return duration;
2965
2923
  }
2966
2924
 
2967
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2925
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2968
2926
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2969
2927
  @intCast(val)
2928
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
+ battle.rng.attackingDuration(player)
2970
2930
  else if (showdown)
2971
- @intCast(battle.rng.range(u4, 2, 4))
2931
+ battle.rng.range(u3, 2, 4)
2972
2932
  else
2973
2933
  @intCast((battle.rng.next() & 1) + 2);
2974
2934
 
2935
+ assert(duration >= 2 and duration <= 3);
2975
2936
  options.chance.duration(.attacking, player, player, duration);
2976
2937
  return duration;
2977
2938
  }
2978
2939
 
2979
- inline fn distribution(
2940
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2941
+
2942
+ fn distribution(
2980
2943
  battle: anytype,
2981
2944
  comptime effect: Move.Effect,
2982
2945
  player: Player,
@@ -2985,13 +2948,16 @@ pub const Rolls = struct {
2985
2948
  const roll = if (effect == .Binding) .duration else .multi_hit;
2986
2949
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2987
2950
  @intCast(val)
2951
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
+ battle.rng.distribution(player)
2988
2953
  else if (showdown)
2989
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2954
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2990
2955
  else n: {
2991
2956
  const r = (battle.rng.next() & 3);
2992
2957
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2993
2958
  };
2994
2959
 
2960
+ assert(n >= 2 and n <= 5);
2995
2961
  if (effect == .Binding) {
2996
2962
  options.chance.duration(.binding, player, player, n);
2997
2963
  } else {
@@ -3016,7 +2982,9 @@ pub const Rolls = struct {
3016
2982
  else
3017
2983
  null;
3018
2984
 
3019
- 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: {
3020
2988
  if (showdown) {
3021
2989
  if (check_pp == 0) {
3022
2990
  var i: usize = moves.len;
@@ -3032,10 +3000,9 @@ pub const Rolls = struct {
3032
3000
  while (i < moves.len and r > 0) : (i += 1) {
3033
3001
  if (moves[i].pp > 0) {
3034
3002
  r -= 1;
3035
- if (r == 0) break;
3003
+ if (r == 0) break :slot @intCast(i + 1);
3036
3004
  }
3037
3005
  }
3038
- break :slot @intCast(i + 1);
3039
3006
  }
3040
3007
  }
3041
3008
 
@@ -3057,11 +3024,11 @@ test "RNG agreement" {
3057
3024
  expected[i] = @intCast(i * 0x1000000);
3058
3025
  }
3059
3026
 
3060
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3061
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3062
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3063
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3064
- 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 };
3065
3032
 
3066
3033
  for (0..expected.len) |i| {
3067
3034
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3097,7 +3064,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3097
3064
  const side = battle.side(player);
3098
3065
  const foe = battle.foe(player);
3099
3066
 
3100
- var active = side.active;
3067
+ const active = &side.active;
3101
3068
  const stored = side.stored();
3102
3069
 
3103
3070
  // While players are not given any input options on the cartridge in these cases,
@@ -3151,7 +3118,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3151
3118
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3152
3119
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3153
3120
  const struggle =
3154
- 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);
3155
3122
  const s = if (struggle) 0 else slot;
3156
3123
  out[n] = .{ .type = .Move, .data = s };
3157
3124
  n += 1;
@@ -3166,7 +3133,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3166
3133
  const m = active.moves[slot - 1];
3167
3134
  if (m.id == .None) break;
3168
3135
  if (m.pp == 0) continue;
3169
- if (active.volatiles.disabled_move == slot) continue;
3136
+ if (active.volatiles.disable_move == slot) continue;
3170
3137
  out[n] = .{ .type = .Move, .data = slot };
3171
3138
  n += 1;
3172
3139
  }