@pkmn/engine 0.1.0-dev.dd5b9081 → 0.1.0-dev.deda8593

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 +9 -9
  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 +739 -318
  28. package/src/lib/common/rational.zig +12 -9
  29. package/src/lib/common/rng.zig +25 -16
  30. package/src/lib/gen1/calc.zig +219 -49
  31. package/src/lib/gen1/chance.zig +138 -115
  32. package/src/lib/gen1/data/moves.zig +190 -176
  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 +44 -44
  36. package/src/lib/gen1/helpers.zig +148 -82
  37. package/src/lib/gen1/mechanics.zig +392 -406
  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 +377 -344
  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 +52 -44
  45. package/src/lib/gen2/generated.zig +806 -0
  46. package/src/lib/gen2/helpers.zig +37 -7
  47. package/src/lib/gen2/mechanics.zig +3086 -211
  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 -894
  53. package/src/lib/gen2/README.md +0 -383
@@ -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,7 +196,7 @@ fn selectMove(
210
196
  if (choice.data == 0) {
211
197
  const struggle = ok: {
212
198
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
199
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
200
  }
215
201
  break :ok true;
216
202
  };
@@ -229,7 +215,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
229
215
  if (c.data == 0) {
230
216
  side.last_selected_move = .Struggle;
231
217
  } else {
232
- assert(showdown or side.active.volatiles.disabled_move != c.data);
218
+ assert(showdown or side.active.volatiles.disable_move != c.data);
233
219
  const move = side.active.move(c.data);
234
220
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
235
221
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,14 +256,14 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
256
 
271
257
  foe.active.volatiles.Binding = false;
272
258
 
273
- try options.log.switched(battle.active(player), incoming);
259
+ try options.log.switched(.{ battle.active(player), incoming });
274
260
  options.chance.switched(player, side.order[0], out);
275
261
 
276
262
  if (showdown and incoming.status == Status.TOX) {
277
263
  incoming.status = Status.init(.PSN);
278
264
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
279
265
  // rather not clutter up turnOrder just for this (incorrect) log message
280
- try options.log.status(battle.active(player), incoming.status, .Silent);
266
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
281
267
  }
282
268
  }
283
269
 
@@ -340,12 +326,10 @@ fn doTurn(
340
326
  player_choice: Choice,
341
327
  player_rewrap: bool,
342
328
  player_skip: bool,
343
- player_from: ?Move,
344
329
  foe_player: Player,
345
330
  foe_choice: Choice,
346
331
  foe_rewrap: bool,
347
332
  foe_skip: bool,
348
- foe_from: ?Move,
349
333
  options: anytype,
350
334
  ) !?Result {
351
335
  assert(player_choice.type != .Pass);
@@ -358,16 +342,15 @@ fn doTurn(
358
342
  player_choice,
359
343
  player_rewrap,
360
344
  player_skip,
361
- player_from,
362
345
  &residual,
363
346
  options,
364
347
  )) |r| return r;
365
348
  if (!replace) {
366
349
  if (player_choice.type != .Switch) {
367
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
368
351
  }
369
352
  if (residual) try handleResidual(battle, player, options);
370
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
371
354
  } else if (foe_choice.type == .Pass) return null;
372
355
 
373
356
  options.chance.clearPending();
@@ -381,16 +364,15 @@ fn doTurn(
381
364
  foe_choice,
382
365
  foe_rewrap,
383
366
  foe_skip,
384
- foe_from,
385
367
  &residual,
386
368
  options,
387
369
  )) |r| return r;
388
370
  if (!replace) {
389
371
  if (!calc and foe_choice.type != .Switch) {
390
- if (try checkFaint(battle, player, options)) |r| return r;
372
+ if (try checkFaint(battle, player, true, options)) |r| return r;
391
373
  }
392
374
  if (residual) try handleResidual(battle, foe_player, options);
393
- if (try checkFaint(battle, foe_player, options)) |r| return r;
375
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
394
376
  }
395
377
 
