@pkmn/engine 0.1.0-dev.d3e796d5 → 0.1.0-dev.d4bd80b1

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