@pkmn/engine 0.1.0-dev.c5fe6d07 → 0.1.0-dev.cb3978dd

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 (80) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -19
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +44 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +97 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +21 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +22 -0
  13. package/build/pkg/addon.js +35 -58
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/ids.json +12 -12
  16. package/build/pkg/data/layout.json +17 -4
  17. package/build/pkg/data/protocol.json +84 -13
  18. package/build/pkg/data.d.ts +27 -5
  19. package/build/pkg/data.js.map +1 -1
  20. package/build/pkg/gen1.d.ts +10 -13
  21. package/build/pkg/gen1.js +3 -3
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +55 -13
  24. package/build/pkg/index.js +15 -10
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +20 -1
  27. package/build/pkg/protocol.js +123 -102
  28. package/build/pkg/protocol.js.map +1 -1
  29. package/build/tools/debug.js +36 -68
  30. package/build/tools/debug.js.map +1 -1
  31. package/build/tools/display/debug.jsx +141 -0
  32. package/build/tools/display/debug.jsx.map +1 -0
  33. package/build/tools/display/index.js +141 -0
  34. package/build/tools/display/index.js.map +1 -0
  35. package/build/tools/display/ui.jsx +349 -0
  36. package/build/tools/display/ui.jsx.map +1 -0
  37. package/build/tools/display/util.js +18 -0
  38. package/build/tools/display/util.js.map +1 -0
  39. package/build.zig +277 -107
  40. package/package.json +72 -32
  41. package/src/bin/install-pkmn-engine +29 -19
  42. package/src/bin/pkmn-debug +1 -1
  43. package/src/lib/common/array.zig +77 -0
  44. package/src/lib/common/data.zig +12 -24
  45. package/src/lib/common/debug.zig +39 -8
  46. package/src/lib/common/js.zig +4 -2
  47. package/src/lib/common/optional.zig +21 -10
  48. package/src/lib/common/options.zig +6 -0
  49. package/src/lib/common/protocol.zig +808 -332
  50. package/src/lib/common/rational.zig +132 -36
  51. package/src/lib/common/rng.zig +22 -15
  52. package/src/lib/common/util.zig +50 -1
  53. package/src/lib/gen1/calc.zig +688 -162
  54. package/src/lib/gen1/chance.zig +436 -295
  55. package/src/lib/gen1/data/moves.zig +193 -183
  56. package/src/lib/gen1/data/species.zig +4 -6
  57. package/src/lib/gen1/data/types.zig +8 -9
  58. package/src/lib/gen1/data.zig +55 -70
  59. package/src/lib/gen1/helpers.zig +91 -562
  60. package/src/lib/gen1/mechanics.zig +653 -578
  61. package/src/lib/gen2/calc.zig +153 -0
  62. package/src/lib/gen2/chance.zig +902 -0
  63. package/src/lib/gen2/data/items.zig +13 -21
  64. package/src/lib/gen2/data/moves.zig +664 -572
  65. package/src/lib/gen2/data/species.zig +2 -4
  66. package/src/lib/gen2/data/types.zig +135 -33
  67. package/src/lib/gen2/data.zig +115 -91
  68. package/src/lib/gen2/generated.zig +801 -0
  69. package/src/lib/gen2/helpers.zig +54 -37
  70. package/src/lib/gen2/mechanics.zig +3569 -29
  71. package/src/lib/{binding/node.zig → node.zig} +9 -14
  72. package/src/lib/pkmn.zig +36 -15
  73. package/src/lib/{binding/wasm.zig → wasm.zig} +1 -1
  74. package/src/tools/display/dom.ts +96 -0
  75. package/src/tools/display/ui.css +354 -0
  76. package/build/test/display.js +0 -400
  77. package/build/test/display.js.map +0 -1
  78. package/src/lib/gen1/README.md +0 -892
  79. package/src/lib/gen2/README.md +0 -308
  80. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,38 +1,28 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
5
+ const data = @import("data.zig");
6
+ const pkmn = @import("../pkmn.zig");
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
-
10
- const Action = @import("chance.zig").Action;
11
-
12
- const data = @import("data.zig");
9
+ const std = @import("std");
13
10
 
14
11
  const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
- const showdown = pkmn.options.showdown;
24
-
25
12
  const Boost = protocol.Boost;
13
+ const Choice = common.Choice;
26
14
  const Damage = protocol.Damage;
27
- const Heal = protocol.Heal;
28
-
29
- const Gen12 = rng.Gen12;
30
-
31
- const ActivePokemon = data.ActivePokemon;
32
15
  const Effectiveness = data.Effectiveness;
16
+ const expectEqual = std.testing.expectEqual;
17
+ const Fail = protocol.Fail;
18
+ const Gen12 = rng.Gen12;
19
+ const Heal = protocol.Heal;
20
+ const ID = common.ID;
33
21
  const Move = data.Move;
34
22
  const MoveSlot = data.MoveSlot;
35
- const Pokemon = data.Pokemon;
23
+ const Player = common.Player;
24
+ const Result = common.Result;
25
+ const showdown = pkmn.options.showdown;
36
26
  const Side = data.Side;
37
27
  const Species = data.Species;
38
28
  const Stats = data.Stats;
@@ -59,34 +49,26 @@ const BOOSTS = &[_][2]u8{
59
49
 
60
50
  const MAX_STAT_VALUE = 999;
61
51
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
52
  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
53
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
54
  if (battle.turn == 0) return start(battle, options);
71
55
 
72
56
  var s1 = false;
73
57
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
58
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
59
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
60
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
61
 
80
62
  var p1 = battle.side(.P1);
81
63
  var p2 = battle.side(.P2);
82
64
 
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;
65
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
66
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
67
 
86
68
  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;
69
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
70
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
71
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
72
  }
91
73
 
92
74
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -99,11 +81,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
81
  const p1 = battle.side(.P1);
100
82
  const p2 = battle.side(.P2);
101
83
 
102
- var p1_slot = findFirstAlive(p1);
84
+ const p1_slot = findFirstAlive(p1);
103
85
  assert(!showdown or p1_slot == 1);
104
86
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
87
 
106
- var p2_slot = findFirstAlive(p2);
88
+ const p2_slot = findFirstAlive(p2);
107
89
  assert(!showdown or p2_slot == 1);
108
90
  if (p2_slot == 0) return Result.Win;
109
91
 