396
378
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -409,7 +391,6 @@ fn executeMove(
409
391
  choice: Choice,
410
392
  rewrap: bool,
411
393
  skip: bool,
412
- from: ?Move,
413
394
  residual: *bool,
414
395
  options: anytype,
415
396
  ) !?Result {
@@ -426,7 +407,7 @@ fn executeMove(
426
407
  if (side.last_selected_move == .SKIP_TURN) {
427
408
  assert(!showdown);
428
409
  if (battle.foe(player).active.volatiles.Binding) {
429
- try options.log.cant(battle.active(player), .Bound);
410
+ try options.log.cant(.{ battle.active(player), .Bound });
430
411
  }
431
412
  return null;
432
413
  }
@@ -444,11 +425,13 @@ fn executeMove(
444
425
  mslot = @intCast(battle.lastMove(player).index);
445
426
  const stored = side.stored();
446
427
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
447
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
448
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
428
+ auto = isForced(&side.active) or
429
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
430
+ side.last_selected_move == .HyperBeam or
431
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
449
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
450
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
451
- // 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
452
435
  if (!auto) {
453
436
  // side.active.moves(slot) is safe to check even though the slot in question might not
454
437
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -467,7 +450,7 @@ fn executeMove(
467
450
 
468
451
  var skip_can = false;
469
452
  var skip_pp = false;
470
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
453
+ switch (try beforeMove(battle, player, skip, residual, options)) {
471
454
  .done => return null,
472
455
  .skip_can => skip_can = true,
473
456
  .skip_pp => skip_pp = true,
@@ -475,9 +458,9 @@ fn executeMove(
475
458
  .err => return @as(?Result, Result.Error),
476
459
  }
477
460
 
478
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
479
- return null;
480
- }
461
+ const can = skip_can or
462
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
+ if (!can) return null;
481
464
 
482
465
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
483
466
  }
@@ -488,7 +471,6 @@ fn beforeMove(
488
471
  battle: anytype,
489
472
  player: Player,
490
473
  skip: bool,
491
- from: ?Move,
492
474
  residual: *bool,
493
475
  options: anytype,
494
476
  ) !BeforeMove {
@@ -515,23 +497,23 @@ fn beforeMove(
515
497
  }
516
498
 
517
499
  if (duration == 0) {
518
- try log.curestatus(ident, before, .Message);
500
+ try log.curestatus(.{ ident, before, .Message });
519
501
  stored.status = 0; // clears EXT if present
520
502
  } else {
521
- try log.cant(ident, .Sleep);
503
+ try log.cant(.{ ident, .Sleep });
522
504
  }
523
505
  side.last_used_move = .None;
524
506
  return .done;
525
507
  }
526
508
 
527
509
  if (Status.is(stored.status, .FRZ)) {
528
- try log.cant(ident, .Freeze);
510
+ try log.cant(.{ ident, .Freeze });
529
511
  side.last_used_move = .None;
530
512
  return .done;
531
513
  }
532
514
 
533
515
  if (skip or foe.active.volatiles.Binding) {
534
- try log.cant(ident, .Bound);
516
+ try log.cant(.{ ident, .Bound });
535
517
  return .done;
536
518
  }
537
519
 
@@ -539,27 +521,27 @@ fn beforeMove(
539
521
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
540
522
  // Pokémon can sometimes flinch multiple times from the same original hit
541
523
  if (!showdown) volatiles.Flinch = false;
542
- try log.cant(ident, .Flinch);
524
+ try log.cant(.{ ident, .Flinch });
543
525
  return .done;
544
526
  }
545
527
 
546
528
  if (volatiles.Recharging) {
547
529
  volatiles.Recharging = false;
548
- try log.cant(ident, .Recharge);
530
+ try log.cant(.{ ident, .Recharge });
549
531
  return .done;
550
532
  }
551
533
 
552
- if (volatiles.disabled_duration > 0) {
534
+ if (volatiles.disable_duration > 0) {
553
535
  if (options.calc.modify(player, .disable)) |extend| {
554
- if (!extend) volatiles.disabled_duration = 0;
536
+ if (!extend) volatiles.disable_duration = 0;
555
537
  } else {
556
- volatiles.disabled_duration -= 1;
538
+ volatiles.disable_duration -= 1;
557
539
  }
558
- try options.chance.disable(player, volatiles.disabled_duration);
540
+ try options.chance.disable(player, volatiles.disable_duration);
559
541
 
560
- if (volatiles.disabled_duration == 0) {
561
- volatiles.disabled_move = 0;
562
- try log.end(ident, .Disable);
542
+ if (volatiles.disable_duration == 0) {
543
+ volatiles.disable_move = 0;
544
+ try log.end(.{ ident, .Disable });
563
545
  }
564
546
  }
565
547
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -584,9 +566,9 @@ fn beforeMove(
584
566
 
585
567
  if (volatiles.confusion == 0) {
586
568
  volatiles.Confusion = false;
587
- try log.end(ident, .Confusion);
569
+ try log.end(.{ ident, .Confusion });
588
570
  } else {
589
- try log.activate(ident, .Confusion);
571
+ try log.activate(.{ ident, .Confusion });
590
572
 
591
573
  if (try Rolls.confused(battle, player, options)) {
592
574
  assert(!volatiles.MultiHit);
@@ -637,7 +619,7 @@ fn beforeMove(
637
619
  volatiles.Binding = false;
638
620
  options.chance.clearDurations(player, null);
639
621
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
640
- try log.cant(ident, .Paralysis);
622
+ try log.cant(.{ ident, .Paralysis });
641
623
  return .done;
642
624
  }
643
625
 
@@ -653,8 +635,6 @@ fn beforeMove(
653
635
  volatiles.state +%= battle.last_damage;
654
636
  }
655
637
 
656
- assert(volatiles.attacks > 0);
657
-
658
638
  if (options.calc.modify(player, .attacking)) |extend| {
659
639
  if (!extend) volatiles.attacks = 0;
660
640
  } else {
@@ -663,18 +643,18 @@ fn beforeMove(
663
643
  try options.chance.attacking(player, volatiles.attacks);
664
644
 
665
645
  if (volatiles.attacks != 0) {
666
- try log.activate(ident, .Bide);
646
+ try log.activate(.{ ident, .Bide });
667
647
  return .done;
668
648
  }
669
649
 
670
650
  volatiles.Bide = false;
671
- try log.end(ident, .Bide);
651
+ try log.end(.{ ident, .Bide });
672
652
 
673
653
  battle.last_damage = volatiles.state *% 2;
674
654
  volatiles.state = 0;
675
655
 
676
656
  if (battle.last_damage == 0) {
677
- try log.fail(ident, .None);
657
+ try log.fail(.{ ident, .None });
678
658
  return .done;
679
659
  }
680
660
 
@@ -694,9 +674,6 @@ fn beforeMove(
694
674
  }
695
675
 
696
676
  if (volatiles.Thrashing) {
697
- assert(volatiles.attacks > 0);
698
- assert(from != null);
699
-
700
677
  if (options.calc.modify(player, .attacking)) |extend| {
701
678
  if (!extend) volatiles.attacks = 0;
702
679
  } else {
@@ -705,23 +682,21 @@ fn beforeMove(
705
682
  try options.chance.attacking(player, volatiles.attacks);
706
683
 
707
684
  if (!showdown and handleThrashing(battle, active, player, options)) {
708
- try log.start(battle.active(player), .ConfusionSilent);
685
+ try log.start(.{ battle.active(player), .ConfusionSilent });
709
686
  }
710
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
687
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
711
688
  if (showdown) {
712
689
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
713
690
  // used in useMove and doesn't have the notion of skipping canMove semantics
714
691
  side.last_used_move = side.last_selected_move;
715
692
  if (handleThrashing(battle, active, player, options)) {
716
- try log.start(battle.active(player), .ConfusionSilent);
693
+ try log.start(.{ battle.active(player), .ConfusionSilent });
717
694
  }
718
695
  }
719
696
  return .skip_can;
720
697
  }
721
698
 
722
699
  if (volatiles.Binding) {
723
- assert(volatiles.attacks > 0);
724
-
725
700
  if (options.calc.modify(player, .binding)) |extend| {
726
701
  if (!extend) volatiles.attacks = 0;
727
702
  } else {
@@ -729,12 +704,7 @@ fn beforeMove(
729
704
  }
730
705
  try options.chance.binding(player, volatiles.attacks);
731
706
 
732
- try log.move(
733
- battle.active(player),
734
- side.last_selected_move,
735
- battle.active(player.foe()),
736
- side.last_selected_move,
737
- );
707
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
738
708
  if (showdown or battle.last_damage != 0) {
739
709
  const sub = showdown and foe.active.volatiles.Substitute;
740
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -754,7 +724,7 @@ fn canMove(
754
724
  mslot: u4,
755
725
  auto: bool,
756
726
  skip_pp: bool,
757
- from: ?Move,
727
+ from: Move,
758
728
  residual: *bool,
759
729
  options: anytype,
760
730
  ) !bool {
@@ -762,33 +732,21 @@ fn canMove(
762
732
  const player_ident = battle.active(player);
763
733
  const move = Move.get(side.last_selected_move);
764
734
 
765
- const foe_last = battle.foe(player).last_used_move;
766
- const special = from != null and (from.? == .Metronome and
767
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
768
-
769
- var skip = skip_pp;
770
735
  if (side.active.volatiles.Charging) {
771
736
  side.active.volatiles.Charging = false;
772
737
  side.active.volatiles.Invulnerable = false;
773
738
  } else if (move.effect == .Charge) {
774
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
739
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
775
740
  setCounterable(battle, player, side, move);
776
741
  try Effects.charge(battle, player, options);
777
742
  return false;
778
743
  }
779
744
 
780
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
781
- if (!skip) decrementPP(side, mslot, auto);
745
+ side.last_used_move = side.last_selected_move;
746
+ if (!skip_pp) decrementPP(side, mslot, auto);
782
747
 
783
748
  const target = if (move.target == .Self) player else player.foe();
784
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
785
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
786
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
787
- // meaning sometimes we lose this information. from is still set because it not being null is
788
- // relevant for control flow, but we don't have anything useful to put in the log. This is
789
- // regrettable, but this information is more "nice to have" than required
790
- const f = if (from != null and from.? == .None) null else from;
791
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
749
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
792
750
  setCounterable(battle, player, side, move);
793
751
 
794
752
  if (move.effect.onBegin()) {
@@ -826,12 +784,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
826
784
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
827
785
  if (volatiles.Bide) return;
828
786
 
829
- assert(active.move(mslot).pp > 0 or auto);
830
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
787
+ var move_slot = active.move(mslot);
788
+ assert(move_slot.pp > 0 or auto);
789
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
831
790
  if (volatiles.Transform) return;
832
791
 
833
- assert(side.stored().move(mslot).pp > 0 or auto);
834
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
792
+ move_slot = side.stored().move(mslot);
793
+ assert(move_slot.pp > 0 or auto);
794
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
835
795
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
836
796
  }
837
797
 
@@ -892,7 +852,7 @@ fn doMove(
892
852
  break :miss true;
893
853
  }
894
854
  break :miss (move.target != .Self and
895
- !moveHit(battle, player, move, &immune, &mist, options));
855
+ !try moveHit(battle, player, move, &immune, &mist, options));
896
856
  };
897
857
  assert(!immune or miss or (showdown and move.effect == .Binding));
898
858
 
@@ -935,11 +895,11 @@ fn doMove(
935
895
  }
936
896
  }
937
897
 
938
- var zero = battle.last_damage == 0;
898
+ const zero = battle.last_damage == 0;
939
899
  miss = if (showdown or skip)
940
900
  miss
941
901
  else
942
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
902
+ (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
943
903
 
944
904
  assert(showdown or miss or battle.last_damage > 0 or skip);
945
905
  assert((!showdown and miss) or !(ohko and immune));
@@ -963,16 +923,16 @@ fn doMove(
963
923
  ohko = (!showdown or (!immune and !invulnerable)) and
964
924
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
965
925
  if (ohko) {
966
- try log.immune(foe_ident, .OHKO);
926
+ try log.immune(.{ foe_ident, .OHKO });
967
927
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
968
- try log.immune(foe_ident, .None);
928
+ try log.immune(.{ foe_ident, .None });
969
929
  } else if (mist) {
970
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
971
- try log.fail(foe_ident, .None);
930
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
+ try log.fail(.{ foe_ident, .None });
972
932
  } else {
973
- if (showdown or !zero) try options.chance.commit(player, .miss);
974
- try log.lastmiss();
975
- 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)});
976
936
  }
977
937
  if (move.effect == .JumpKick) {
978
938
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -989,13 +949,13 @@ fn doMove(
989
949
  try options.chance.commit(player, .binding);
990
950
  }
991
951
  return null;
992
- } else {
952
+ } else if (!showdown) {
993
953
  try options.chance.commit(player, .hit);
994
954
  }
995
955
 
996
956
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
997
957
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
998
- // it up above before damage calculation).
958
+ // it up above before damage calculation)
999
959
  if (!showdown and move.effect.isMulti()) {
1000
960
  try Effects.multiHit(battle, player, move, options);
1001
961
  hits = side.active.volatiles.attacks;
@@ -1008,15 +968,15 @@ fn doMove(
1008
968
  var hit: u4 = 0;
1009
969
  while (hit < hits) {
1010
970
  if (hit == 0) {
1011
- if (crit) try log.crit(battle.active(player.foe()));
971
+ if (crit) try log.crit(.{battle.active(player.foe())});
1012
972
  if (effectiveness > Effectiveness.neutral) {
1013
- try log.supereffective(battle.active(player.foe()));
973
+ try log.supereffective(.{battle.active(player.foe())});
1014
974
  } else if (effectiveness < Effectiveness.neutral) {
1015
- try log.resisted(battle.active(player.foe()));
975
+ try log.resisted(.{battle.active(player.foe())});
1016
976
  }
1017
977
  }
1018
978
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1019
- if (hit == 0 and ohko) try log.ohko();
979
+ if (hit == 0 and ohko) try log.ohko(.{});
1020
980
  hit += 1;
1021
981
  if (foe.stored().hp == 0) break;
1022
982
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1033,7 +993,7 @@ fn doMove(
1033
993
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1034
994
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1035
995
  }
1036
- try log.hitcount(battle.active(player.foe()), hit);
996
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1037
997
  } else if (showdown) {
1038
998
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1039
999
  if (move.effect == .Binding) {
@@ -1043,9 +1003,9 @@ fn doMove(
1043
1003
  assert(foe.stored().hp > 0);
1044
1004
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1045
1005
  if (sub) {
1046
- try log.activate(battle.active(player.foe()), .Substitute);
1006
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1047
1007
  } else {
1048
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1049
1009
  try buildRage(battle, player.foe(), options);
1050
1010
  }
1051
1011
  return null;
@@ -1054,12 +1014,12 @@ fn doMove(
1054
1014
  }
1055
1015
 
1056
1016
  // Substitute being broken nullifies the move's effect completely so even
1057
- // 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
1058
1018
  if (nullified) return null;
1059
1019
 
1060
1020
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1061
1021
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1062
- // 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
1063
1023
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1064
1024
 
1065
1025
  if (foe.stored().hp == 0) return null;
@@ -1074,7 +1034,7 @@ fn doMove(
1074
1034
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1075
1035
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1076
1036
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1077
- // puts the |-status| message before |-hitcount| instead of after.
1037
+ // puts the |-status| message before |-hitcount| instead of after
1078
1038
  if (move.effect == .Twineedle) {
1079
1039
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1080
1040
  } else if (move.effect == .Disable) {
@@ -1093,12 +1053,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1093
1053
  const side = battle.side(player);
1094
1054
 
1095
1055
  // Base speed is used for the critical hit calculation, even when Transform-ed
1096
- var rate = @as(u16, Species.chance(side.stored().species));
1056
+ var rate: u16 = Species.chance(side.stored().species);
1097
1057
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1098
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1099
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1100
1060
 
1101
- 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);
1102
1064
  }
1103
1065
 
1104
1066
  fn calcDamage(
@@ -1150,9 +1112,9 @@ fn calcDamage(
1150
1112
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1151
1113
  }
1152
1114
 
1153
- 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);
1154
1116
 
1155
- 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;
1156
1118
 
1157
1119
  if (def == 0) return false;
1158
1120
 
@@ -1213,7 +1175,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1213
1175
  const foe = battle.foe(player);
1214
1176
 
1215
1177
  if (!try checkHit(battle, player, move, options)) return null;
1216
- try options.chance.commit(player, .hit);
1217
1178
 
1218
1179
  battle.last_damage = switch (side.last_selected_move) {
1219
1180
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1243,7 +1204,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1243
1204
  const foe = battle.foe(player);
1244
1205
 
1245
1206
  if (battle.last_damage == 0) {
1246
- try options.log.fail(battle.active(player), .None);
1207
+ try options.log.fail(.{ battle.active(player), .None });
1247
1208
  return null;
1248
1209
  }
1249
1210
 
@@ -1261,22 +1222,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1261
1222
  foe_last_selected_move.type == .Fighting);
1262
1223
 
1263
1224
  if (!used and !selected) {
1264
- try options.log.fail(battle.active(player), .None);
1225
+ try options.log.fail(.{ battle.active(player), .None });
1265
1226
  return null;
1266
1227
  }
1267
1228
 
1268
1229
  if (!used or !selected) {
1269
1230
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1270
1231
  if (!showdown) return Result.Error;
1271
- try options.log.fail(battle.active(player), .None);
1232
+ try options.log.fail(.{ battle.active(player), .None });
1272
1233
  return null;
1273
1234
  }
1274
1235
 
1275
1236
  battle.last_damage *|= 2;
1276
1237
 
1277
- // Pokémon Showdown calls checkHit before Counter
1238
+ // Pokémon Showdown calls moveHit before Counter
1278
1239
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1279
- try options.chance.commit(player, .hit);
1280
1240
 
1281
1241
  const sub = showdown and foe.active.volatiles.Substitute;
1282
1242
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1304,20 +1264,20 @@ fn applyDamage(
1304
1264
  subbed.active.volatiles.substitute = 0;
1305
1265
  subbed.active.volatiles.Substitute = false;
1306
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1307
- try options.log.end(battle.active(sub_player), .Substitute);
1308
- options.calc.capped(target_player);
1267
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
+ options.calc.capped(sub_player);
1309
1269
  return true;
1310
1270
  } else {
1311
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1312
1272
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1313
- try options.log.activate(battle.active(sub_player), .Substitute);
1273
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1314
1274
  return false;
1315
1275
  }
1316
1276
  }
1317
1277
 
1318
1278
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1319
1279
  target.stored().hp -= battle.last_damage;
1320
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1280
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1321
1281
  return false;
1322
1282
  }
1323
1283
 
@@ -1336,7 +1296,7 @@ fn mirrorMove(
1336
1296
  side.last_selected_move = foe.last_used_move;
1337
1297
 
1338
1298
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1339
- try options.log.fail(battle.active(player), .None);
1299
+ try options.log.fail(.{ battle.active(player), .None });
1340
1300
  return null;
1341
1301
  } else if (!showdown or foe.last_used_move != .Struggle) {
1342
1302
  incrementPP(side, mslot);
@@ -1372,20 +1332,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1372
1332
  var immune = false;
1373
1333
  var mist = false;
1374
1334
 
1375
- 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
+ }
1376
1339
 
1377
1340
  assert(!immune);
1378
1341
  if (mist) {
1379
1342
  assert(!showdown);
1380
1343
  const foe_ident = battle.active(player.foe());
1381
- try options.log.activate(foe_ident, .Mist);
1382
- try options.log.fail(foe_ident, .None);
1344
+ try options.log.activate(.{ foe_ident, .Mist });
1345
+ try options.log.fail(.{ foe_ident, .None });
1383
1346
  } else {
1384
- try options.chance.commit(player, .miss);
1385
- try options.log.lastmiss();
1386
- try options.log.miss(battle.active(player));
1347
+ try options.log.lastmiss(.{});
1348
+ try options.log.miss(.{battle.active(player)});
1387
1349
  }
1388
1350
 
1351
+ if (!showdown) try options.chance.commit(player, .miss);
1389
1352
  return false;
1390
1353
  }
1391
1354
 
@@ -1396,7 +1359,7 @@ fn moveHit(
1396
1359
  immune: *bool,
1397
1360
  mist: *bool,
1398
1361
  options: anytype,
1399
- ) bool {
1362
+ ) !bool {
1400
1363
  var side = battle.side(player);
1401
1364
  const foe = battle.foe(player);
1402
1365
 
@@ -1435,7 +1398,7 @@ fn moveHit(
1435
1398
  const overwritten = overwrite and state > 0;
1436
1399
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1437
1400
 
1438
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1401
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1439
1402
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1440
1403
  accuracy = accuracy * boost[0] / boost[1];
1441
1404
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1447,7 +1410,7 @@ fn moveHit(
1447
1410
  if (overwrite) side.active.volatiles.state = accuracy;
1448
1411
 
1449
1412
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1450
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1413
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1451
1414
  };
1452
1415
 
1453
1416
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1465,6 +1428,7 @@ fn moveHit(
1465
1428
  fn checkFaint(
1466
1429
  battle: anytype,
1467
1430
  player: Player,
1431
+ cap: bool,
1468
1432
  options: anytype,
1469
1433
  ) @TypeOf(options.log).Error!?Result {
1470
1434
  const side = battle.side(player);
@@ -1474,33 +1438,32 @@ fn checkFaint(
1474
1438
  const foe_fainted = foe.stored().hp == 0;
1475
1439
 
1476
1440
  const player_out = findFirstAlive(side) == 0;
1477
- const foe_out = findFirstAlive(foe) == 0;
1478
- const tie = player_out and foe_out;
1479
- const more = tie or player_out or foe_out;
1441
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1442
+ const more = player_out or foe_out;
1480
1443
 
1481
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1482
- 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;
1483
1446
 
1484
1447
  assert(!side.active.volatiles.MultiHit);
1485
1448
  assert(!foe.active.volatiles.MultiHit);
1486
1449
 
1487
- if (tie) {
1488
- try options.log.tie();
1450
+ if (player_out and foe_out) {
1451
+ try options.log.tie(.{});
1489
1452
  return Result.Tie;
1490
1453
  } else if (player_out) {
1491
- try options.log.win(player.foe());
1454
+ try options.log.win(.{player.foe()});
1492
1455
  return if (player == .P1) Result.Lose else Result.Win;
1493
1456
  } else if (foe_out) {
1494
- try options.log.win(player);
1457
+ try options.log.win(.{player});
1495
1458
  return if (player == .P1) Result.Win else Result.Lose;
1496
1459
  }
1497
1460
 
1498
1461
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1499
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1500
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1462
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1463
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1501
1464
  }
1502
1465
 
1503
- 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 {
1504
1467
  var side = battle.side(player);
1505
1468
  var foe = battle.foe(player);
1506
1469
  assert(side.stored().hp == 0);
@@ -1533,8 +1496,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1533
1496
  side.stored().stats.spe;
1534
1497
  }
1535
1498
 
1536
- try options.log.faint(battle.active(player), done);
1537
- 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);
1538
1503
  return null;
1539
1504
  }
1540
1505
 
@@ -1555,8 +1520,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1555
1520
 
1556
1521
  if (!showdown or damage > 0) {
1557
1522
  stored.hp -= damage;
1558
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1559
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
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 });
1560
1525
  }
1561
1526
  }
1562
1527
 
@@ -1581,14 +1546,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1581
1546
  const amount = @min(damage, stored.hp);
1582
1547
  stored.hp -= amount;
1583
1548
 
1584
- // As above, Pokémon Showdown uses damageOf but its not relevant
1585
- 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 });
1586
1551
 
1587
1552
  const before = foe_stored.hp;
1588
1553
  // Uncapped damage is added back to the foe
1589
1554
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1590
1555
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1591
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1556
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1592
1557
  }
1593
1558
  }
1594
1559
 
@@ -1599,18 +1564,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1599
1564
  battle.turn += 1;
1600
1565
 
1601
1566
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1602
- try options.log.tie();
1567
+ try options.log.tie(.{});
1603
1568
  return Result.Tie;
1604
1569
  }
1605
1570
 
1606
1571
  if (showdown and battle.turn >= 1000) {
1607
- try options.log.tie();
1572
+ try options.log.tie(.{});
1608
1573
  return Result.Tie;
1609
1574
  } else if (battle.turn >= 65535) {
1610
1575
  return Result.Error;
1611
1576
  }
1612
1577
 
1613
- try options.log.turn(battle.turn);
1578
+ try options.log.turn(.{battle.turn});
1614
1579
 
1615
1580
  return Result.Default;
1616
1581
  }
@@ -1670,9 +1635,9 @@ fn checkEBC(battle: anytype) bool {
1670
1635
  }
1671
1636
 
1672
1637
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1673
- if (side.active.volatiles.disabled_move != 0) {
1638
+ if (side.active.volatiles.disable_move != 0) {
1674
1639
  // A Pokémon that transforms after being disabled may end up with less move slots
1675
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1640
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1676
1641
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1677
1642
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1678
1643
  m
@@ -1680,14 +1645,14 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1680
1645
  side.last_selected_move;
1681
1646
  if (m != .None and m == last) {
1682
1647
  side.active.volatiles.Charging = false;
1683
- try options.log.disabled(ident, last);
1648
+ try options.log.cant(.{ ident, .Disable, last });
1684
1649
  return true;
1685
1650
  }
1686
1651
  }
1687
1652
  return false;
1688
1653
  }
1689
1654
 
1690
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1655
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1691
1656
  const side = battle.side(who);
1692
1657
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1693
1658
  try Effects.boost(battle, who, Move.get(.Rage), options);
@@ -1705,7 +1670,7 @@ fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, opti
1705
1670
  return true;
1706
1671
  }
1707
1672
 
1708
- inline fn onBegin(
1673
+ fn onBegin(
1709
1674
  battle: anytype,
1710
1675
  player: Player,
1711
1676
  move: Move.Data,
@@ -1713,7 +1678,6 @@ inline fn onBegin(
1713
1678
  residual: *bool,
1714
1679
  options: anytype,
1715
1680
  ) !void {
1716
- assert(move.effect.onBegin());
1717
1681
  return switch (move.effect) {
1718
1682
  .Confusion => Effects.confusion(battle, player, move, options),
1719
1683
  .Conversion => Effects.conversion(battle, player, options),
@@ -1735,8 +1699,7 @@ inline fn onBegin(
1735
1699
  };
1736
1700
  }
1737
1701
 
1738
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1739
- assert(move.effect.onEnd());
1702
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1740
1703
  return switch (move.effect) {
1741
1704
  .Bide => Effects.bide(battle, player, options),
1742
1705
  // zig fmt: off
@@ -1751,14 +1714,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1751
1714
  };
1752
1715
  }
1753
1716
 
1754
- inline fn alwaysHappens(
1717
+ fn alwaysHappens(
1755
1718
  battle: anytype,
1756
1719
  player: Player,
1757
1720
  move: Move.Data,
1758
1721
  residual: *bool,
1759
1722
  options: anytype,
1760
1723
  ) !void {
1761
- assert(move.effect.alwaysHappens());
1762
1724
  return switch (move.effect) {
1763
1725
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1764
1726
  .Explode => Effects.explode(battle, player),
@@ -1770,7 +1732,7 @@ inline fn alwaysHappens(
1770
1732
  };
1771
1733
  }
1772
1734
 
1773
- 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 {
1774
1736
  return switch (move.effect) {
1775
1737
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1776
1738
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1797,7 +1759,7 @@ pub const Effects = struct {
1797
1759
  side.active.volatiles.state = 0;
1798
1760
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1799
1761
 
1800
- try options.log.start(battle.active(player), .Bide);
1762
+ try options.log.start(.{ battle.active(player), .Bide });
1801
1763
  }
1802
1764
 
1803
1765
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1811,11 +1773,11 @@ pub const Effects = struct {
1811
1773
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1812
1774
  if (Status.is(foe_stored.status, .FRZ)) {
1813
1775
  assert(move.type == .Fire);
1814
- try options.log.curestatus(
1776
+ try options.log.curestatus(.{
1815
1777
  battle.active(player.foe()),
1816
1778
  foe_stored.status,
1817
1779
  .Message,
1818
- );
1780
+ });
1819
1781
  foe_stored.status = 0;
1820
1782
  }
1821
1783
  return;
@@ -1829,7 +1791,7 @@ pub const Effects = struct {
1829
1791
  foe_stored.status = Status.init(.BRN);
1830
1792
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1831
1793
 
1832
- 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 });
1833
1795
  }
1834
1796
 
1835
1797
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1839,8 +1801,8 @@ pub const Effects = struct {
1839
1801
  volatiles.Charging = true;
1840
1802
  const move = side.last_selected_move;
1841
1803
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1842
- try options.log.laststill();
1843
- try options.log.prepare(battle.active(player), move);
1804
+ try options.log.laststill(.{});
1805
+ try options.log.prepare(.{ battle.active(player), move });
1844
1806
  }
1845
1807
 
1846
1808
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1851,42 +1813,36 @@ pub const Effects = struct {
1851
1813
  if (!try Rolls.confusionChance(battle, player, options)) return;
1852
1814
  } else {
1853
1815
  if (showdown) {
1854
- if (!try checkHit(battle, player, move, options)) {
1855
- return;
1856
- } else if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1858
- }
1816
+ if (!try checkHit(battle, player, move, options)) return;
1817
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1859
1818
  } else {
1860
- if (sub) {
1861
- return options.log.fail(battle.active(player.foe()), .None);
1862
- } else if (!try checkHit(battle, player, move, options)) {
1863
- return;
1864
- }
1819
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1820
+ if (!try checkHit(battle, player, move, options)) return;
1865
1821
  }
1866
- try options.chance.commit(player, .hit);
1867
1822
  }
1868
1823
 
1869
1824
  if (foe.active.volatiles.Confusion) return;
1870
1825
  foe.active.volatiles.Confusion = true;
1871
1826
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1872
1827
 
1873
- try options.log.start(battle.active(player.foe()), .Confusion);
1828
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1874
1829
  }
1875
1830
 
1876
1831
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1877
1832
  const foe = battle.foe(player);
1878
1833
 
1879
1834
  if (foe.active.volatiles.Invulnerable) {
1880
- try options.log.lastmiss();
1881
- return options.log.miss(battle.active(player));
1835
+ try options.log.lastmiss(.{});
1836
+ return options.log.miss(.{battle.active(player)});
1882
1837
  }
1883
1838
 
1884
1839
  battle.side(player).active.types = foe.active.types;
1885
- return options.log.typechange(
1840
+ return options.log.start(.{
1886
1841
  battle.active(player),
1842
+ .TypeChange,
1887
1843
  foe.active.types,
1888
1844
  battle.active(player.foe()),
1889
- );
1845
+ });
1890
1846
  }
1891
1847
 
1892
1848
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1895,13 +1851,10 @@ pub const Effects = struct {
1895
1851
  const foe_ident = battle.active(player.foe());
1896
1852
 
1897
1853
  // Pokémon Showdown handles hit/miss earlier in doMove
1898
- if (!showdown) {
1899
- if (!try checkHit(battle, player, move, options)) return null;
1900
- try options.chance.commit(player, .hit);
1901
- }
1854
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1902
1855
 
1903
- if (volatiles.disabled_move != 0) {
1904
- try options.log.fail(foe_ident, .None);
1856
+ if (volatiles.disable_move != 0) {
1857
+ try options.log.fail(.{ foe_ident, .None });
1905
1858
  return null;
1906
1859
  }
1907
1860
 
@@ -1917,7 +1870,7 @@ pub const Effects = struct {
1917
1870
  // Technically this is still considered simply a "miss" on the cartridge,
1918
1871
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1919
1872
  if (n == 0) {
1920
- try options.log.fail(foe_ident, .None);
1873
+ try options.log.fail(.{ foe_ident, .None });
1921
1874
  return null;
1922
1875
  } else if (err) {
1923
1876
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1925,18 +1878,18 @@ pub const Effects = struct {
1925
1878
  return Result.Error;
1926
1879
  }
1927
1880
 
1928
- volatiles.disabled_move =
1881
+ volatiles.disable_move =
1929
1882
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1930
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1883
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1931
1884
 
1932
- const id = foe.active.move(volatiles.disabled_move).id;
1933
- 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 });
1934
1887
  return null;
1935
1888
  }
1936
1889
 
1937
1890
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1938
- var side = battle.side(player);
1939
- var stored = side.stored();
1891
+ var stored = battle.side(player).stored();
1892
+ const ident = battle.active(player);
1940
1893
 
1941
1894
  if (battle.last_damage == 0) {
1942
1895
  assert(showdown);
@@ -1949,7 +1902,7 @@ pub const Effects = struct {
1949
1902
  if (stored.hp == stored.stats.hp) return;
1950
1903
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1951
1904
 
1952
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1905
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1953
1906
  }
1954
1907
 
1955
1908
  fn explode(battle: anytype, player: Player) !void {
@@ -1981,7 +1934,7 @@ pub const Effects = struct {
1981
1934
  if (side.active.volatiles.FocusEnergy) return;
1982
1935
  side.active.volatiles.FocusEnergy = true;
1983
1936
 
1984
- try options.log.start(battle.active(player), .FocusEnergy);
1937
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1985
1938
  }
1986
1939
 
1987
1940
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2003,7 +1956,7 @@ pub const Effects = struct {
2003
1956
  foe_stored.status = Status.init(.FRZ);
2004
1957
  // GLITCH: Hyper Beam recharging status is not cleared
2005
1958
 
2006
- try options.log.status(foe_ident, foe_stored.status, .None);
1959
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2007
1960
  }
2008
1961
 
2009
1962
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2011,7 +1964,7 @@ pub const Effects = struct {
2011
1964
  var side = battle.side(player);
2012
1965
  var foe = battle.foe(player);
2013
1966
 
2014
- var side_stored = side.stored();
1967
+ const side_stored = side.stored();
2015
1968
  var foe_stored = foe.stored();
2016
1969
 
2017
1970
  const player_ident = battle.active(player);
@@ -2023,8 +1976,8 @@ pub const Effects = struct {
2023
1976
  side.active.stats = side_stored.stats;
2024
1977
  foe.active.stats = foe_stored.stats;
2025
1978
 
2026
- try log.activate(player_ident, .Haze);
2027
- try log.clearallboost();
1979
+ try log.activate(.{ player_ident, .Haze });
1980
+ try log.clearallboost(.{});
2028
1981
 
2029
1982
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2030
1983
  if (showdown) {
@@ -2032,11 +1985,11 @@ pub const Effects = struct {
2032
1985
  const p: Player = @enumFromInt(i);
2033
1986
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2034
1987
  if (p != player and Status.any(s.stored().status)) {
2035
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
1988
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2036
1989
  s.stored().status = 0;
2037
1990
  } else if (showdown and s.stored().status == Status.TOX) {
2038
1991
  s.stored().status = Status.init(.PSN);
2039
- try log.status(battle.active(p), s.stored().status, .None);
1992
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2040
1993
  }
2041
1994
  try clearVolatiles(battle, p, p != player, options);
2042
1995
  }
@@ -2045,7 +1998,7 @@ pub const Effects = struct {
2045
1998
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2046
1999
  foe.last_selected_move = .SKIP_TURN;
2047
2000
  }
2048
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2001
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2049
2002
  foe_stored.status = 0;
2050
2003
  }
2051
2004
  try clearVolatiles(battle, player, false, options);
@@ -2061,7 +2014,7 @@ pub const Effects = struct {
2061
2014
  // GLITCH: HP recovery move failure glitches
2062
2015
  const delta = stored.stats.hp - stored.hp;
2063
2016
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2064
- return try options.log.fail(ident, .None);
2017
+ return options.log.fail(.{ ident, .None });
2065
2018
  }
2066
2019
 
2067
2020
  const rest = side.last_selected_move == .Rest;
@@ -2069,17 +2022,17 @@ pub const Effects = struct {
2069
2022
  // Adding the sleep status runs the sleep condition handler to roll duration
2070
2023
  if (showdown) battle.rng.advance(1);
2071
2024
  stored.status = Status.slf(2);
2072
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2025
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2073
2026
  stored.hp = stored.stats.hp;
2074
2027
  } else {
2075
2028
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2076
2029
  }
2077
- 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 });
2078
2031
  }
2079
2032
 
2080
2033
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2081
2034
  battle.side(player).active.volatiles.Recharging = true;
2082
- try options.log.mustrecharge(battle.active(player));
2035
+ try options.log.mustrecharge(.{battle.active(player)});
2083
2036
  }
2084
2037
 
2085
2038
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2088,33 +2041,31 @@ pub const Effects = struct {
2088
2041
  if (showdown) {
2089
2042
  // Invulnerability trumps type immunity on Pokémon Showdown
2090
2043
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2091
- return options.log.immune(battle.active(player.foe()), .None);
2044
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2092
2045
  }
2093
2046
  if (!try checkHit(battle, player, move, options)) return;
2094
2047
  if (foe.active.volatiles.LeechSeed) return;
2095
2048
  } else {
2096
2049
  if (!try checkHit(battle, player, move, options)) return;
2097
2050
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2098
- try options.log.lastmiss();
2099
- return options.log.miss(battle.active(player));
2051
+ try options.log.lastmiss(.{});
2052
+ return options.log.miss(.{battle.active(player)});
2100
2053
  }
2101
2054
  }
2102
2055
 
2103
- try options.chance.commit(player, .hit);
2104
2056
  foe.active.volatiles.LeechSeed = true;
2105
2057
 
2106
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2058
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2107
2059
  }
2108
2060
 
2109
2061
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2110
2062
  var side = battle.side(player);
2063
+ const ident = battle.active(player);
2111
2064
 
2112
- if (side.active.volatiles.LightScreen) {
2113
- return options.log.fail(battle.active(player), .None);
2114
- }
2065
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2115
2066
  side.active.volatiles.LightScreen = true;
2116
2067
 
2117
- try options.log.start(battle.active(player), .LightScreen);
2068
+ try options.log.start(.{ ident, .LightScreen });
2118
2069
  }
2119
2070
 
2120
2071
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2142,17 +2093,15 @@ pub const Effects = struct {
2142
2093
  if (!has_mimic) {
2143
2094
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2144
2095
  if (!try checkHit(battle, player, move, options)) return;
2145
- try options.chance.commit(player, .hit);
2146
- return try options.log.fail(battle.active(player.foe()), .None);
2096
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2147
2097
  }
2148
2098
  }
2149
2099
  if (!try checkHit(battle, player, move, options)) return;
2150
- try options.chance.commit(player, .hit);
2151
2100
 
2152
2101
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2153
2102
  side.active.move(oslot).id = foe.active.move(rslot).id;
2154
2103
 
2155
- 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 });
2156
2105
  }
2157
2106
 
2158
2107
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2161,7 +2110,7 @@ pub const Effects = struct {
2161
2110
  if (side.active.volatiles.Mist) return;
2162
2111
  side.active.volatiles.Mist = true;
2163
2112
 
2164
- try options.log.start(battle.active(player), .Mist);
2113
+ try options.log.start(.{ battle.active(player), .Mist });
2165
2114
  }
2166
2115
 
2167
2116
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2187,25 +2136,25 @@ pub const Effects = struct {
2187
2136
 
2188
2137
  if (showdown) {
2189
2138
  // Invulnerability trumps type immunity on Pokémon Showdown
2190
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2191
- if (!try checkHit(battle, player, move, options)) return;
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;
2192
2142
  }
2193
2143
  if (Status.any(foe_stored.status)) {
2194
- return log.fail(
2144
+ return log.fail(.{
2195
2145
  foe_ident,
2196
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2197
- );
2146
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2147
+ });
2198
2148
  }
2199
2149
  if (!showdown) {
2200
- if (immune) return log.immune(foe_ident, .None);
2150
+ if (immune) return log.immune(.{ foe_ident, .None });
2201
2151
  if (!try checkHit(battle, player, move, options)) return;
2202
2152
  }
2203
- try options.chance.commit(player, .hit);
2204
2153
 
2205
2154
  foe_stored.status = Status.init(.PAR);
2206
2155
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2207
2156
 
2208
- try log.status(foe_ident, foe_stored.status, .None);
2157
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2209
2158
  }
2210
2159
 
2211
2160
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2227,12 +2176,12 @@ pub const Effects = struct {
2227
2176
  foe_stored.status = Status.init(.PAR);
2228
2177
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2229
2178
 
2230
- 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 });
2231
2180
  }
2232
2181
 
2233
2182
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2234
2183
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2235
- try options.log.fieldactivate();
2184
+ try options.log.fieldactivate(.{});
2236
2185
  }
2237
2186
  }
2238
2187
 
@@ -2247,28 +2196,27 @@ pub const Effects = struct {
2247
2196
  return;
2248
2197
  } else if (foe.active.volatiles.Substitute) {
2249
2198
  if (move.effect != .Poison) return;
2250
- return log.fail(foe_ident, .None);
2199
+ return log.fail(.{ foe_ident, .None });
2251
2200
  } else if (Status.any(foe_stored.status)) {
2252
2201
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2253
2202
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2254
2203
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2255
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2204
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2256
2205
  if (!showdown)
2257
- .Poison
2206
+ Fail.Poison
2258
2207
  else if (toxic == (foe_stored.status == Status.TOX))
2259
- if (toxic) .Toxic else .Poison
2208
+ if (toxic) Fail.Toxic else Fail.Poison
2260
2209
  else
2261
- .None
2210
+ Fail.None
2262
2211
  else
2263
- .None);
2212
+ Fail.None });
2264
2213
  } else if (foe.active.types.includes(.Poison)) {
2265
2214
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2266
- return log.immune(foe_ident, .None);
2215
+ return log.immune(.{ foe_ident, .None });
2267
2216
  }
