@pkmn/engine 0.1.0-dev.437f6a52 → 0.1.0-dev.43ee07e2

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