@@ -124,7 +106,6 @@ fn selectMove(
124
106
  choice: Choice,
125
107
  foe_choice: Choice,
126
108
  skip_turn: *bool,
127
- from: *?Move,
128
109
  ) ?Result {
129
110
  if (choice.type == .Pass) return null;
130
111
 
@@ -141,7 +122,6 @@ fn selectMove(
141
122
  return null;
142
123
  }
143
124
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
125
  if (showdown) {
146
126
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
127
  saveMove(battle, player, null);
@@ -151,7 +131,6 @@ fn selectMove(
151
131
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
132
  if (!showdown) volatiles.Flinch = false;
153
133
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
134
  if (showdown) {
156
135
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
136
  saveMove(battle, player, null);
@@ -172,15 +151,13 @@ fn selectMove(
172
151
  return null;
173
152
  }
174
153
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
154
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
155
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
156
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
157
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
181
158
  // set the selected move will not actually be used, it will just be reported as having
182
159
  // been used (this differs from how Pokémon Showdown works, but its impossible to
183
- // replicate the incorrect behavior with the correct mechanisms).
160
+ // replicate the incorrect behavior with the correct mechanisms)
184
161
  saveMove(battle, player, choice);
185
162
  } else {
186
163
  assert(choice.data == 0);
@@ -188,7 +165,10 @@ fn selectMove(
188
165
  if (foe_choice.type == .Switch) {
189
166
  const last = side.active.move(battle.lastMove(player).index);
190
167
  if (last.id == .Metronome) side.last_selected_move = last.id;
191
- if (last.id == .MirrorMove) return Result.Error;
168
+ if (last.id == .MirrorMove) {
169
+ if (!pkmn.options.mod) return Result.Error;
170
+ side.last_selected_move = last.id;
171
+ }
192
172
  }
193
173
  }
194
174
  return null;
@@ -210,16 +190,13 @@ fn selectMove(
210
190
  if (choice.data == 0) {
211
191
  const struggle = ok: {
212
192
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
193
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
194
  }
215
195
  break :ok true;
216
196
  };
217
-
218
197
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
198
  }
199
+ saveMove(battle, player, choice);
223
200
 
224
201
  return null;
225
202
  }
@@ -232,7 +209,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
209
  if (c.data == 0) {
233
210
  side.last_selected_move = .Struggle;
234
211
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
212
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
213
  const move = side.active.move(c.data);
237
214
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
215
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,17 +247,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
247
  active.moves = incoming.moves;
271
248
 
272
249
  statusModify(incoming.status, &active.stats);
250
+ options.chance.switched(player, slot);
273
251
 
274
252
  foe.active.volatiles.Binding = false;
275
253
 
276
- try options.log.switched(battle.active(player), incoming);
277
- options.chance.switched(player, side.order[0], out);
254
+ try options.log.switched(.{ battle.active(player), incoming });
278
255
 
279
256
  if (showdown and incoming.status == Status.TOX) {
280
257
  incoming.status = Status.init(.PSN);
281
258
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
259
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
260
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
261
  }
285
262
  }
286
263
 
@@ -343,12 +320,10 @@ fn doTurn(
343
320
  player_choice: Choice,
344
321
  player_rewrap: bool,
345
322
  player_skip: bool,
346
- player_from: ?Move,
347
323
  foe_player: Player,
348
324
  foe_choice: Choice,
349
325
  foe_rewrap: bool,
350
326
  foe_skip: bool,
351
- foe_from: ?Move,
352
327
  options: anytype,
353
328
  ) !?Result {
354
329
  assert(player_choice.type != .Pass);
@@ -361,19 +336,18 @@ fn doTurn(
361
336
  player_choice,
362
337
  player_rewrap,
363
338
  player_skip,
364
- player_from,
365
339
  &residual,
366
340
  options,
367
341
  )) |r| return r;
368
342
  if (!replace) {
369
343
  if (player_choice.type != .Switch) {
370
- if (try checkFaint(battle, foe_player, options)) |r| return r;
344
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
371
345
  }
372
346
  if (residual) try handleResidual(battle, player, options);
373
- if (try checkFaint(battle, player, options)) |r| return r;
347
+ if (try checkFaint(battle, player, false, options)) |r| return r;
374
348
  } else if (foe_choice.type == .Pass) return null;
375
349
 
376
- options.chance.clearPending();
350
+ if (!showdown) options.chance.clearPending();
377
351
 
378
352
  residual = true;
379
353
  replace = battle.side(foe_player).stored().hp == 0;
@@ -384,16 +358,15 @@ fn doTurn(
384
358
  foe_choice,
385
359
  foe_rewrap,
386
360
  foe_skip,
387
- foe_from,
388
361
  &residual,
389
362
  options,
390
363
  )) |r| return r;
391
364
  if (!replace) {
392
365
  if (!calc and foe_choice.type != .Switch) {
393
- if (try checkFaint(battle, player, options)) |r| return r;
366
+ if (try checkFaint(battle, player, true, options)) |r| return r;
394
367
  }
395
368
  if (residual) try handleResidual(battle, foe_player, options);
396
- if (try checkFaint(battle, foe_player, options)) |r| return r;
369
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
397
370
  }
398
371
 
399
372
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -412,7 +385,6 @@ fn executeMove(
412
385
  choice: Choice,
413
386
  rewrap: bool,
414
387
  skip: bool,
415
- from: ?Move,
416
388
  residual: *bool,
417
389
  options: anytype,
418
390
  ) !?Result {
@@ -429,7 +401,7 @@ fn executeMove(
429
401
  if (side.last_selected_move == .SKIP_TURN) {
430
402
  assert(!showdown);
431
403
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
404
+ try options.log.cant(.{ battle.active(player), .Bound });
433
405
  }
434
406
  return null;
435
407
  }
@@ -447,11 +419,13 @@ fn executeMove(
447
419
  mslot = @intCast(battle.lastMove(player).index);
448
420
  const stored = side.stored();
449
421
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
450
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
451
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
422
+ auto = pkmn.options.mod or isForced(&side.active) or
423
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
424
+ side.last_selected_move == .HyperBeam or
425
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
452
426
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
427
  // thawed, in which case we will desync unless the last_selected_move happened to be at
454
- // index 1 and the current Pokémon has the same move in its first slot.
428
+ // index 1 and the current Pokémon has the same move in its first slot
455
429
  if (!auto) {
456
430
  // side.active.moves(slot) is safe to check even though the slot in question might not
457
431
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -470,7 +444,7 @@ fn executeMove(
470
444
 
471
445
  var skip_can = false;
472
446
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
447
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
448
  .done => return null,
475
449
  .skip_can => skip_can = true,
476
450
  .skip_pp => skip_pp = true,
@@ -478,10 +452,29 @@ fn executeMove(
478
452
  .err => return @as(?Result, Result.Error),
479
453
  }
480
454
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
455
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
456
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
457
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
458
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
459
+ side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
460
+ {
461
+ assert(mslot == 1);
462
+ const has_beam = has_beam: {
463
+ for (side.active.moves, 0..) |m, i| {
464
+ if (m.id == .HyperBeam) {
465
+ mslot = @intCast(i + 1);
466
+ break :has_beam true;
467
+ }
468
+ }
469
+ break :has_beam false;
470
+ };
471
+ if (!has_beam) skip_pp = true;
483
472
  }
484
473
 
474
+ const can = skip_can or
475
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
476
+ if (!can) return null;
477
+
485
478
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
479
  }
487
480
 
@@ -491,7 +484,6 @@ fn beforeMove(
491
484
  battle: anytype,
492
485
  player: Player,
493
486
  skip: bool,
494
- from: ?Move,
495
487
  residual: *bool,
496
488
  options: anytype,
497
489
  ) !BeforeMove {
@@ -505,36 +497,41 @@ fn beforeMove(
505
497
 
506
498
  if (Status.is(stored.status, .SLP)) {
507
499
  const before = stored.status;
500
+ const slf = Status.is(stored.status, .EXT);
508
501
  // Even if the EXT bit is set this will still correctly modify the sleep duration
509
- if (options.calc.modify(player, .sleep)) |extend| {
510
- if (!extend) stored.status = 0;
502
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
503
+ .started, .ended => stored.status = 0,
504
+ .continuing => if (Status.duration(stored.status) > 1) {
505
+ stored.status -= 1;
506
+ },
507
+ else => unreachable,
511
508
  } else {
512
509
  stored.status -= 1;
513
510
  }
514
511
 
515
512
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
513
+ try options.chance.sleep(player, if (slf)
514
+ .None
515
+ else if (duration == 0) .ended else .continuing);
519
516
 
520
517
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
518
+ try log.curestatus(.{ ident, before, .Message });
522
519
  stored.status = 0; // clears EXT if present
523
520
  } else {
524
- try log.cant(ident, .Sleep);
521
+ try log.cant(.{ ident, .Sleep });
525
522
  }
526
523
  side.last_used_move = .None;
527
524
  return .done;
528
525
  }
529
526
 
530
527
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
528
+ try log.cant(.{ ident, .Freeze });
532
529
  side.last_used_move = .None;
533
530
  return .done;
534
531
  }
535
532
 
536
533
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
534
+ try log.cant(.{ ident, .Bound });
538
535
  return .done;
539
536
  }
540
537
 
@@ -542,27 +539,27 @@ fn beforeMove(
542
539
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
540
  // Pokémon can sometimes flinch multiple times from the same original hit
544
541
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
542
+ try log.cant(.{ ident, .Flinch });
546
543
  return .done;
547
544
  }
548
545
 
549
546
  if (volatiles.Recharging) {
550
547
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
548
+ try log.cant(.{ ident, .Recharge });
552
549
  return .done;
553
550
  }
554
551
 
555
- if (volatiles.disabled_duration > 0) {
556
- if (options.calc.modify(player, .disable)) |extend| {
557
- if (!extend) volatiles.disabled_duration = 0;
558
- } else {
559
- volatiles.disabled_duration -= 1;
560
- }
561
- try options.chance.disable(player, volatiles.disabled_duration);
552
+ if (volatiles.disable_duration > 0) {
553
+ volatiles.disable_duration =
554
+ decrement(.disable, player, options, volatiles.disable_duration);
555
+ try options.chance.disable(
556
+ player,
557
+ if (volatiles.disable_duration == 0) .ended else .continuing,
558
+ );
562
559
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
560
+ if (volatiles.disable_duration == 0) {
561
+ volatiles.disable_move = 0;
562
+ try log.end(.{ ident, .Disable });
566
563
  }
567
564
  }
568
565
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -571,25 +568,30 @@ fn beforeMove(
571
568
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
572
569
  // before the side had a selected a move - we simply need to assume this leads to a desync
573
570
  if (side.last_selected_move == .None) {
574
- assert(!showdown);
571
+ assert(!pkmn.options.mod);
575
572
  return .err;
576
573
  }
577
574
 
578
575
  if (volatiles.Confusion) {
579
576
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- if (!extend) volatiles.confusion = 0;
577
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
578
+ .started, .ended => volatiles.confusion = 0,
579
+ .continuing => if (volatiles.confusion > 1) {
580
+ volatiles.confusion -= 1;
581
+ },
582
+ .overwritten => {},
583
+ else => unreachable,
583
584
  } else {
584
585
  volatiles.confusion -= 1;
585
586
  }
586
- try options.chance.confusion(player, volatiles.confusion);
587
+
588
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
589
 
588
590
  if (volatiles.confusion == 0) {
589
591
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
592
+ try log.end(.{ ident, .Confusion });
591
593
  } else {
592
- try log.activate(ident, .Confusion);
594
+ try log.activate(.{ ident, .Confusion });
593
595
 
594
596
  if (try Rolls.confused(battle, player, options)) {
595
597
  assert(!volatiles.MultiHit);
@@ -601,7 +603,9 @@ fn beforeMove(
601
603
  volatiles.Charging = false;
602
604
  volatiles.Binding = false;
603
605
  volatiles.Invulnerable = false;
604
- options.chance.clearDurations(player, null);
606
+
607
+ options.chance.observe(.attacking, player, .None);
608
+ options.chance.observe(.binding, player, .None);
605
609
  {
606
610
  // This feels (and is) disgusting but the cartridge literally just overwrites
607
611
  // the opponent's defense with the user's defense and resets it after. As a
@@ -609,7 +613,9 @@ fn beforeMove(
609
613
  const def = foe.active.stats.def;
610
614
  foe.active.stats.def = active.stats.def;
611
615
  defer foe.active.stats.def = def;
612
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
616
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
617
+ return .err;
618
+ }
613
619
  }
614
620
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
621
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +644,12 @@ fn beforeMove(
638
644
  volatiles.Thrashing = false;
639
645
  volatiles.Charging = false;
640
646
  volatiles.Binding = false;
641
- options.chance.clearDurations(player, null);
647
+
648
+ options.chance.observe(.attacking, player, .None);
649
+ options.chance.observe(.binding, player, .None);
650
+
642
651
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
652
+ try log.cant(.{ ident, .Paralysis });
644
653
  return .done;
645
654
  }
646
655
 
@@ -656,28 +665,22 @@ fn beforeMove(
656
665
  volatiles.state +%= battle.last_damage;
657
666
  }
658
667
 
659
- assert(volatiles.attacks > 0);
660
-
661
- if (options.calc.modify(player, .attacking)) |extend| {
662
- if (!extend) volatiles.attacks = 0;
663
- } else {
664
- volatiles.attacks -= 1;
665
- }
666
- try options.chance.attacking(player, volatiles.attacks);
668
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
669
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
670
 
668
671
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
672
+ try log.activate(.{ ident, .Bide });
670
673
  return .done;
671
674
  }
672
675
 
673
676
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
677
+ try log.end(.{ ident, .Bide });
675
678
 
676
679
  battle.last_damage = volatiles.state *% 2;
677
680
  volatiles.state = 0;
678
681
 
679
682
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
683
+ try log.fail(.{ ident, .None });
681
684
  return .done;
682
685
  }
683
686
 
@@ -697,47 +700,42 @@ fn beforeMove(
697
700
  }
698
701
 
699
702
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
703
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
704
 
703
- if (options.calc.modify(player, .attacking)) |extend| {
704
- if (!extend) volatiles.attacks = 0;
705
- } else {
706
- volatiles.attacks -= 1;
707
- }
708
- try options.chance.attacking(player, volatiles.attacks);
705
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
706
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
707
 
710
- if (!showdown and handleThrashing(battle, active, player, options)) {
711
- try log.start(battle.active(player), .ConfusionSilent);
712
- }
713
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
714
- if (showdown) {
715
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
716
- // used in useMove and doesn't have the notion of skipping canMove semantics
717
- side.last_used_move = side.last_selected_move;
718
- if (handleThrashing(battle, active, player, options)) {
719
- try log.start(battle.active(player), .ConfusionSilent);
720
- }
708
+ if (volatiles.attacks == 0) {
709
+ const overwritten = volatiles.Confusion;
710
+
711
+ volatiles.Thrashing = false;
712
+ volatiles.Confusion = true;
713
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
714
+
715
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
716
+ // ended (the engine's protocol is always from the omniscient perspective and clients
717
+ // can be expected to recognize this pattern and pass the information only to the
718
+ // correct player)
719
+ try log.start(.{ battle.active(player), .ConfusionSilent });
720
+ options.chance.observe(.confusion, player, if (overwritten)
721
+ if (pkmn.options.overwrite) .overwritten else .continuing
722
+ else
723
+ .started);
724
+ if (!overwritten) options.calc.confusion(player);
721
725
  }
726
+
727
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
728
+ // used in useMove and doesn't have the notion of skipping canMove semantics
729
+ if (showdown) side.last_used_move = side.last_selected_move;
730
+
722
731
  return .skip_can;
723
732
  }
724
733
 
725
734
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
735
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
736
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
727
737
 
728
- if (options.calc.modify(player, .binding)) |extend| {
729
- if (!extend) volatiles.attacks = 0;
730
- } else {
731
- volatiles.attacks -= 1;
732
- }
733
- try options.chance.binding(player, volatiles.attacks);
734
-
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
738
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
739
  if (showdown or battle.last_damage != 0) {
742
740
  const sub = showdown and foe.active.volatiles.Substitute;
743
741
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +755,7 @@ fn canMove(
757
755
  mslot: u4,
758
756
  auto: bool,
759
757
  skip_pp: bool,
760
- from: ?Move,
758
+ from: Move,
761
759
  residual: *bool,
762
760
  options: anytype,
763
761
  ) !bool {
@@ -765,33 +763,21 @@ fn canMove(
765
763
  const player_ident = battle.active(player);
766
764
  const move = Move.get(side.last_selected_move);
767
765
 
768
- const foe_last = battle.foe(player).last_used_move;
769
- const special = from != null and (from.? == .Metronome and
770
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
771
-
772
- var skip = skip_pp;
773
766
  if (side.active.volatiles.Charging) {
774
767
  side.active.volatiles.Charging = false;
775
768
  side.active.volatiles.Invulnerable = false;
776
769
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
770
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
771
  setCounterable(battle, player, side, move);
779
772
  try Effects.charge(battle, player, options);
780
773
  return false;
781
774
  }
782
775
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
776
+ side.last_used_move = side.last_selected_move;
777
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
778
 
786
779
  const target = if (move.target == .Self) player else player.foe();
787
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
788
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
789
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
790
- // meaning sometimes we lose this information. from is still set because it not being null is
791
- // relevant for control flow, but we don't have anything useful to put in the log. This is
792
- // regrettable, but this information is more "nice to have" than required
793
- const f = if (from != null and from.? == .None) null else from;
794
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
780
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
781
  setCounterable(battle, player, side, move);
796
782
 
797
783
  if (move.effect.onBegin()) {
@@ -801,9 +787,14 @@ fn canMove(
801
787
 
802
788
  if (move.effect == .Thrashing) {
803
789
  Effects.thrashing(battle, player, options);
804
- } else if (!showdown and move.effect == .Binding) {
805
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
806
- try Effects.binding(battle, player, false, options);
790
+ } else if (move.effect == .Binding) {
791
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
792
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
793
+ if (showdown) {
794
+ battle.foe(player).active.volatiles.Recharging = false;
795
+ } else {
796
+ try Effects.binding(battle, player, false, options);
797
+ }
807
798
  }
808
799
 
809
800
  return true;
@@ -829,16 +820,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
820
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
821
  if (volatiles.Bide) return;
831
822
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
823
+ var move_slot = active.move(mslot);
824
+ assert(move_slot.pp > 0 or auto);
825
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
826
  if (volatiles.Transform) return;
835
827
 
836
- assert(side.stored().move(mslot).pp > 0 or auto);
837
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
828
+ move_slot = side.stored().move(mslot);
829
+ assert(move_slot.pp > 0 or auto);
830
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
831
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
832
  }
840
833
 
841
- fn incrementPP(side: *Side, mslot: u4) void {
834
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
842
835
  var active = &side.active;
843
836
  const volatiles = &active.volatiles;
844
837
 
@@ -846,8 +839,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
846
839
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
847
840
  if (showdown and volatiles.Transform) return;
848
841
 
842
+ const n = options.calc.overridden(player, .pp) orelse 1;
843
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
844
+
849
845
  assert(mslot > 0 and mslot <= 4);
850
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
846
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
851
847
  }
852
848
 
853
849
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -895,13 +891,13 @@ fn doMove(
895
891
  break :miss true;
896
892
  }
897
893
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
894
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
895
  };
900
896
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
897
 
902
898
  var late = showdown and move.effect != .Explode;
903
899
  const skip = status or immune;
904
- if ((!showdown or (!skip or counter)) and !miss) blk: {
900
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
905
901
  if (showdown and move.effect.isMulti()) {
906
902
  try Effects.multiHit(battle, player, move, options);
907
903
  hits = side.active.volatiles.attacks;
@@ -923,8 +919,9 @@ fn doMove(
923
919
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
924
920
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
921
  battle.last_damage = if (ohko) 65535 else 0;
926
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
927
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
922
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
923
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
924
+ if (!showdown) try options.chance.commit(player, .err);
928
925
  return @as(?Result, Result.Error);
929
926
  }
930
927
  if (battle.last_damage == 0) {
@@ -938,11 +935,12 @@ fn doMove(
938
935
  }
939
936
  }
940
937
 
941
- var zero = battle.last_damage == 0;
942
- miss = if (showdown or skip)
943
- miss
944
- else
945
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
938
+ var missed: bool = undefined;
939
+ const zero = battle.last_damage == 0;
940
+ if (!showdown and !skip) {
941
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
942
+ miss = missed or zero;
943
+ }
946
944
 
947
945
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
946
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +964,37 @@ fn doMove(
966
964
  ohko = (!showdown or (!immune and !invulnerable)) and
967
965
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
966
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
967
+ try log.immune(.{ foe_ident, .OHKO });
970
968
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- try log.immune(foe_ident, .None);
969
+ try log.immune(.{ foe_ident, .None });
970
+ if (!showdown) {
971
+ if (move.effect == .Binding) {
972
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
973
+ // then simply clears it later on if the move happens to miss in moveHit.
974
+ // However, moveHit can return true and we'll still end up in the outer miss
975
+ // handling block because attacking into an immune Pokemon does zero damage
976
+ // (considered missing)
977
+ assert(zero);
978
+ // Thus we still need to actually save the hit result (but not the crit roll).
979
+ // Using .miss for this commit would accomplish what we want, but is somewhat
980
+ // misleading as its entirely possible the move actually did hit (and the target
981
+ // is now trapped) so we use the .binding value instead which is effectively
982
+ // identical, just doesn't claim to know about the actual results of the hit
983
+ // roll)
984
+ try options.chance.commit(player, .binding);
985
+ } else {
986
+ // The .glitch option allows for conditionally committing if we are in a
987
+ // division-by-zero freeze alternative timeline scenario
988
+ try options.chance.commit(player, .glitch);
989
+ }
990
+ }
972
991
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- try log.fail(foe_ident, .None);
992
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
993
+ try log.fail(.{ foe_ident, .None });
975
994
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- try log.miss(battle.active(player));
995
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
996
+ try log.lastmiss(.{});
997
+ try log.miss(.{battle.active(player)});
979
998
  }
980
999
  if (move.effect == .JumpKick) {
981
1000
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +1007,15 @@ fn doMove(
988
1007
  try buildRage(battle, player.foe(), options);
989
1008
  } else if (showdown and move.effect == .Disable) {
990
1009
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
1010
  }
994
1011
  return null;
995
- } else {
1012
+ } else if (!showdown) {
996
1013
  try options.chance.commit(player, .hit);
997
1014
  }
998
1015
 
999
1016
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
1017
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
1018
+ // it up above before damage calculation)
1002
1019
  if (!showdown and move.effect.isMulti()) {
1003
1020
  try Effects.multiHit(battle, player, move, options);
1004
1021
  hits = side.active.volatiles.attacks;
@@ -1011,15 +1028,15 @@ fn doMove(
1011
1028
  var hit: u4 = 0;
1012
1029
  while (hit < hits) {
1013
1030
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1031
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1032
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1033
+ try log.supereffective(.{battle.active(player.foe())});
1017
1034
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1035
+ try log.resisted(.{battle.active(player.foe())});
1019
1036
  }
1020
1037
  }
1021
1038
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1039
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1040
  hit += 1;
1024
1041
  if (foe.stored().hp == 0) break;
1025
1042
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,33 +1053,31 @@ fn doMove(
1036
1053
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1054
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1055
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1040
- } else if (showdown) {
1056
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1057
+ } else if (showdown and move.effect == .Binding) {
1041
1058
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
- if (move.effect == .Binding) {
1043
- try Effects.binding(battle, player, rewrap, options);
1044
- if (immune) {
1045
- battle.last_damage = 0;
1046
- assert(foe.stored().hp > 0);
1047
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
- if (sub) {
1049
- try log.activate(battle.active(player.foe()), .Substitute);
1050
- } else {
1051
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1052
- try buildRage(battle, player.foe(), options);
1053
- }
1054
- return null;
1059
+ try Effects.binding(battle, player, rewrap, options);
1060
+ if (immune) {
1061
+ battle.last_damage = 0;
1062
+ assert(foe.stored().hp > 0);
1063
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1064
+ if (sub) {
1065
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1066
+ } else {
1067
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1068
+ try buildRage(battle, player.foe(), options);
1055
1069
  }
1070
+ return null;
1056
1071
  }
1057
1072
  }
1058
1073
 
1059
1074
  // Substitute being broken nullifies the move's effect completely so even
1060
- // if an effect was intended to "always happen" it will still get skipped.
1075
+ // if an effect was intended to "always happen" it will still get skipped
1061
1076
  if (nullified) return null;
1062
1077
 
1063
1078
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1079
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1065
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1080
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1081
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1082
 
1068
1083
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1092,7 @@ fn doMove(
1077
1092
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1093
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1094
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1095
+ // puts the |-status| message before |-hitcount| instead of after
1081
1096
  if (move.effect == .Twineedle) {
1082
1097
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1098
  } else if (move.effect == .Disable) {
@@ -1096,12 +1111,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1111
  const side = battle.side(player);
1097
1112
 
1098
1113
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1114
+ var rate: u16 = Species.chance(side.stored().species);
1100
1115
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1116
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1117
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1118
 
1104
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1119
+ const spurious =
1120
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1121
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1105
1122
  }
1106
1123
 
1107
1124
  fn calcDamage(
@@ -1110,6 +1127,7 @@ fn calcDamage(
1110
1127
  target_player: Player,
1111
1128
  m: ?Move.Data,
1112
1129
  crit: bool,
1130
+ options: anytype,
1113
1131
  ) bool {
1114
1132
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1133
  const cfz = m == null;
@@ -1121,28 +1139,8 @@ fn calcDamage(
1121
1139
 
1122
1140
  const special = move.type.special();
1123
1141
 
1124
- // zig fmt: off
1125
- var atk: u32 =
1126
- if (crit)
1127
- if (special) side.stored().stats.spc
1128
- else side.stored().stats.atk
1129
- else
1130
- if (special) side.active.stats.spc
1131
- else side.active.stats.atk;
1132
- var def: u32 =
1133
- if (crit)
1134
- if (special) target.stored().stats.spc
1135
- else target.stored().stats.def
1136
- else
1137
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1138
- if (special)
1139
- target.active.stats.spc *
1140
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1141
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1142
- else
1143
- target.active.stats.def *
1144
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1145
- // zig fmt: on
1142
+ var atk = attack(side, crit, special);
1143
+ var def = defense(target, crit, special, cfz);
1146
1144
 
1147
1145
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1146
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1150,12 +1148,12 @@ fn calcDamage(
1150
1148
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1151
1149
  atk = @max((atk / 4) & 255, 1);
1152
1150
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1153
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1151
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1154
1152
  }
1155
1153
 
1156
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1154
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1157
1155
 
1158
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1156
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1159
1157
 
1160
1158
  if (def == 0) return false;
1161
1159
 
@@ -1167,11 +1165,41 @@ fn calcDamage(
1167
1165
  d = @min(997, d);
1168
1166
  d += 2;
1169
1167
 
1168
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1169
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1170
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1171
+ // that results in counter intuitive probabilities in the common case
1172
+ if (pkmn.options.chance and !pkmn.options.mod) {
1173
+ atk = attack(side, !crit, special);
1174
+ def = defense(target, !crit, special, false);
1175
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1176
+ }
1177
+
1170
1178
  battle.last_damage = @intCast(d);
1171
1179
 
1172
1180
  return true;
1173
1181
  }
1174
1182
 
1183
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1186
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1187
+ }
1188
+
1189
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1190
+ return if (crit)
1191
+ if (special) target.stored().stats.spc else target.stored().stats.def
1192
+ else
1193
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1194
+ if (special)
1195
+ target.active.stats.spc *
1196
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1197
+ else
1198
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1199
+ target.active.stats.def *
1200
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1201
+ }
1202
+
1175
1203
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1204
  const side = battle.side(player);
1177
1205
  const foe = battle.foe(player);
@@ -1216,7 +1244,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1244
  const foe = battle.foe(player);
1217
1245
 
1218
1246
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1247
 
1221
1248
  battle.last_damage = switch (side.last_selected_move) {
1222
1249
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1227,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1254
  .Psywave => power: {
1228
1255
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1229
1256
  // GLITCH: Psywave infinite glitch loop
1230
- if (!showdown and max <= 1) return Result.Error;
1257
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1231
1258
  break :power try Rolls.psywave(battle, player, max, options);
1232
1259
  },
1233
1260
  else => unreachable,
1234
1261
  };
1235
1262
 
1236
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1263
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1237
1264
 
1238
1265
  const sub = showdown and foe.active.volatiles.Substitute;
1239
1266
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1246,7 +1273,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1273
  const foe = battle.foe(player);
1247
1274
 
1248
1275
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1276
+ try options.log.fail(.{ battle.active(player), .None });
1250
1277
  return null;
1251
1278
  }
1252
1279
 
@@ -1264,22 +1291,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1291
  foe_last_selected_move.type == .Fighting);
1265
1292
 
1266
1293
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1294
+ try options.log.fail(.{ battle.active(player), .None });
1268
1295
  return null;
1269
1296
  }
1270
1297
 
1271
1298
  if (!used or !selected) {
1272
1299
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
- if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1300
+ if (!pkmn.options.mod) return Result.Error;
1301
+ try options.log.fail(.{ battle.active(player), .None });
1275
1302
  return null;
1276
1303
  }
1277
1304
 
1278
1305
  battle.last_damage *|= 2;
1279
1306
 
1280
- // Pokémon Showdown calls checkHit before Counter
1307
+ // Pokémon Showdown calls moveHit before Counter
1281
1308
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1309
 
1284
1310
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1311
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1333,20 @@ fn applyDamage(
1307
1333
  subbed.active.volatiles.substitute = 0;
1308
1334
  subbed.active.volatiles.Substitute = false;
1309
1335
  // battle.last_damage is not updated with the amount of HP the Substitute had
1310
- try options.log.end(battle.active(sub_player), .Substitute);
1311
- options.calc.capped(target_player);
1336
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1337
+ if (reason != .Confusion) options.calc.capped(sub_player);
1312
1338
  return true;
1313
1339
  } else {
1314
1340
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1341
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1342
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1343
  return false;
1318
1344
  }
1319
1345
  }
1320
1346
 
1321
1347
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1348
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1349
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1350
  return false;
1325
1351
  }
1326
1352
 
@@ -1339,10 +1365,10 @@ fn mirrorMove(
1339
1365
  side.last_selected_move = foe.last_used_move;
1340
1366
 
1341
1367
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1368
+ try options.log.fail(.{ battle.active(player), .None });
1343
1369
  return null;
1344
1370
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1371
+ incrementPP(side, player, mslot, options);
1346
1372
  }
1347
1373
 
1348
1374
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1389,7 @@ fn metronome(
1363
1389
  var side = battle.side(player);
1364
1390
 
1365
1391
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1392
+ incrementPP(side, player, mslot, options);
1367
1393
 
1368
1394
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1395
  return null;
@@ -1375,20 +1401,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1401
  var immune = false;
1376
1402
  var mist = false;
1377
1403
 
1378
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1404
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1405
+ if (!showdown) try options.chance.commit(player, .hit);
1406
+ return true;
1407
+ }
1379
1408
 
1380
1409
  assert(!immune);
1381
1410
  if (mist) {
1382
1411
  assert(!showdown);
1383
1412
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1413
+ try options.log.activate(.{ foe_ident, .Mist });
1414
+ try options.log.fail(.{ foe_ident, .None });
1386
1415
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1416
+ try options.log.lastmiss(.{});
1417
+ try options.log.miss(.{battle.active(player)});
1390
1418
  }
1391
1419
 
1420
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1421
  return false;
1393
1422
  }
1394
1423
 
@@ -1399,7 +1428,7 @@ fn moveHit(
1399
1428
  immune: *bool,
1400
1429
  mist: *bool,
1401
1430
  options: anytype,
1402
- ) bool {
1431
+ ) !bool {
1403
1432
  var side = battle.side(player);
1404
1433
  const foe = battle.foe(player);
1405
1434
 
@@ -1438,7 +1467,7 @@ fn moveHit(
1438
1467
  const overwritten = overwrite and state > 0;
1439
1468
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1469
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1470
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1471
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1472
  accuracy = accuracy * boost[0] / boost[1];
1444
1473
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1479,7 @@ fn moveHit(
1450
1479
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1480
 
1452
1481
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1482
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1483
  };
1455
1484
 
1456
1485
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1490,17 @@ fn moveHit(
1461
1490
 
1462
1491
  if (!miss) return true;
1463
1492
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1493
+
1464
1494
  side.active.volatiles.Binding = false;
1495
+ options.chance.observe(.binding, player, .None);
1496
+
1465
1497
  return false;
1466
1498
  }
1467
1499
 
1468
1500
  fn checkFaint(
1469
1501
  battle: anytype,
1470
1502
  player: Player,
1503
+ cap: bool,
1471
1504
  options: anytype,
1472
1505
  ) @TypeOf(options.log).Error!?Result {
1473
1506
  const side = battle.side(player);
@@ -1477,33 +1510,32 @@ fn checkFaint(
1477
1510
  const foe_fainted = foe.stored().hp == 0;
1478
1511
 
1479
1512
  const player_out = findFirstAlive(side) == 0;
1480
- const foe_out = findFirstAlive(foe) == 0;
1481
- const tie = player_out and foe_out;
1482
- const more = tie or player_out or foe_out;
1513
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1514
+ const more = player_out or foe_out;
1483
1515
 
1484
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1485
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1516
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1517
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1486
1518
 
1487
1519
  assert(!side.active.volatiles.MultiHit);
1488
1520
  assert(!foe.active.volatiles.MultiHit);
1489
1521
 
1490
- if (tie) {
1491
- try options.log.tie();
1522
+ if (player_out and foe_out) {
1523
+ try options.log.tie(.{});
1492
1524
  return Result.Tie;
1493
1525
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1526
+ try options.log.win(.{player.foe()});
1495
1527
  return if (player == .P1) Result.Lose else Result.Win;
1496
1528
  } else if (foe_out) {
1497
- try options.log.win(player);
1529
+ try options.log.win(.{player});
1498
1530
  return if (player == .P1) Result.Win else Result.Lose;
1499
1531
  }
1500
1532
 
1501
1533
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1502
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1503
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1534
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1535
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1536
  }
1505
1537
 
1506
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1538
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1507
1539
  var side = battle.side(player);
1508
1540
  var foe = battle.foe(player);
1509
1541
  assert(side.stored().hp == 0);
@@ -1512,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1512
1544
  assert(!foe_volatiles.MultiHit);
1513
1545
  if (foe_volatiles.Bide) {
1514
1546
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1515
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1547
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1516
1548
  if (foe_volatiles.state != 0) return Result.Error;
1517
1549
  }
1518
1550
 
@@ -1536,8 +1568,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1568
  side.stored().stats.spe;
1537
1569
  }
1538
1570
 
1539
- try options.log.faint(battle.active(player), done);
1540
- options.calc.capped(player);
1571
+ try options.log.faint(.{ battle.active(player), done });
1572
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1573
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1574
+ if (cap) options.calc.capped(player);
1541
1575
  return null;
1542
1576
  }
1543
1577
 
@@ -1548,21 +1582,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1582
  var volatiles = &side.active.volatiles;
1549
1583
 
1550
1584
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1585
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1586
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1587
  if (volatiles.Toxic) {
1555
1588
  volatiles.toxic += 1;
1556
1589
  damage *= volatiles.toxic;
1557
1590
  }
1591
+ damage = @min(damage, stored.hp);
1558
1592
 
1559
- const amount = @min(damage, stored.hp);
1560
- if (showdown and amount == 0) break :blk;
1561
-
1562
- stored.hp -= amount;
1563
-
1564
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1565
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1593
+ if (!showdown or damage > 0) {
1594
+ stored.hp -= damage;
1595
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1596
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1597
+ }
1566
1598
  }
1567
1599
 
1568
1600
  if (volatiles.LeechSeed) {
@@ -1586,14 +1618,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1618
  const amount = @min(damage, stored.hp);
1587
1619
  stored.hp -= amount;
1588
1620
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1621
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1622
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1623
 
1592
1624
  const before = foe_stored.hp;
1593
1625
  // Uncapped damage is added back to the foe
1594
1626
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1627
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1596
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1628
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1629
  }
1598
1630
  }
1599
1631
 
@@ -1603,19 +1635,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1603
1635
 
1604
1636
  battle.turn += 1;
1605
1637
 
1606
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1638
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1639
+ try options.log.tie(.{});
1608
1640
  return Result.Tie;
1609
1641
  }
1610
1642
 
1611
- if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1643
+ if (pkmn.options.mod and battle.turn >= 1000) {
1644
+ try options.log.tie(.{});
1613
1645
  return Result.Tie;
1614
1646
  } else if (battle.turn >= 65535) {
1615
1647
  return Result.Error;
1616
1648
  }
1617
1649
 
1618
- try options.log.turn(battle.turn);
1650
+ try options.log.turn(.{battle.turn});
1619
1651
 
1620
1652
  return Result.Default;
1621
1653
  }
@@ -1675,9 +1707,9 @@ fn checkEBC(battle: anytype) bool {
1675
1707
  }
1676
1708
 
1677
1709
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1710
+ if (side.active.volatiles.disable_move != 0) {
1679
1711
  // A Pokémon that transforms after being disabled may end up with less move slots
1680
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1712
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1713
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1714
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1715
  m
@@ -1685,32 +1717,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1717
  side.last_selected_move;
1686
1718
  if (m != .None and m == last) {
1687
1719
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1720
+ try options.log.cant(.{ ident, .Disable, last });
1689
1721
  return true;
1690
1722
  }
1691
1723
  }
1692
1724
  return false;
1693
1725
  }
1694
1726
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1727
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1728
  const side = battle.side(who);
1697
1729
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1730
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1731
  }
1700
1732
  }
1701
1733
 
1702
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1703
- var volatiles = &active.volatiles;
1704
- assert(volatiles.Thrashing);
1705
- if (volatiles.attacks > 0) return false;
1706
-
1707
- volatiles.Thrashing = false;
1708
- volatiles.Confusion = true;
1709
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1710
- return true;
1711
- }
1712
-
1713
- inline fn onBegin(
1734
+ fn onBegin(
1714
1735
  battle: anytype,
1715
1736
  player: Player,
1716
1737
  move: Move.Data,
@@ -1718,7 +1739,6 @@ inline fn onBegin(
1718
1739
  residual: *bool,
1719
1740
  options: anytype,
1720
1741
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1742
  return switch (move.effect) {
1723
1743
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1744
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1760,7 @@ inline fn onBegin(
1740
1760
  };
1741
1761
  }
1742
1762
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1763
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1764
  return switch (move.effect) {
1746
1765
  .Bide => Effects.bide(battle, player, options),
1747
1766
  // zig fmt: off
@@ -1756,14 +1775,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1775
  };
1757
1776
  }
1758
1777
 
1759
- inline fn alwaysHappens(
1778
+ fn alwaysHappens(
1760
1779
  battle: anytype,
1761
1780
  player: Player,
1762
1781
  move: Move.Data,
1763
1782
  residual: *bool,
1764
1783
  options: anytype,
1765
1784
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1785
  return switch (move.effect) {
1768
1786
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1787
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1793,7 @@ inline fn alwaysHappens(
1775
1793
  };
1776
1794
  }
1777
1795
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1796
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1797
  return switch (move.effect) {
1780
1798
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1799
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1817,12 @@ pub const Effects = struct {
1799
1817
 
1800
1818
  side.active.volatiles.Bide = true;
1801
1819
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1820
+
1802
1821
  side.active.volatiles.state = 0;
1803
1822
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1823
+ options.chance.observe(.attacking, player, .started);
1804
1824
 
1805
- try options.log.start(battle.active(player), .Bide);
1825
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1826
  }
1807
1827
 
1808
1828
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1836,11 @@ pub const Effects = struct {
1816
1836
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1837
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1838
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1839
+ try options.log.curestatus(.{
1820
1840
  battle.active(player.foe()),
1821
1841
  foe_stored.status,
1822
1842
  .Message,
1823
- );
1843
+ });
1824
1844
  foe_stored.status = 0;
1825
1845
  }
1826
1846
  return;
@@ -1834,7 +1854,7 @@ pub const Effects = struct {
1834
1854
  foe_stored.status = Status.init(.BRN);
1835
1855
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1856
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1857
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1858
  }
1839
1859
 
1840
1860
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1864,8 @@ pub const Effects = struct {
1844
1864
  volatiles.Charging = true;
1845
1865
  const move = side.last_selected_move;
1846
1866
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1867
+ try options.log.laststill(.{});
1868
+ try options.log.prepare(.{ battle.active(player), move });
1849
1869
  }
1850
1870
 
1851
1871
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1876,39 @@ pub const Effects = struct {
1856
1876
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1877
  } else {
1858
1878
  if (showdown) {
1859
- if (!try checkHit(battle, player, move, options)) {
1860
- return;
1861
- } else if (sub) {
1862
- return options.log.fail(battle.active(player.foe()), .None);
1863
- }
1879
+ if (!try checkHit(battle, player, move, options)) return;
1880
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1881
  } else {
1865
- if (sub) {
1866
- return options.log.fail(battle.active(player.foe()), .None);
1867
- } else if (!try checkHit(battle, player, move, options)) {
1868
- return;
1869
- }
1882
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1883
+ if (!try checkHit(battle, player, move, options)) return;
1870
1884
  }
1871
- try options.chance.commit(player, .hit);
1872
1885
  }
1873
1886
 
1874
1887
  if (foe.active.volatiles.Confusion) return;
1875
1888
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1889
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1890
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1891
+ options.chance.observe(.confusion, player.foe(), .started);
1892
+ options.calc.confusion(player.foe());
1893
+
1894
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1895
  }
1880
1896
 
1881
1897
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1898
  const foe = battle.foe(player);
1883
1899
 
1884
1900
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1901
+ try options.log.lastmiss(.{});
1902
+ return options.log.miss(.{battle.active(player)});
1887
1903
  }
1888
1904
 
1889
1905
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1906
+ return options.log.start(.{
1891
1907
  battle.active(player),
1908
+ .TypeChange,
1892
1909
  foe.active.types,
1893
1910
  battle.active(player.foe()),
1894
- );
1911
+ });
1895
1912
  }
1896
1913
 
1897
1914
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1917,10 @@ pub const Effects = struct {
1900
1917
  const foe_ident = battle.active(player.foe());
1901
1918
 
1902
1919
  // Pokémon Showdown handles hit/miss earlier in doMove
1903
- if (!showdown) {
1904
- if (!try checkHit(battle, player, move, options)) return null;
1905
- try options.chance.commit(player, .hit);
1906
- }
1920
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1921
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1922
+ if (volatiles.disable_move != 0) {
1923
+ try options.log.fail(.{ foe_ident, .None });
1910
1924
  return null;
1911
1925
  }
1912
1926
 
@@ -1921,27 +1935,27 @@ pub const Effects = struct {
1921
1935
 
1922
1936
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1937
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
- if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1938
+ if (n == 0 or (pkmn.options.mod and err)) {
1939
+ try options.log.fail(.{ foe_ident, .None });
1926
1940
  return null;
1927
1941
  } else if (err) {
1928
1942
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1929
- assert(!showdown);
1930
1943
  return Result.Error;
1931
1944
  }
1932
1945
 
1933
- volatiles.disabled_move =
1946
+ volatiles.disable_move =
1934
1947
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1948
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1949
+ options.chance.observe(.disable, player.foe(), .started);
1936
1950
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1951
+ const id = foe.active.move(volatiles.disable_move).id;
1952
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1953
  return null;
1940
1954
  }
1941
1955
 
1942
1956
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1957
+ var stored = battle.side(player).stored();
1958
+ const ident = battle.active(player);
1945
1959
 
1946
1960
  if (battle.last_damage == 0) {
1947
1961
  assert(showdown);
@@ -1954,7 +1968,7 @@ pub const Effects = struct {
1954
1968
  if (stored.hp == stored.stats.hp) return;
1955
1969
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1970
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1971
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1972
  }
1959
1973
 
1960
1974
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +2000,7 @@ pub const Effects = struct {
1986
2000
  if (side.active.volatiles.FocusEnergy) return;
1987
2001
  side.active.volatiles.FocusEnergy = true;
1988
2002
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
2003
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
2004
  }
1991
2005
 
1992
2006
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2003,12 +2017,12 @@ pub const Effects = struct {
2003
2017
  if (foe.active.types.includes(move.type)) return;
2004
2018
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2005
2019
  // Freeze Clause Mod
2006
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2020
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2007
2021
 
2008
2022
  foe_stored.status = Status.init(.FRZ);
2009
2023
  // GLITCH: Hyper Beam recharging status is not cleared
2010
2024
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
2025
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2026
  }
2013
2027
 
2014
2028
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2030,7 @@ pub const Effects = struct {
2016
2030
  var side = battle.side(player);
2017
2031
  var foe = battle.foe(player);
2018
2032
 
2019
- var side_stored = side.stored();
2033
+ const side_stored = side.stored();
2020
2034
  var foe_stored = foe.stored();
2021
2035
 
2022
2036
  const player_ident = battle.active(player);
@@ -2028,8 +2042,8 @@ pub const Effects = struct {
2028
2042
  side.active.stats = side_stored.stats;
2029
2043
  foe.active.stats = foe_stored.stats;
2030
2044
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2045
+ try log.activate(.{ player_ident, .Haze });
2046
+ try log.clearallboost(.{});
2033
2047
 
2034
2048
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2049
  if (showdown) {
@@ -2037,24 +2051,26 @@ pub const Effects = struct {
2037
2051
  const p: Player = @enumFromInt(i);
2038
2052
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2053
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2054
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2055
  s.stored().status = 0;
2056
+ options.chance.observe(.sleep, p, .None);
2042
2057
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2058
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2059
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2060
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2061
+ try clearVolatiles(battle, p, options);
2047
2062
  }
2048
2063
  } else {
2049
2064
  if (Status.any(foe_stored.status)) {
2050
2065
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2066
  foe.last_selected_move = .SKIP_TURN;
2067
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2068
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2069
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2070
  foe_stored.status = 0;
2055
2071
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2072
+ try clearVolatiles(battle, player, options);
2073
+ try clearVolatiles(battle, player.foe(), options);
2058
2074
  }
2059
2075
  }
2060
2076
 
@@ -2066,7 +2082,7 @@ pub const Effects = struct {
2066
2082
  // GLITCH: HP recovery move failure glitches
2067
2083
  const delta = stored.stats.hp - stored.hp;
2068
2084
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2085
+ return options.log.fail(.{ ident, .None });
2070
2086
  }
2071
2087
 
2072
2088
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2090,17 @@ pub const Effects = struct {
2074
2090
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2091
  if (showdown) battle.rng.advance(1);
2076
2092
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2093
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2094
  stored.hp = stored.stats.hp;
2079
2095
  } else {
2080
2096
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2097
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2098
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2099
  }
2084
2100
 
2085
2101
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2102
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2103
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2104
  }
2089
2105
 
2090
2106
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2109,31 @@ pub const Effects = struct {
2093
2109
  if (showdown) {
2094
2110
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2111
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2112
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2113
  }
2098
2114
  if (!try checkHit(battle, player, move, options)) return;
2099
2115
  if (foe.active.volatiles.LeechSeed) return;
2100
2116
  } else {
2101
2117
  if (!try checkHit(battle, player, move, options)) return;
2102
2118
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2119
+ try options.log.lastmiss(.{});
2120
+ return options.log.miss(.{battle.active(player)});
2105
2121
  }
2106
2122
  }
2107
2123
 
2108
- try options.chance.commit(player, .hit);
2109
2124
  foe.active.volatiles.LeechSeed = true;
2110
2125
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2126
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2127
  }
2113
2128
 
2114
2129
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2130
  var side = battle.side(player);
2131
+ const ident = battle.active(player);
2116
2132
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2133
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2134
  side.active.volatiles.LightScreen = true;
2121
2135
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2136
+ try options.log.start(.{ ident, .LightScreen });
2123
2137
  }
2124
2138
 
2125
2139
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2161,15 @@ pub const Effects = struct {
2147
2161
  if (!has_mimic) {
2148
2162
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2163
  if (!try checkHit(battle, player, move, options)) return;
2150
- try options.chance.commit(player, .hit);
2151
- return try options.log.fail(battle.active(player.foe()), .None);
2164
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2165
  }
2153
2166
  }
2154
2167
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2168
 
2157
2169
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2170
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2171
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2172
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2173
  }
2162
2174
 
2163
2175
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2178,7 @@ pub const Effects = struct {
2166
2178
  if (side.active.volatiles.Mist) return;
2167
2179
  side.active.volatiles.Mist = true;
2168
2180
 
2169
- try options.log.start(battle.active(player), .Mist);
2181
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2182
  }
2171
2183
 
2172
2184
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2204,25 @@ pub const Effects = struct {
2192
2204
 
2193
2205
  if (showdown) {
2194
2206
  // Invulnerability trumps type immunity on Pokémon Showdown
2195
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2196
- if (!try checkHit(battle, player, move, options)) return;
2207
+ if (immune and !foe.active.volatiles.Invulnerable) {
2208
+ return log.immune(.{ foe_ident, .None });
2209
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2210
  }
2198
2211
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2212
+ return log.fail(.{
2200
2213
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2214
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2215
+ });
2203
2216
  }
2204
2217
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2218
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2219
  if (!try checkHit(battle, player, move, options)) return;
2207
2220
  }
2208
- try options.chance.commit(player, .hit);
2209
2221
 
2210
2222
  foe_stored.status = Status.init(.PAR);
2211
2223
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2224
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2225
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2226
  }
2215
2227
 
2216
2228
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2244,12 @@ pub const Effects = struct {
2232
2244
  foe_stored.status = Status.init(.PAR);
2233
2245
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2246
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2247
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2248
  }
2237
2249
 
2238
2250
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2251
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2252
+ try options.log.fieldactivate(.{});
2241
2253
  }
2242
2254
  }
2243
2255
 
@@ -2252,28 +2264,27 @@ pub const Effects = struct {
2252
2264
  return;
2253
2265
  } else if (foe.active.volatiles.Substitute) {
2254
2266
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2267
+ return log.fail(.{ foe_ident, .None });
2256
2268
  } else if (Status.any(foe_stored.status)) {
2257
2269
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2270
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2271
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2260
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2272
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2273
  if (!showdown)
2262
- .Poison
2274
+ Fail.Poison
2263
2275
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2276
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2277
  else
2266
- .None
2278
+ Fail.None
2267
2279
  else
2268
- .None);
2280
+ Fail.None });
2269
2281
  } else if (foe.active.types.includes(.Poison)) {
2270
2282
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2283
+ return log.immune(.{ foe_ident, .None });
2272
2284
  }