2268
2217
 
2269
2218
  if (move.effect == .Poison) {
2270
2219
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2271
- try options.chance.commit(player, .hit);
2272
2220
  } else {
2273
2221
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2274
2222
  return;
@@ -2282,7 +2230,7 @@ pub const Effects = struct {
2282
2230
  foe.active.volatiles.toxic = 0;
2283
2231
  }
2284
2232
 
2285
- try log.status(foe_ident, foe_stored.status, .None);
2233
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2286
2234
  }
2287
2235
 
2288
2236
  fn rage(battle: anytype, player: Player) !void {
@@ -2302,22 +2250,23 @@ pub const Effects = struct {
2302
2250
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2303
2251
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2304
2252
 
2305
- try options.log.damageOf(
2253
+ try options.log.damage(.{
2306
2254
  battle.active(player),
2307
2255
  stored,
2308
2256
  .RecoilOf,
2309
2257
  battle.active(player.foe()),
2310
- );
2258
+ });
2311
2259
  if (showdown and stored.hp == 0) residual.* = false;
2312
2260
  }
2313
2261
 
2314
2262
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2315
2263
  var side = battle.side(player);
2264
+ const ident = battle.active(player);
2316
2265
 
2317
- 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 });
2318
2267
  side.active.volatiles.Reflect = true;
