@pkmn/engine 0.1.0-dev.a324c50f → 0.1.0-dev.a5846b2d

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