2273
2285
 
2274
2286
  if (move.effect == .Poison) {
2275
2287
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2288
  } else {
2278
2289
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2290
  return;
@@ -2287,7 +2298,7 @@ pub const Effects = struct {
2287
2298
  foe.active.volatiles.toxic = 0;
2288
2299
  }
2289
2300
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2301
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2302
  }
2292
2303
 
2293
2304
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2318,23 @@ pub const Effects = struct {
2307
2318
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2319
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2320
 
2310
- try options.log.damageOf(
2321
+ try options.log.damage(.{
2311
2322
  battle.active(player),
2312
2323
  stored,
2313
2324
  .RecoilOf,
2314
2325
  battle.active(player.foe()),
2315
- );
2326
+ });
2316
2327
  if (showdown and stored.hp == 0) residual.* = false;
2317
2328
  }
2318
2329
 
2319
2330
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2331
  var side = battle.side(player);
2332
+ const ident = battle.active(player);
2321
2333
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2334
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2335
  side.active.volatiles.Reflect = true;
2324
2336
 
2325
- try options.log.start(battle.active(player), .Reflect);
2337
+ try options.log.start(.{ ident, .Reflect });
2326
2338
  }
2327
2339
 
2328
2340
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,18 +2348,17 @@ pub const Effects = struct {
2336
2348
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2349
  } else {
2338
2350
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2351
+ return options.log.fail(.{
2340
2352
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2353
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2354
+ });
2343
2355
  }