2319
2268
 
2320
- try options.log.start(battle.active(player), .Reflect);
2269
+ try options.log.start(.{ ident, .Reflect });
2321
2270
  }
2322
2271
 
2323
2272
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2331,14 +2280,13 @@ pub const Effects = struct {
2331
2280
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2332
2281
  } else {
2333
2282
  if (Status.any(foe_stored.status)) {
2334
- return options.log.fail(
2283
+ return options.log.fail(.{
2335
2284
  foe_ident,
2336
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2337
- );
2285
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2286
+ });
2338
2287
  }
2339
- // 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
2340
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2341
- try options.chance.commit(player, .hit);
2342
2290
  }
2343
2291
 
2344
2292
  // Sleep Clause Mod
@@ -2351,18 +2299,17 @@ pub const Effects = struct {
2351
2299
 
2352
2300
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2353
2301
  const last = battle.side(player).last_selected_move;
2354
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2302
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2355
2303
  }
2356
2304
 
2357
2305
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2358
- try options.log.activate(battle.active(player), .Splash);
2306
+ try options.log.activate(.{ battle.active(player), .Splash });
2359
2307
  }
2360
2308
 
2361
2309
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2362
2310
  var side = battle.side(player);
2363
2311
  if (side.active.volatiles.Substitute) {
2364
- try options.log.fail(battle.active(player), .Substitute);
2365
- return;
2312
+ return options.log.fail(.{ battle.active(player), .Substitute });
2366
2313
  }
