@pkmn/engine 0.1.0-dev.6158d5ad → 0.1.0-dev.62399ed5

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