2344
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2356
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2357
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2358
  }
2348
2359
 
2349
2360
  // Sleep Clause Mod
2350
- if (showdown) {
2361
+ if (pkmn.options.mod) {
2351
2362
  for (foe.pokemon) |p| {
2352
2363
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2353
2364
  }
@@ -2355,19 +2366,20 @@ pub const Effects = struct {
2355
2366
  foe.active.volatiles.Recharging = false;
2356
2367
 
2357
2368
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2369
+ options.chance.observe(.sleep, player.foe(), .started);
2370
+
2358
2371
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2372
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2373
  }
2361
2374
 
2362
2375
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2376
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2377
  }
2365
2378
 
2366
2379
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2380
  var side = battle.side(player);
2368
2381
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2382
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2383
  }
2372
2384
 
2373
2385
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2391,30 @@ pub const Effects = struct {
2379
2391
  // avoid having to convert to floating point)
2380
2392
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2393
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2394
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2395
  }
2385
2396
 
2386
2397
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2398
  side.stored().hp -= hp;
2388
2399
  side.active.volatiles.substitute = hp + 1;
2389
2400
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2401
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2402
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2403
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2404
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2405
  }
2395
2406
  }
2396
2407
 
2397
2408
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2398
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2399
-
2400
2409
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2401
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2402
-
2403
- battle.last_damage = 0;
2410
+ if (showdown) {
2411
+ if (battle.side(player).last_selected_move == .Teleport) return;
2412
+ _ = try checkHit(battle, player, move, options);
2413
+ battle.last_damage = 0;
2414
+ } else {
2415
+ try options.log.fail(.{ battle.active(player), .None });
2416
+ try options.log.laststill(.{});
2417
+ }
2404
2418
  }