2367
2314
 
2368
2315
  assert(side.stored().stats.hp <= 1023);
@@ -2374,17 +2321,16 @@ pub const Effects = struct {
2374
2321
  // avoid having to convert to floating point)
2375
2322
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2376
2323
  if (side.stored().hp < required_hp) {
2377
- try options.log.fail(battle.active(player), .Weak);
2378
- return;
2324
+ return options.log.fail(.{ battle.active(player), .Weak });
2379
2325
  }
2380
2326
 
2381
2327
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2382
2328
  side.stored().hp -= hp;
2383
2329
  side.active.volatiles.substitute = hp + 1;
2384
2330
  side.active.volatiles.Substitute = true;
2385
- try options.log.start(battle.active(player), .Substitute);
2331
+ try options.log.start(.{ battle.active(player), .Substitute });
2386
2332
  if (hp > 0) {
2387
- try options.log.damage(battle.active(player), side.stored(), .None);
2333
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2388
2334
  if (showdown and side.stored().hp == 0) residual.* = false;
2389
2335
  }
2390
2336
  }
@@ -2393,13 +2339,11 @@ pub const Effects = struct {
2393
2339
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2394
2340
  if (showdown) {
2395
2341
  if (battle.side(player).last_selected_move == .Teleport) return;
2396
- if (try checkHit(battle, player, move, options)) {
2397
- try options.chance.commit(player, .hit);
2398
- }
2342
+ _ = try checkHit(battle, player, move, options);
2399
2343
  battle.last_damage = 0;
2400
2344
  } else {
2401
- try options.log.fail(battle.active(player), .None);
2402
- try options.log.laststill();
2345
+ try options.log.fail(.{ battle.active(player), .None });
2346
+ try options.log.laststill(.{});
2403
2347
  }
2404
2348
  }
2405
2349
 
@@ -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
  }