@pkmn/engine 0.1.0-dev.bcb16a6c → 0.1.0-dev.bce6e8a5

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