2405
2419
 
2406
2420
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2424,8 @@ pub const Effects = struct {
2410
2424
 
2411
2425
  volatiles.Thrashing = true;
2412
2426
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2427
+
2428
+ options.chance.observe(.attacking, player, .started);
2413
2429
  }
2414
2430
 
2415
2431
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2454,7 @@ pub const Effects = struct {
2438
2454
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2455
  }
2440
2456
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2457
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2458
  }
2443
2459
 
2444
2460
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2448,13 +2464,14 @@ pub const Effects = struct {
2448
2464
  if (side.active.volatiles.Binding) return;
2449
2465
  side.active.volatiles.Binding = true;
2450
2466
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2451
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2467
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2452
2468
  if (!showdown) {
2453
2469
  foe.active.volatiles.Recharging = false;
2454
2470
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2455
2471
 
2456
2472
  side.active.volatiles.attacks =
2457
2473
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2474
+ options.chance.observe(.binding, player, .started);
2458
2475
  }
2459
2476
 
2460
2477
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2485,87 @@ pub const Effects = struct {
2468
2485
  switch (move.effect) {
2469
2486
  .AttackUp1, .AttackUp2, .Rage => {
2470
2487
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2488
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2489
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2490
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2491
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2492
  if (stats.atk == MAX_STAT_VALUE) {
2476
2493
  boosts.atk -= 1;
2477
2494
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2495
+ try log.boost(.{ ident, reason, n });
2496
+ try log.boost(.{ ident, .Attack, -1 });
2480
2497
  if (move.effect == .Rage) return;
2481
2498
  }
2482
- return try log.fail(ident, .None);
2499
+ return log.fail(.{ ident, .None });
2483
2500
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2501
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2502
  const stat = unmodifiedStats(battle, side).atk;
2486
2503
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2504
+ try log.boost(.{ ident, reason, n });
2488
2505
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2506
  if (showdown and move.effect == .Rage) return;
2490
2507
  },
2491
2508
  .DefenseUp1, .DefenseUp2 => {
2492
2509
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2510
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2511
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2512
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2513
  if (stats.def == MAX_STAT_VALUE) {
2497
2514
  boosts.def -= 1;
2498
2515
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2516
+ try log.boost(.{ ident, .Defense, n });
2517
+ try log.boost(.{ ident, .Defense, -1 });
2501
2518
  }
2502
- return try log.fail(ident, .None);
2519
+ return log.fail(.{ ident, .None });
2503
2520
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2521
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2522
  const stat = unmodifiedStats(battle, side).def;
2506
2523
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2524
+ try log.boost(.{ ident, .Defense, n });
2508
2525
  },
2509
2526
  .SpeedUp2 => {
2510
2527
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2528
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2529
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2530
  if (stats.spe == MAX_STAT_VALUE) {
2514
2531
  boosts.spe -= 1;
2515
2532
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2533
+ try log.boost(.{ ident, .Speed, 2 });
2534
+ try log.boost(.{ ident, .Speed, -1 });
2518
2535
  }
2519
- return try log.fail(ident, .None);
2536
+ return log.fail(.{ ident, .None });
2520
2537
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2538
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2539
  const stat = unmodifiedStats(battle, side).spe;
2523
2540
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2541
+ try log.boost(.{ ident, .Speed, 2 });
2525
2542
  },
2526
2543
  .SpecialUp1, .SpecialUp2 => {
2527
2544
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2545
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2546
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2547
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2548
  if (stats.spc == MAX_STAT_VALUE) {
2532
2549
  boosts.spc -= 1;
2533
2550
  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);
2551
+ try log.boost(.{ ident, .SpecialAttack, n });
2552
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2553
+ try log.boost(.{ ident, .SpecialDefense, n });
2554
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2538
2555
  }
2539
- return try log.fail(ident, .None);
2556
+ return log.fail(.{ ident, .None });
2540
2557
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2558
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2559
  const stat = unmodifiedStats(battle, side).spc;
2543
2560
  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);
2561
+ try log.boost(.{ ident, .SpecialAttack, n });
2562
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2563
  },
2547
2564
  .EvasionUp1 => {
2548
2565
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2566
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2567
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2568
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2569
  },
2553
2570
  else => unreachable,
2554
2571
  }
@@ -2563,7 +2580,7 @@ pub const Effects = struct {
2563
2580
  const foe_ident = battle.active(player.foe());
2564
2581
  const secondary = move.effect.isStatDownChance();
2565
2582
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2583
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2584
 
2568
2585
  if (secondary) {
2569
2586
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2588,6 @@ pub const Effects = struct {
2571
2588
  } else {
2572
2589
  // checkHit already checks for Invulnerable
2573
2590
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2591
  }
2576
2592
 
2577
2593
  var stats = &foe.active.stats;
@@ -2581,82 +2597,82 @@ pub const Effects = struct {
2581
2597
  switch (move.effect) {
2582
2598
  .AttackDown1, .AttackDownChance => {
2583
2599
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2600
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2601
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2602
  if (stats.atk == 1) {
2587
2603
  boosts.atk += 1;
2588
2604
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2605
+ try log.boost(.{ foe_ident, .Attack, -1 });
2606
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2607
  }
2592
- return try log.fail(foe_ident, .None);
2608
+ return log.fail(.{ foe_ident, .None });
2593
2609
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2610
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2611
  const stat = unmodifiedStats(battle, foe).atk;
2596
2612
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2613
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2614
  },
2599
2615
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2616
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2617
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2618
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2619
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2620
  if (stats.def == 1) {
2605
2621
  boosts.def += 1;
2606
2622
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2623
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2624
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2625
  }
2610
- return try log.fail(foe_ident, .None);
2626
+ return log.fail(.{ foe_ident, .None });
2611
2627
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2628
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2629
  const stat = unmodifiedStats(battle, foe).def;
2614
2630
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2631
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2632
  },
2617
2633
  .SpeedDown1, .SpeedDownChance => {
2618
2634
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2635
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2636
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2637
  if (stats.spe == 1) {
2622
2638
  boosts.spe += 1;
2623
2639
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2640
+ try log.boost(.{ foe_ident, .Speed, -1 });
2641
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2642
  }
2627
- return try log.fail(foe_ident, .None);
2643
+ return log.fail(.{ foe_ident, .None });
2628
2644
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2645
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2646
  const stat = unmodifiedStats(battle, foe).spe;
2631
2647
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2648
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2649
  assert(boosts.spe >= -6);
2634
2650
  },
2635
2651
  .SpecialDownChance => {
2636
2652
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2653
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2654
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2655
  if (stats.spc == 1) {
2640
2656
  boosts.spc += 1;
2641
2657
  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);
2658
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2659
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2660
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2661
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2662
  }
2647
- return try log.fail(foe_ident, .None);
2663
+ return log.fail(.{ foe_ident, .None });
2648
2664
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2665
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2666
  const stat = unmodifiedStats(battle, foe).spc;
2651
2667
  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);
2668
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2669
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2670
  },
2655
2671
  .AccuracyDown1 => {
2656
2672
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2673
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2674
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2675
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2676
  },
2661
2677
  else => unreachable,
2662
2678
  }
@@ -2680,40 +2696,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2696
  }
2681
2697
  }
2682
2698
 
2683
- inline fn isForced(active: anytype) bool {
2699
+ fn isForced(active: anytype) bool {
2684
2700
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2701
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2702
  }
2687
2703
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2704
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2705
  var log = options.log;
2690
2706
  var side = battle.side(who);
2691
2707
  var volatiles = &side.active.volatiles;
2692
2708
  const ident = battle.active(who);
2693
2709
 
2694
- options.chance.clearDurations(who, clear);
2695
-
2696
- if (volatiles.disabled_move != 0) {
2697
- volatiles.disabled_move = 0;
2698
- volatiles.disabled_duration = 0;
2699
- try log.end(ident, .DisableSilent);
2710
+ if (volatiles.disable_move != 0) {
2711
+ volatiles.disable_move = 0;
2712
+ volatiles.disable_duration = 0;
2713
+ options.chance.observe(.disable, who, .None);
2714
+ try log.end(.{ ident, .DisableSilent });
2700
2715
  }
2701
2716
  if (volatiles.Confusion) {
2702
2717
  // volatiles.confusion is left unchanged
2703
2718
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2719
+ options.chance.observe(.confusion, who, .None);
2720
+ try log.end(.{ ident, .ConfusionSilent });
2705
2721
  }
2706
2722
  if (volatiles.Mist) {
2707
2723
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2724
+ try log.end(.{ ident, .MistSilent });
2709
2725
  }
2710
2726
  if (volatiles.FocusEnergy) {
2711
2727
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2728
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2729
  }
2714
2730
  if (volatiles.LeechSeed) {
2715
2731
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2732
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2733
  }
2718
2734
  if (!showdown and volatiles.Toxic) {
2719
2735
  volatiles.Toxic = false;
@@ -2721,24 +2737,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2737
  }
2722
2738
  if (volatiles.LightScreen) {
2723
2739
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2740
+ try log.end(.{ ident, .LightScreenSilent });
2725
2741
  }
2726
2742
  if (volatiles.Reflect) {
2727
2743
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2744
+ try log.end(.{ ident, .ReflectSilent });
2729
2745
  }
2730
2746
  if (showdown and volatiles.Toxic) {
2731
2747
  volatiles.Toxic = false;
2732
2748
  // Pokémon Showdown erroneously clears the toxic counter
2733
2749
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2750
+ try log.end(.{ ident, .ToxicSilent });
2735
2751
  }
2736
2752
  }
2737
2753
 
2754
+ fn decrement(
2755
+ comptime field: chance.Action.Field,
2756
+ player: Player,
2757
+ options: anytype,
2758
+ n: anytype,
2759
+ ) @TypeOf(n) {
2760
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2761
+ .started, .ended => 0,
2762
+ .continuing => if (n > 1) n - 1 else n,
2763
+ else => unreachable,
2764
+ } else n - 1;
2765
+ }
2766
+
2738
2767
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2768
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2769
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2770
  player == .P1
2771
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2772
+ battle.rng.speedTie()
2742
2773
  else if (showdown)
2743
2774
  battle.rng.range(u8, 0, 2) == 0
2744
2775
  else
@@ -2748,21 +2779,31 @@ pub const Rolls = struct {
2748
2779
  return p1;
2749
2780
  }
2750
2781
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2782
+ fn criticalHit(
2783
+ battle: anytype,
2784
+ player: Player,
2785
+ rate: u8,
2786
+ spurious: bool,
2787
+ options: anytype,
2788
+ ) !bool {
2752
2789
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2790
  val == .true
2791
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2792
+ battle.rng.criticalHit(player, rate)
2754
2793
  else if (showdown)
2755
2794
  battle.rng.chance(u8, rate, 256)
2756
2795
  else
2757
2796
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2797
 
2759
- try options.chance.criticalHit(player, crit, rate);
2798
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2799
  return crit;
2761
2800
  }
2762
2801
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2802
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2803
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2804
  val
2805
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2806
+ battle.rng.damage(player)
2766
2807
  else roll: {
2767
2808
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2809
  while (true) {
@@ -2776,21 +2817,27 @@ pub const Rolls = struct {
2776
2817
  return roll;
2777
2818
  }
2778
2819
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2820
+ const uN = if (pkmn.options.miss) u8 else u9;
2821
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2822
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2780
2823
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2824
  val == .true
2825
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2826
+ battle.rng.hit(player, acc)
2782
2827
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2828
+ battle.rng.chance(uN, acc, 256)
2784
2829
  else
2785
- battle.rng.next() < accuracy;
2830
+ battle.rng.next() < acc;
2786
2831
 
2787
- options.chance.hit(ok, accuracy);
2832
+ try options.chance.hit(player, ok, acc);
2788
2833
  return ok;
2789
2834
  }
2790
2835
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2836
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2837
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2838
  val == .true
2839
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2840
+ battle.rng.confused(player)
2794
2841
  else if (showdown)
2795
2842
  !battle.rng.chance(u8, 128, 256)
2796
2843
  else
@@ -2800,9 +2847,11 @@ pub const Rolls = struct {
2800
2847
  return cfz;
2801
2848
  }
2802
2849
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2850
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2851
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2852
  val == .true
2853
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2854
+ battle.rng.paralyzed(player)
2806
2855
  else if (showdown)
2807
2856
  battle.rng.chance(u8, 63, 256)
2808
2857
  else
@@ -2812,9 +2861,11 @@ pub const Rolls = struct {
2812
2861
  return par;
2813
2862
  }
2814
2863
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2864
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2865
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2866
  val == .true
2867
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2868
+ battle.rng.secondaryChance(player, 25)
2818
2869
  else if (showdown)
2819
2870
  battle.rng.chance(u8, 25, 256)
2820
2871
  else
@@ -2824,11 +2875,13 @@ pub const Rolls = struct {
2824
2875
  return proc;
2825
2876
  }
2826
2877
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2878
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2879
  const rate: u8 = if (low) 26 else 77;
2829
2880
 
2830
2881
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2882
  val == .true
2883
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2884
+ battle.rng.secondaryChance(player, rate)
2832
2885
  else if (showdown)
2833
2886
  battle.rng.chance(u8, rate, 256)
2834
2887
  else
@@ -2838,11 +2891,13 @@ pub const Rolls = struct {
2838
2891
  return proc;
2839
2892
  }
2840
2893
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2894
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2895
  const rate: u8 = if (low) 52 else 103;
2843
2896
 
2844
2897
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2898
  val == .true
2899
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2900
+ battle.rng.secondaryChance(player, rate)
2846
2901
  else if (showdown)
2847
2902
  battle.rng.chance(u8, rate, 256)
2848
2903
  else
@@ -2852,9 +2907,11 @@ pub const Rolls = struct {
2852
2907
  return proc;
2853
2908
  }
2854
2909
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2910
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2911
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2912
  val == .true
2913
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2914
+ battle.rng.secondaryChance(player, 85)
2858
2915
  else if (showdown)
2859
2916
  battle.rng.chance(u8, 85, 256)
2860
2917
  else
@@ -2864,14 +2921,14 @@ pub const Rolls = struct {
2864
2921
  return proc;
2865
2922
  }
2866
2923
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2924
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2925
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2926
  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: {
2927
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2928
+ battle.rng.metronome(player)
2929
+ else if (showdown)
2930
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2931
+ else move: {
2875
2932
  while (true) {
2876
2933
  const r = battle.rng.next();
2877
2934
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2941,11 @@ pub const Rolls = struct {
2884
2941
  return move;
2885
2942
  }
2886
2943
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2944
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2945
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2946
  val - 1
2947
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2948
+ battle.rng.psywave(player)
2890
2949
  else if (showdown)
2891
2950
  battle.rng.range(u8, 0, max)
2892
2951
  else power: {
@@ -2901,11 +2960,14 @@ pub const Rolls = struct {
2901
2960
  return power;
2902
2961
  }
2903
2962
 
2904
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2905
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2906
- @intCast(val)
2963
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2964
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2965
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2966
+ else
2967
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2968
+ battle.rng.sleepDuration(player)
2907
2969
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2970
+ battle.rng.range(u3, 1, 8)
2909
2971
  else duration: {
2910
2972
  while (true) {
2911
2973
  const r = battle.rng.next() & 7;
@@ -2914,60 +2976,74 @@ pub const Rolls = struct {
2914
2976
  };
2915
2977
 
2916
2978
  assert(duration >= 1 and duration <= 7);
2917
- options.chance.duration(.sleep, player, player.foe(), duration);
2979
+ options.chance.duration(player, duration);
2918
2980
  return duration;
2919
2981
  }
2920
2982
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
- val
2983
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2984
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2985
+ if (val >= 1 and val <= 8) val else null
2986
+ else
2987
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2988
+ battle.rng.disableDuration(player)
2924
2989
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2990
+ battle.rng.range(u4, 1, 9)
2926
2991
  else
2927
2992
  @intCast((battle.rng.next() & 7) + 1);
2928
2993
 
2929
2994
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
2995
+ options.chance.duration(player, duration);
2931
2996
  return duration;
2932
2997
  }
2933
2998
 
2934
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2935
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2936
- @intCast(val)
2999
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3000
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3001
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3002
+ else
3003
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3004
+ battle.rng.confusionDuration(player)
2937
3005
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
3006
+ battle.rng.range(u3, 2, 6)
2939
3007
  else
2940
3008
  @intCast((battle.rng.next() & 3) + 2);
2941
3009
 
2942
3010
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3011
+ options.chance.duration(player, duration);
2944
3012
  return duration;
2945
3013
  }
2946
3014
 
2947
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2948
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2949
- @intCast(val)
3015
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3016
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3017
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3018
+ else
3019
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3020
+ battle.rng.attackingDuration(player)
2950
3021
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
3022
+ battle.rng.range(u3, 2, 4)
2952
3023
  else
2953
3024
  @intCast((battle.rng.next() & 1) + 2);
2954
3025
 
2955
3026
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3027
+ options.chance.duration(player, duration);
2957
3028
  return duration;
2958
3029
  }
2959
3030
 
2960
- inline fn distribution(
3031
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3032
+
3033
+ fn distribution(
2961
3034
  battle: anytype,
2962
3035
  comptime effect: Move.Effect,
2963
3036
  player: Player,
2964
3037
  options: anytype,
2965
3038
  ) !u3 {
2966
3039
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
- @intCast(val)
3040
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3041
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3042
+ else
3043
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3044
+ battle.rng.distribution(player)
2969
3045
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3046
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3047
  else n: {
2972
3048
  const r = (battle.rng.next() & 3);
2973
3049
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3051,7 @@ pub const Rolls = struct {
2975
3051
 
2976
3052
  assert(n >= 2 and n <= 5);
2977
3053
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3054
+ options.chance.duration(player, n);
2979
3055
  } else {
2980
3056
  try options.chance.multiHit(player, n);
2981
3057
  }
@@ -2990,15 +3066,15 @@ pub const Rolls = struct {
2990
3066
  options: anytype,
2991
3067
  ) !u4 {
2992
3068
  // TODO: consider throwing error instead of rerolling?
2993
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3069
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2994
3070
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2995
3071
  val
2996
3072
  else
2997
3073
  null
2998
3074
  else
2999
- null;
3000
-
3001
- const slot: u4 = overridden orelse slot: {
3075
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3076
+ battle.rng.moveSlot(player, moves, check_pp)
3077
+ else slot: {
3002
3078
  if (showdown) {
3003
3079
  if (check_pp == 0) {
3004
3080
  var i: usize = moves.len;
@@ -3014,10 +3090,9 @@ pub const Rolls = struct {
3014
3090
  while (i < moves.len and r > 0) : (i += 1) {
3015
3091
  if (moves[i].pp > 0) {
3016
3092
  r -= 1;
3017
- if (r == 0) break;
3093
+ if (r == 0) break :slot @intCast(i + 1);
3018
3094
  }
3019
3095
  }
3020
- break :slot @intCast(i + 1);
3021
3096
  }
3022
3097
  }
3023
3098
 
@@ -3039,11 +3114,11 @@ test "RNG agreement" {
3039
3114
  expected[i] = @intCast(i * 0x1000000);
3040
3115
  }
3041
3116
 
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 };
3117
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3118
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3119
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3120
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3121
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3122
 
3048
3123
  for (0..expected.len) |i| {
3049
3124
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3154,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3154
  const side = battle.side(player);
3080
3155
  const foe = battle.foe(player);
3081
3156
 
3082
- var active = &side.active;
3157
+ const active = &side.active;
3083
3158
  const stored = side.stored();
3084
3159
 
3085
3160
  // 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.
3161
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3162
  //
3088
3163
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3164
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3166,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3166
  // passing due to being forced into using a move. Instead of introducing another option
3092
3167
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3168
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3169
+ // switch at all
3095
3170
  if (isForced(active)) {
3096
3171
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3172
  n += 1;
@@ -3110,7 +3185,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3185
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3186
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3187
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3188
+ // move normally if sleeping/frozen/bound
3114
3189
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3190
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3191
  {
@@ -3123,7 +3198,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3198
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3199
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3200
  // 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.
3201
+ // proc-ed move in their moveset) and disabling all other moves
3127
3202
  if (limited) {
3128
3203
  assert(showdown);
3129
3204
  assert(side.last_selected_move != .None);
@@ -3133,7 +3208,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3208
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3209
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3210
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3211
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3212
  const s = if (struggle) 0 else slot;
3138
3213
  out[n] = .{ .type = .Move, .data = s };
3139
3214
  n += 1;
@@ -3148,14 +3223,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3223
  const m = active.moves[slot - 1];
3149
3224
  if (m.id == .None) break;
3150
3225
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3226
+ if (active.volatiles.disable_move == slot) continue;
3152
3227
  out[n] = .{ .type = .Move, .data = slot };
3153
3228
  n += 1;
3154
3229
  }
3155
3230
  // Struggle (Pokémon Showdown would use 'move 1' here)
3156
3231
  if (n == before) {
3157
3232
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3158
- if (!showdown) {
3233
+ if (!pkmn.options.mod) {
3159
3234
  while (slot <= 4) : (slot += 1) {
3160
3235
  if (active.moves[slot - 1].pp != 0) return n;
3161
3236
  }