@pkmn/engine 0.1.0-dev.437f6a52 → 0.1.0-dev.4438e918

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 (86) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +104 -59
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +26 -4
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/ids.json +12 -12
  16. package/build/pkg/data/layout.json +17 -4
  17. package/build/pkg/data/protocol.json +84 -13
  18. package/build/pkg/data.d.ts +27 -5
  19. package/build/pkg/data.js.map +1 -1
  20. package/build/pkg/gen1.d.ts +15 -18
  21. package/build/pkg/gen1.js +30 -21
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +42 -91
  24. package/build/pkg/index.js +23 -70
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +29 -1
  27. package/build/pkg/protocol.js +123 -102
  28. package/build/pkg/protocol.js.map +1 -1
  29. package/build/tools/debug.js +84 -73
  30. package/build/tools/debug.js.map +1 -1
  31. package/build/tools/display/debug.jsx +66 -0
  32. package/build/tools/display/debug.jsx.map +1 -0
  33. package/build/tools/display/index.js +152 -0
  34. package/build/tools/display/index.js.map +1 -0
  35. package/build/tools/display/ui.jsx +360 -0
  36. package/build/tools/display/ui.jsx.map +1 -0
  37. package/build/tools/display/util.js +157 -0
  38. package/build/tools/display/util.js.map +1 -0
  39. package/build.zig +304 -142
  40. package/package.json +71 -121
  41. package/src/bin/install-pkmn-engine +48 -22
  42. package/src/bin/pkmn-debug +1 -1
  43. package/src/lib/bindings/c.zig +200 -0
  44. package/src/lib/{binding → bindings}/node.zig +15 -32
  45. package/src/lib/bindings/wasm.zig +50 -0
  46. package/src/lib/common/array.zig +70 -0
  47. package/src/lib/common/battle.zig +17 -0
  48. package/src/lib/common/data.zig +12 -24
  49. package/src/lib/common/debug.zig +33 -28
  50. package/src/lib/common/js.zig +114 -0
  51. package/src/lib/common/optional.zig +61 -0
  52. package/src/lib/common/options.zig +6 -0
  53. package/src/lib/common/protocol.zig +964 -450
  54. package/src/lib/common/rational.zig +440 -0
  55. package/src/lib/common/rng.zig +22 -15
  56. package/src/lib/common/util.zig +44 -0
  57. package/src/lib/gen1/calc.zig +975 -0
  58. package/src/lib/gen1/chance.zig +1222 -0
  59. package/src/lib/gen1/data/moves.zig +193 -183
  60. package/src/lib/gen1/data/species.zig +4 -6
  61. package/src/lib/gen1/data/types.zig +9 -10
  62. package/src/lib/gen1/data.zig +71 -81
  63. package/src/lib/gen1/helpers.zig +90 -410
  64. package/src/lib/gen1/mechanics.zig +712 -648
  65. package/src/lib/gen2/calc.zig +150 -0
  66. package/src/lib/gen2/chance.zig +896 -0
  67. package/src/lib/gen2/data/items.zig +13 -21
  68. package/src/lib/gen2/data/moves.zig +664 -572
  69. package/src/lib/gen2/data/species.zig +2 -4
  70. package/src/lib/gen2/data/types.zig +139 -34
  71. package/src/lib/gen2/data.zig +121 -96
  72. package/src/lib/gen2/generated.zig +801 -0
  73. package/src/lib/gen2/helpers.zig +53 -38
  74. package/src/lib/gen2/mechanics.zig +3580 -29
  75. package/src/lib/napi.h +2 -0
  76. package/src/lib/node.zig +14 -0
  77. package/src/lib/pkmn.zig +125 -29
  78. package/src/lib/wasm.zig +13 -0
  79. package/src/tools/display/dom.ts +108 -0
  80. package/src/tools/display/ui.css +354 -0
  81. package/build/test/display.js +0 -400
  82. package/build/test/display.js.map +0 -1
  83. package/src/lib/binding/wasm.zig +0 -12
  84. package/src/lib/gen1/README.md +0 -814
  85. package/src/lib/gen2/README.md +0 -309
  86. 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,13 +82,13 @@ 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
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
87
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
105
88
 
106
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
107
90
  assert(!showdown or p2_slot == 1);
108
- if (p2_slot == 0) return Result.Win;
91
+ if (p2_slot == 0) return .Win;
109
92
 
110
93
  try switchIn(battle, .P1, p1_slot, true, options);
111
94
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -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 .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,23 +417,22 @@ 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
472
433
  // every Pokémon must have at least one move
473
434
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
474
- return Result.Error;
435
+ return .Error;
475
436
  } else {
476
437
  auto = true;
477
438
  }
@@ -479,26 +440,42 @@ 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,
494
452
  .ok => {},
495
- .err => return @as(?Result, Result.Error),
453
+ .err => return .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,19 +614,14 @@ 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
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
632
- // on the targeting behavior of the confused Pokémon's selected move which results
633
- // in the wrong behavior with respect to the Substitute + Confusion glitch
634
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
635
- player
636
- else
637
- player.foe();
638
621
 
639
622
  const uncapped = battle.last_damage;
640
623
  // Skipping adjustDamage / randomizeDamage / checkHit
641
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
642
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
643
626
  if (showdown) battle.last_damage = uncapped;
644
627
 
@@ -655,9 +638,12 @@ fn beforeMove(
655
638
  volatiles.Thrashing = false;
656
639
  volatiles.Charging = false;
657
640
  volatiles.Binding = false;
658
- options.chance.clearDurations(player, null);
641
+
642
+ options.chance.observe(.attacking, player, .None);
643
+ options.chance.observe(.binding, player, .None);
644
+
659
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
660
- try log.cant(ident, .Paralysis);
646
+ try log.cant(.{ ident, .Paralysis });
661
647
  return .done;
662
648
  }
663
649
 
@@ -673,28 +659,22 @@ fn beforeMove(
673
659
  volatiles.state +%= battle.last_damage;
674
660
  }
675
661
 
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);
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
684
664
 
685
665
  if (volatiles.attacks != 0) {
686
- try log.activate(ident, .Bide);
666
+ try log.activate(.{ ident, .Bide });
687
667
  return .done;
688
668
  }
689
669
 
690
670
  volatiles.Bide = false;
691
- try log.end(ident, .Bide);
671
+ try log.end(.{ ident, .Bide });
692
672
 
693
673
  battle.last_damage = volatiles.state *% 2;
694
674
  volatiles.state = 0;
695
675
 
696
676
  if (battle.last_damage == 0) {
697
- try log.fail(ident, .None);
677
+ try log.fail(.{ ident, .None });
698
678
  return .done;
699
679
  }
700
680
 
@@ -714,47 +694,42 @@ fn beforeMove(
714
694
  }
715
695
 
716
696
  if (volatiles.Thrashing) {
717
- assert(volatiles.attacks > 0);
718
- assert(from != null);
697
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
719
698
 
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);
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
726
701
 
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
- }
702
+ if (volatiles.attacks == 0) {
703
+ const overwritten = volatiles.Confusion;
704
+
705
+ volatiles.Thrashing = false;
706
+ volatiles.Confusion = true;
707
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
708
+
709
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
710
+ // ended (the engine's protocol is always from the omniscient perspective and clients
711
+ // can be expected to recognize this pattern and pass the information only to the
712
+ // correct player)
713
+ try log.start(.{ battle.active(player), .ConfusionSilent });
714
+ options.chance.observe(.confusion, player, if (overwritten)
715
+ if (pkmn.options.overwrite) .overwritten else .continuing
716
+ else
717
+ .started);
718
+ if (!overwritten) options.calc.confusion(player);
738
719
  }
720
+
721
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
722
+ // used in useMove and doesn't have the notion of skipping canMove semantics
723
+ if (showdown) side.last_used_move = side.last_selected_move;
724
+
739
725
  return .skip_can;
740
726
  }
741
727
 
742
728
  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);
729
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
751
731
 
752
- try log.move(
753
- battle.active(player),
754
- side.last_selected_move,
755
- battle.active(player.foe()),
756
- side.last_selected_move,
757
- );
732
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
758
733
  if (showdown or battle.last_damage != 0) {
759
734
  const sub = showdown and foe.active.volatiles.Substitute;
760
735
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -774,7 +749,7 @@ fn canMove(
774
749
  mslot: u4,
775
750
  auto: bool,
776
751
  skip_pp: bool,
777
- from: ?Move,
752
+ from: Move,
778
753
  residual: *bool,
779
754
  options: anytype,
780
755
  ) !bool {
@@ -782,33 +757,21 @@ fn canMove(
782
757
  const player_ident = battle.active(player);
783
758
  const move = Move.get(side.last_selected_move);
784
759
 
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
760
  if (side.active.volatiles.Charging) {
791
761
  side.active.volatiles.Charging = false;
792
762
  side.active.volatiles.Invulnerable = false;
793
763
  } else if (move.effect == .Charge) {
794
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
764
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
795
765
  setCounterable(battle, player, side, move);
796
766
  try Effects.charge(battle, player, options);
797
767
  return false;
798
768
  }
799
769
 
800
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
801
- if (!skip) decrementPP(side, mslot, auto);
770
+ side.last_used_move = side.last_selected_move;
771
+ if (!skip_pp) decrementPP(side, mslot, auto);
802
772
 
803
773
  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);
774
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
812
775
  setCounterable(battle, player, side, move);
813
776
 
814
777
  if (move.effect.onBegin()) {
@@ -818,9 +781,14 @@ fn canMove(
818
781
 
819
782
  if (move.effect == .Thrashing) {
820
783
  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);
784
+ } else if (move.effect == .Binding) {
785
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
786
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
787
+ if (showdown) {
788
+ battle.foe(player).active.volatiles.Recharging = false;
789
+ } else {
790
+ try Effects.binding(battle, player, false, options);
791
+ }
824
792
  }
825
793
 
826
794
  return true;
@@ -834,11 +802,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
834
802
  // as the desync results from actually having two separate battle states that subtly disagree
835
803
  const counterable = side.last_selected_move != .Counter and move.bp > 0 and
836
804
  (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
- }
805
+ battle.lastMove(player).counterable = @intFromBool(counterable);
842
806
  }
843
807
 
844
808
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
@@ -850,16 +814,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
850
814
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
851
815
  if (volatiles.Bide) return;
852
816
 
853
- assert(active.move(mslot).pp > 0 or auto);
854
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
817
+ var move_slot = active.move(mslot);
818
+ assert(move_slot.pp > 0 or auto);
819
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
855
820
  if (volatiles.Transform) return;
856
821
 
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;
822
+ move_slot = side.stored().move(mslot);
823
+ assert(move_slot.pp > 0 or auto);
824
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
859
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
860
826
  }
861
827
 
862
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
863
829
  var active = &side.active;
864
830
  const volatiles = &active.volatiles;
865
831
 
@@ -867,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
867
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
868
834
  if (showdown and volatiles.Transform) return;
869
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
870
839
  assert(mslot > 0 and mslot <= 4);
871
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
840
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
872
841
  }
873
842
 
874
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -909,20 +878,20 @@ fn doMove(
909
878
  var miss = showdown and miss: {
910
879
  immune = move.target != .Self and !status and !counter and
911
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
912
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
913
882
  if (immune and move.effect != .Binding) break :miss true;
914
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
915
884
  battle.last_damage = 0;
916
885
  break :miss true;
917
886
  }
918
887
  break :miss (move.target != .Self and
919
- !moveHit(battle, player, move, &immune, &mist, options));
888
+ !try moveHit(battle, player, move, &immune, &mist, options));
920
889
  };
921
890
  assert(!immune or miss or (showdown and move.effect == .Binding));
922
891
 
923
892
  var late = showdown and move.effect != .Explode;
924
893
  const skip = status or immune;
925
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
926
895
  if (showdown and move.effect.isMulti()) {
927
896
  try Effects.multiHit(battle, player, move, options);
928
897
  hits = side.active.volatiles.attacks;
@@ -944,9 +913,10 @@ fn doMove(
944
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
945
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
946
915
  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)) {
949
- return @as(?Result, Result.Error);
916
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
917
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
918
+ if (!showdown) try options.chance.commit(player, .err);
919
+ return .Error;
950
920
  }
951
921
  if (battle.last_damage == 0) {
952
922
  immune = true;
@@ -959,11 +929,12 @@ fn doMove(
959
929
  }
960
930
  }
961
931
 
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);
932
+ var missed: bool = undefined;
933
+ const zero = battle.last_damage == 0;
934
+ if (!showdown and !skip) {
935
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
936
+ miss = missed or zero;
937
+ }
967
938
 
968
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
969
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -987,16 +958,37 @@ fn doMove(
987
958
  ohko = (!showdown or (!immune and !invulnerable)) and
988
959
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
989
960
  if (ohko) {
990
- try log.immune(foe_ident, .OHKO);
961
+ try log.immune(.{ foe_ident, .OHKO });
991
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
992
- try log.immune(foe_ident, .None);
963
+ try log.immune(.{ foe_ident, .None });
964
+ if (!showdown) {
965
+ if (move.effect == .Binding) {
966
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
967
+ // then simply clears it later on if the move happens to miss in moveHit.
968
+ // However, moveHit can return true and we'll still end up in the outer miss
969
+ // handling block because attacking into an immune Pokemon does zero damage
970
+ // (considered missing)
971
+ assert(zero);
972
+ // Thus we still need to actually save the hit result (but not the crit roll).
973
+ // Using .miss for this commit would accomplish what we want, but is somewhat
974
+ // misleading as its entirely possible the move actually did hit (and the target
975
+ // is now trapped) so we use the .binding value instead which is effectively
976
+ // identical, just doesn't claim to know about the actual results of the hit
977
+ // roll)
978
+ try options.chance.commit(player, .binding);
979
+ } else {
980
+ // The .glitch option allows for conditionally committing if we are in a
981
+ // division-by-zero freeze alternative timeline scenario
982
+ try options.chance.commit(player, .glitch);
983
+ }
984
+ }
993
985
  } else if (mist) {
994
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
995
- try log.fail(foe_ident, .None);
986
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
987
+ try log.fail(.{ foe_ident, .None });
996
988
  } else {
997
- if (showdown or !zero) try options.chance.commit(player, .miss);
998
- try log.lastmiss();
999
- try log.miss(battle.active(player));
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
990
+ try log.lastmiss(.{});
991
+ try log.miss(.{battle.active(player)});
1000
992
  }
1001
993
  if (move.effect == .JumpKick) {
1002
994
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -1009,17 +1001,15 @@ fn doMove(
1009
1001
  try buildRage(battle, player.foe(), options);
1010
1002
  } else if (showdown and move.effect == .Disable) {
1011
1003
  try buildRage(battle, player.foe(), options);
1012
- } else if (!showdown and immune and move.effect == .Binding) {
1013
- try options.chance.commit(player, .binding);
1014
1004
  }
1015
1005
  return null;
1016
- } else {
1006
+ } else if (!showdown) {
1017
1007
  try options.chance.commit(player, .hit);
1018
1008
  }
1019
1009
 
1020
1010
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1021
1011
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1022
- // it up above before damage calculation).
1012
+ // it up above before damage calculation)
1023
1013
  if (!showdown and move.effect.isMulti()) {
1024
1014
  try Effects.multiHit(battle, player, move, options);
1025
1015
  hits = side.active.volatiles.attacks;
@@ -1029,18 +1019,18 @@ fn doMove(
1029
1019
  const sub = showdown and foe.active.volatiles.Substitute;
1030
1020
 
1031
1021
  var nullified = false;
1032
- var hit: u4 = 0;
1022
+ var hit: usize = 0;
1033
1023
  while (hit < hits) {
1034
1024
  if (hit == 0) {
1035
- if (crit) try log.crit(battle.active(player.foe()));
1025
+ if (crit) try log.crit(.{battle.active(player.foe())});
1036
1026
  if (effectiveness > Effectiveness.neutral) {
1037
- try log.supereffective(battle.active(player.foe()));
1027
+ try log.supereffective(.{battle.active(player.foe())});
1038
1028
  } else if (effectiveness < Effectiveness.neutral) {
1039
- try log.resisted(battle.active(player.foe()));
1029
+ try log.resisted(.{battle.active(player.foe())});
1040
1030
  }
1041
1031
  }
1042
1032
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1043
- if (hit == 0 and ohko) try log.ohko();
1033
+ if (hit == 0 and ohko) try log.ohko(.{});
1044
1034
  hit += 1;
1045
1035
  if (foe.stored().hp == 0) break;
1046
1036
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1057,33 +1047,31 @@ fn doMove(
1057
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1058
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1059
1049
  }
1060
- try log.hitcount(battle.active(player.foe()), hit);
1061
- } else if (showdown) {
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1051
+ } else if (showdown and move.effect == .Binding) {
1062
1052
  // 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;
1053
+ try Effects.binding(battle, player, rewrap, options);
1054
+ if (immune) {
1055
+ battle.last_damage = 0;
1056
+ assert(foe.stored().hp > 0);
1057
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1058
+ if (sub) {
1059
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1060
+ } else {
1061
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1062
+ try buildRage(battle, player.foe(), options);
1076
1063
  }
1064
+ return null;
1077
1065
  }
1078
1066
  }
1079
1067
 
1080
1068
  // 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.
1069
+ // if an effect was intended to "always happen" it will still get skipped
1082
1070
  if (nullified) return null;
1083
1071
 
1084
1072
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1085
1073
  // 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.
1074
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1087
1075
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1088
1076
 
1089
1077
  if (foe.stored().hp == 0) return null;
@@ -1098,7 +1086,7 @@ fn doMove(
1098
1086
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1099
1087
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1100
1088
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1101
- // puts the |-status| message before |-hitcount| instead of after.
1089
+ // puts the |-status| message before |-hitcount| instead of after
1102
1090
  if (move.effect == .Twineedle) {
1103
1091
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1104
1092
  } else if (move.effect == .Disable) {
@@ -1117,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1117
1105
  const side = battle.side(player);
1118
1106
 
1119
1107
  // Base speed is used for the critical hit calculation, even when Transform-ed
1120
- var rate = @as(u16, Species.chance(side.stored().species));
1108
+ var rate: u16 = Species.chance(side.stored().species);
1121
1109
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1122
1110
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1123
1111
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1124
1112
 
1125
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1113
+ const spurious =
1114
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1115
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1126
1116
  }
1127
1117
 
1128
1118
  fn calcDamage(
@@ -1131,6 +1121,7 @@ fn calcDamage(
1131
1121
  target_player: Player,
1132
1122
  m: ?Move.Data,
1133
1123
  crit: bool,
1124
+ options: anytype,
1134
1125
  ) bool {
1135
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1136
1127
  const cfz = m == null;
@@ -1142,28 +1133,8 @@ fn calcDamage(
1142
1133
 
1143
1134
  const special = move.type.special();
1144
1135
 
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
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1167
1138
 
1168
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1169
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1171,12 +1142,12 @@ fn calcDamage(
1171
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1172
1143
  atk = @max((atk / 4) & 255, 1);
1173
1144
  // 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);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1175
1146
  }
1176
1147
 
1177
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1148
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1178
1149
 
1179
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1150
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1180
1151
 
1181
1152
  if (def == 0) return false;
1182
1153
 
@@ -1188,11 +1159,41 @@ fn calcDamage(
1188
1159
  d = @min(997, d);
1189
1160
  d += 2;
1190
1161
 
1162
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1163
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1164
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1165
+ // that results in counter intuitive probabilities in the common case
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1167
+ atk = attack(side, !crit, special);
1168
+ def = defense(target, !crit, special, false);
1169
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1170
+ }
1171
+
1191
1172
  battle.last_damage = @intCast(d);
1192
1173
 
1193
1174
  return true;
1194
1175
  }
1195
1176
 
1177
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1180
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1181
+ }
1182
+
1183
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) target.stored().stats.spc else target.stored().stats.def
1186
+ else
1187
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1188
+ if (special)
1189
+ target.active.stats.spc *
1190
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1191
+ else
1192
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1193
+ target.active.stats.def *
1194
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1195
+ }
1196
+
1196
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1197
1198
  const side = battle.side(player);
1198
1199
  const foe = battle.foe(player);
@@ -1237,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1237
1238
  const foe = battle.foe(player);
1238
1239
 
1239
1240
  if (!try checkHit(battle, player, move, options)) return null;
1240
- try options.chance.commit(player, .hit);
1241
1241
 
1242
1242
  battle.last_damage = switch (side.last_selected_move) {
1243
1243
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1248,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1248
1248
  .Psywave => power: {
1249
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1250
1250
  // GLITCH: Psywave infinite glitch loop
1251
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return .Error;
1252
1252
  break :power try Rolls.psywave(battle, player, max, options);
1253
1253
  },
1254
1254
  else => unreachable,
1255
1255
  };
1256
1256
 
1257
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
1258
1258
 
1259
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1260
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1267,44 +1267,43 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1267
1267
  const foe = battle.foe(player);
1268
1268
 
1269
1269
  if (battle.last_damage == 0) {
1270
- try options.log.fail(battle.active(player), .None);
1270
+ try options.log.fail(.{ battle.active(player), .None });
1271
1271
  return null;
1272
1272
  }
1273
1273
 
1274
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1275
1275
  const foe_last_selected_move =
1276
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1277
- .Splash
1278
- else
1279
- foe.last_selected_move);
1280
-
1281
- const used = (if (player.foe() == .P1)
1282
- battle.last_moves.p1_counterable
1283
- else
1284
- battle.last_moves.p2_counterable) != 0;
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1285
1280
 
1281
+ const used = battle.lastMove(player.foe()).counterable != 0;
1286
1282
  const selected = foe_last_selected_move.bp > 0 and
1287
1283
  foe.last_selected_move != .Counter and
1288
1284
  (foe_last_selected_move.type == .Normal or
1289
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1290
1286
 
1291
1287
  if (!used and !selected) {
1292
- try options.log.fail(battle.active(player), .None);
1288
+ try options.log.fail(.{ battle.active(player), .None });
1293
1289
  return null;
1294
1290
  }
1295
1291
 
1296
1292
  if (!used or !selected) {
1297
1293
  // 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);
1294
+ if (!pkmn.options.mod) return .Error;
1295
+ try options.log.fail(.{ battle.active(player), .None });
1300
1296
  return null;
1301
1297
  }
1302
1298
 
1303
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1304
1304
 
1305
- // Pokémon Showdown calls checkHit before Counter
1305
+ // Pokémon Showdown calls moveHit before Counter
1306
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1307
- try options.chance.commit(player, .hit);
1308
1307
 
1309
1308
  const sub = showdown and foe.active.volatiles.Substitute;
1310
1309
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1332,20 +1331,20 @@ fn applyDamage(
1332
1331
  subbed.active.volatiles.substitute = 0;
1333
1332
  subbed.active.volatiles.Substitute = false;
1334
1333
  // 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);
1334
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1337
1336
  return true;
1338
1337
  } else {
1339
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1340
1339
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1341
- try options.log.activate(battle.active(sub_player), .Substitute);
1340
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1342
1341
  return false;
1343
1342
  }
1344
1343
  }
1345
1344
 
1346
1345
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1347
1346
  target.stored().hp -= battle.last_damage;
1348
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1347
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1349
1348
  return false;
1350
1349
  }
1351
1350
 
@@ -1357,17 +1356,17 @@ fn mirrorMove(
1357
1356
  auto: bool,
1358
1357
  residual: *bool,
1359
1358
  options: anytype,
1360
- ) !?Result {
1359
+ ) anyerror!?Result {
1361
1360
  var side = battle.side(player);
1362
1361
  const foe = battle.foe(player);
1363
1362
 
1364
1363
  side.last_selected_move = foe.last_used_move;
1365
1364
 
1366
1365
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1367
- try options.log.fail(battle.active(player), .None);
1366
+ try options.log.fail(.{ battle.active(player), .None });
1368
1367
  return null;
1369
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1370
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1371
1370
  }
1372
1371
 
1373
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1384,11 +1383,11 @@ fn metronome(
1384
1383
  auto: bool,
1385
1384
  residual: *bool,
1386
1385
  options: anytype,
1387
- ) !?Result {
1386
+ ) anyerror!?Result {
1388
1387
  var side = battle.side(player);
1389
1388
 
1390
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1391
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1392
1391
 
1393
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1394
1393
  return null;
@@ -1400,20 +1399,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1400
1399
  var immune = false;
1401
1400
  var mist = false;
1402
1401
 
1403
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1402
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1403
+ if (!showdown) try options.chance.commit(player, .hit);
1404
+ return true;
1405
+ }
1404
1406
 
1405
1407
  assert(!immune);
1406
1408
  if (mist) {
1407
1409
  assert(!showdown);
1408
1410
  const foe_ident = battle.active(player.foe());
1409
- try options.log.activate(foe_ident, .Mist);
1410
- try options.log.fail(foe_ident, .None);
1411
+ try options.log.activate(.{ foe_ident, .Mist });
1412
+ try options.log.fail(.{ foe_ident, .None });
1411
1413
  } else {
1412
- try options.chance.commit(player, .miss);
1413
- try options.log.lastmiss();
1414
- try options.log.miss(battle.active(player));
1414
+ try options.log.lastmiss(.{});
1415
+ try options.log.miss(.{battle.active(player)});
1415
1416
  }
1416
1417
 
1418
+ if (!showdown) try options.chance.commit(player, .miss);
1417
1419
  return false;
1418
1420
  }
1419
1421
 
@@ -1424,7 +1426,7 @@ fn moveHit(
1424
1426
  immune: *bool,
1425
1427
  mist: *bool,
1426
1428
  options: anytype,
1427
- ) bool {
1429
+ ) !bool {
1428
1430
  var side = battle.side(player);
1429
1431
  const foe = battle.foe(player);
1430
1432
 
@@ -1444,7 +1446,16 @@ fn moveHit(
1444
1446
  break :miss true;
1445
1447
  }
1446
1448
  if (!showdown) {
1447
- if (move.effect == .Swift) return true;
1449
+ if (move.effect == .Swift) {
1450
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1451
+ // Showdown, hence why the block above doesn't include this code), we need the
1452
+ // damage roll to be committed. However, the commit code will not save this roll if
1453
+ // the glitch occurs and the move was not marked as having hit. To work around this
1454
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1455
+ // the hit as zero to ensure it doesn't actually get saved.
1456
+ try options.chance.hit(player, true, 0);
1457
+ return true;
1458
+ }
1448
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1449
1460
  }
1450
1461
 
@@ -1463,7 +1474,7 @@ fn moveHit(
1463
1474
  const overwritten = overwrite and state > 0;
1464
1475
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1465
1476
 
1466
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1477
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1467
1478
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1468
1479
  accuracy = accuracy * boost[0] / boost[1];
1469
1480
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1475,7 +1486,7 @@ fn moveHit(
1475
1486
  if (overwrite) side.active.volatiles.state = accuracy;
1476
1487
 
1477
1488
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1478
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1489
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1479
1490
  };
1480
1491
 
1481
1492
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1486,13 +1497,17 @@ fn moveHit(
1486
1497
 
1487
1498
  if (!miss) return true;
1488
1499
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1500
+
1489
1501
  side.active.volatiles.Binding = false;
1502
+ options.chance.observe(.binding, player, .None);
1503
+
1490
1504
  return false;
1491
1505
  }
1492
1506
 
1493
1507
  fn checkFaint(
1494
1508
  battle: anytype,
1495
1509
  player: Player,
1510
+ cap: bool,
1496
1511
  options: anytype,
1497
1512
  ) @TypeOf(options.log).Error!?Result {
1498
1513
  const side = battle.side(player);
@@ -1502,33 +1517,32 @@ fn checkFaint(
1502
1517
  const foe_fainted = foe.stored().hp == 0;
1503
1518
 
1504
1519
  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;
1520
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1521
+ const more = player_out or foe_out;
1508
1522
 
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;
1523
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1524
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1511
1525
 
1512
1526
  assert(!side.active.volatiles.MultiHit);
1513
1527
  assert(!foe.active.volatiles.MultiHit);
1514
1528
 
1515
- if (tie) {
1516
- try options.log.tie();
1517
- return Result.Tie;
1529
+ if (player_out and foe_out) {
1530
+ try options.log.tie(.{});
1531
+ return .Tie;
1518
1532
  } else if (player_out) {
1519
- try options.log.win(player.foe());
1520
- return if (player == .P1) Result.Lose else Result.Win;
1533
+ try options.log.win(.{player.foe()});
1534
+ return if (player == .P1) .Lose else .Win;
1521
1535
  } else if (foe_out) {
1522
- try options.log.win(player);
1523
- return if (player == .P1) Result.Win else Result.Lose;
1536
+ try options.log.win(.{player});
1537
+ return if (player == .P1) .Win else .Lose;
1524
1538
  }
1525
1539
 
1526
1540
  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 };
1541
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1542
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1529
1543
  }
1530
1544
 
1531
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1545
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1532
1546
  var side = battle.side(player);
1533
1547
  var foe = battle.foe(player);
1534
1548
  assert(side.stored().hp == 0);
@@ -1537,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1537
1551
  assert(!foe_volatiles.MultiHit);
1538
1552
  if (foe_volatiles.Bide) {
1539
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1540
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1541
- if (foe_volatiles.state != 0) return Result.Error;
1554
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1555
+ if (foe_volatiles.state != 0) return .Error;
1542
1556
  }
1543
1557
 
1544
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1561,8 +1575,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1561
1575
  side.stored().stats.spe;
1562
1576
  }
1563
1577
 
1564
- try options.log.faint(battle.active(player), done);
1565
- options.calc.capped(player);
1578
+ try options.log.faint(.{ battle.active(player), done });
1579
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1580
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1581
+ if (cap) options.calc.capped(player);
1566
1582
  return null;
1567
1583
  }
1568
1584
 
@@ -1573,21 +1589,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1573
1589
  var volatiles = &side.active.volatiles;
1574
1590
 
1575
1591
  const brn = Status.is(stored.status, .BRN);
1576
- if (brn or Status.is(stored.status, .PSN)) blk: {
1592
+ if (brn or Status.is(stored.status, .PSN)) {
1577
1593
  var damage = @max(stored.stats.hp / 16, 1);
1578
-
1579
1594
  if (volatiles.Toxic) {
1580
1595
  volatiles.toxic += 1;
1581
1596
  damage *= volatiles.toxic;
1582
1597
  }
1598
+ damage = @min(damage, stored.hp);
1583
1599
 
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);
1600
+ if (!showdown or damage > 0) {
1601
+ stored.hp -= damage;
1602
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1603
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1604
+ }
1591
1605
  }
1592
1606
 
1593
1607
  if (volatiles.LeechSeed) {
@@ -1611,14 +1625,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1611
1625
  const amount = @min(damage, stored.hp);
1612
1626
  stored.hp -= amount;
1613
1627
 
1614
- // As above, Pokémon Showdown uses damageOf but its not relevant
1615
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1628
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1629
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1616
1630
 
1617
1631
  const before = foe_stored.hp;
1618
1632
  // Uncapped damage is added back to the foe
1619
1633
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1620
1634
  // 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);
1635
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1622
1636
  }
1623
1637
  }
1624
1638
 
@@ -1628,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1628
1642
 
1629
1643
  battle.turn += 1;
1630
1644
 
1631
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1632
- try options.log.tie();
1633
- return Result.Tie;
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1646
+ try options.log.tie(.{});
1647
+ return .Tie;
1634
1648
  }
1635
1649
 
1636
- if (showdown and battle.turn >= 1000) {
1637
- try options.log.tie();
1638
- return Result.Tie;
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1651
+ try options.log.tie(.{});
1652
+ return .Tie;
1639
1653
  } else if (battle.turn >= 65535) {
1640
- return Result.Error;
1654
+ return .Error;
1641
1655
  }
1642
1656
 
1643
- try options.log.turn(battle.turn);
1657
+ try options.log.turn(.{battle.turn});
1644
1658
 
1645
- return Result.Default;
1659
+ return .Default;
1646
1660
  }
1647
1661
 
1648
1662
  fn checkEBC(battle: anytype) bool {
@@ -1700,9 +1714,9 @@ fn checkEBC(battle: anytype) bool {
1700
1714
  }
1701
1715
 
1702
1716
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1703
- if (side.active.volatiles.disabled_move != 0) {
1717
+ if (side.active.volatiles.disable_move != 0) {
1704
1718
  // 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;
1719
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1706
1720
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1707
1721
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1708
1722
  m
@@ -1710,32 +1724,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1710
1724
  side.last_selected_move;
1711
1725
  if (m != .None and m == last) {
1712
1726
  side.active.volatiles.Charging = false;
1713
- try options.log.disabled(ident, last);
1727
+ try options.log.cant(.{ ident, .Disable, last });
1714
1728
  return true;
1715
1729
  }
1716
1730
  }
1717
1731
  return false;
1718
1732
  }
1719
1733
 
1720
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1734
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1721
1735
  const side = battle.side(who);
1722
1736
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1723
1737
  try Effects.boost(battle, who, Move.get(.Rage), options);
1724
1738
  }
1725
1739
  }
1726
1740
 
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(
1741
+ fn onBegin(
1739
1742
  battle: anytype,
1740
1743
  player: Player,
1741
1744
  move: Move.Data,
@@ -1743,7 +1746,6 @@ inline fn onBegin(
1743
1746
  residual: *bool,
1744
1747
  options: anytype,
1745
1748
  ) !void {
1746
- assert(move.effect.onBegin());
1747
1749
  return switch (move.effect) {
1748
1750
  .Confusion => Effects.confusion(battle, player, move, options),
1749
1751
  .Conversion => Effects.conversion(battle, player, options),
@@ -1765,8 +1767,7 @@ inline fn onBegin(
1765
1767
  };
1766
1768
  }
1767
1769
 
1768
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1769
- assert(move.effect.onEnd());
1770
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1770
1771
  return switch (move.effect) {
1771
1772
  .Bide => Effects.bide(battle, player, options),
1772
1773
  // zig fmt: off
@@ -1781,14 +1782,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1781
1782
  };
1782
1783
  }
1783
1784
 
1784
- inline fn alwaysHappens(
1785
+ fn alwaysHappens(
1785
1786
  battle: anytype,
1786
1787
  player: Player,
1787
1788
  move: Move.Data,
1788
1789
  residual: *bool,
1789
1790
  options: anytype,
1790
1791
  ) !void {
1791
- assert(move.effect.alwaysHappens());
1792
1792
  return switch (move.effect) {
1793
1793
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1794
1794
  .Explode => Effects.explode(battle, player),
@@ -1800,7 +1800,7 @@ inline fn alwaysHappens(
1800
1800
  };
1801
1801
  }
1802
1802
 
1803
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1803
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1804
1804
  return switch (move.effect) {
1805
1805
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1806
1806
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1824,10 +1824,12 @@ pub const Effects = struct {
1824
1824
 
1825
1825
  side.active.volatiles.Bide = true;
1826
1826
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1827
+
1827
1828
  side.active.volatiles.state = 0;
1828
1829
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1830
+ options.chance.observe(.attacking, player, .started);
1829
1831
 
1830
- try options.log.start(battle.active(player), .Bide);
1832
+ try options.log.start(.{ battle.active(player), .Bide });
1831
1833
  }
1832
1834
 
1833
1835
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1841,11 +1843,11 @@ pub const Effects = struct {
1841
1843
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1842
1844
  if (Status.is(foe_stored.status, .FRZ)) {
1843
1845
  assert(move.type == .Fire);
1844
- try options.log.curestatus(
1846
+ try options.log.curestatus(.{
1845
1847
  battle.active(player.foe()),
1846
1848
  foe_stored.status,
1847
1849
  .Message,
1848
- );
1850
+ });
1849
1851
  foe_stored.status = 0;
1850
1852
  }
1851
1853
  return;
@@ -1859,7 +1861,7 @@ pub const Effects = struct {
1859
1861
  foe_stored.status = Status.init(.BRN);
1860
1862
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1861
1863
 
1862
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1864
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1863
1865
  }
1864
1866
 
1865
1867
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1869,8 +1871,8 @@ pub const Effects = struct {
1869
1871
  volatiles.Charging = true;
1870
1872
  const move = side.last_selected_move;
1871
1873
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1872
- try options.log.laststill();
1873
- try options.log.prepare(battle.active(player), move);
1874
+ try options.log.laststill(.{});
1875
+ try options.log.prepare(.{ battle.active(player), move });
1874
1876
  }
1875
1877
 
1876
1878
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1881,42 +1883,39 @@ pub const Effects = struct {
1881
1883
  if (!try Rolls.confusionChance(battle, player, options)) return;
1882
1884
  } else {
1883
1885
  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
- }
1886
+ if (!try checkHit(battle, player, move, options)) return;
1887
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1889
1888
  } 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
- }
1889
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1890
+ if (!try checkHit(battle, player, move, options)) return;
1895
1891
  }
1896
- try options.chance.commit(player, .hit);
1897
1892
  }
1898
1893
 
1899
1894
  if (foe.active.volatiles.Confusion) return;
1900
1895
  foe.active.volatiles.Confusion = true;
1901
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1902
1896
 
1903
- try options.log.start(battle.active(player.foe()), .Confusion);
1897
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1898
+ options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1900
+
1901
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1904
1902
  }
1905
1903
 
1906
1904
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1907
1905
  const foe = battle.foe(player);
1908
1906
 
1909
1907
  if (foe.active.volatiles.Invulnerable) {
1910
- try options.log.lastmiss();
1911
- return options.log.miss(battle.active(player));
1908
+ try options.log.lastmiss(.{});
1909
+ return options.log.miss(.{battle.active(player)});
1912
1910
  }
1913
1911
 
1914
1912
  battle.side(player).active.types = foe.active.types;
1915
- return options.log.typechange(
1913
+ return options.log.start(.{
1916
1914
  battle.active(player),
1915
+ .TypeChange,
1917
1916
  foe.active.types,
1918
1917
  battle.active(player.foe()),
1919
- );
1918
+ });
1920
1919
  }
1921
1920
 
1922
1921
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1925,13 +1924,10 @@ pub const Effects = struct {
1925
1924
  const foe_ident = battle.active(player.foe());
1926
1925
 
1927
1926
  // 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
- }
1927
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1932
1928
 
1933
- if (volatiles.disabled_move != 0) {
1934
- try options.log.fail(foe_ident, .None);
1929
+ if (volatiles.disable_move != 0) {
1930
+ try options.log.fail(.{ foe_ident, .None });
1935
1931
  return null;
1936
1932
  }
1937
1933
 
@@ -1946,27 +1942,27 @@ pub const Effects = struct {
1946
1942
 
1947
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1948
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1949
- if (n == 0) {
1950
- try options.log.fail(foe_ident, .None);
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1946
+ try options.log.fail(.{ foe_ident, .None });
1951
1947
  return null;
1952
1948
  } else if (err) {
1953
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1954
- assert(!showdown);
1955
- return Result.Error;
1950
+ return .Error;
1956
1951
  }
1957
1952
 
1958
- volatiles.disabled_move =
1953
+ volatiles.disable_move =
1959
1954
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1960
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1955
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1956
+ options.chance.observe(.disable, player.foe(), .started);
1961
1957
 
1962
- const id = foe.active.move(volatiles.disabled_move).id;
1963
- try options.log.startEffect(foe_ident, .Disable, id);
1958
+ const id = foe.active.move(volatiles.disable_move).id;
1959
+ try options.log.start(.{ foe_ident, .Disable, id });
1964
1960
  return null;
1965
1961
  }
1966
1962
 
1967
1963
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1968
- var side = battle.side(player);
1969
- var stored = side.stored();
1964
+ var stored = battle.side(player).stored();
1965
+ const ident = battle.active(player);
1970
1966
 
1971
1967
  if (battle.last_damage == 0) {
1972
1968
  assert(showdown);
@@ -1979,7 +1975,7 @@ pub const Effects = struct {
1979
1975
  if (stored.hp == stored.stats.hp) return;
1980
1976
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1981
1977
 
1982
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1978
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1983
1979
  }
1984
1980
 
1985
1981
  fn explode(battle: anytype, player: Player) !void {
@@ -2011,7 +2007,7 @@ pub const Effects = struct {
2011
2007
  if (side.active.volatiles.FocusEnergy) return;
2012
2008
  side.active.volatiles.FocusEnergy = true;
2013
2009
 
2014
- try options.log.start(battle.active(player), .FocusEnergy);
2010
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
2015
2011
  }
2016
2012
 
2017
2013
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2028,12 +2024,12 @@ pub const Effects = struct {
2028
2024
  if (foe.active.types.includes(move.type)) return;
2029
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2030
2026
  // Freeze Clause Mod
2031
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2027
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2032
2028
 
2033
2029
  foe_stored.status = Status.init(.FRZ);
2034
2030
  // GLITCH: Hyper Beam recharging status is not cleared
2035
2031
 
2036
- try options.log.status(foe_ident, foe_stored.status, .None);
2032
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2037
2033
  }
2038
2034
 
2039
2035
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2041,7 +2037,7 @@ pub const Effects = struct {
2041
2037
  var side = battle.side(player);
2042
2038
  var foe = battle.foe(player);
2043
2039
 
2044
- var side_stored = side.stored();
2040
+ const side_stored = side.stored();
2045
2041
  var foe_stored = foe.stored();
2046
2042
 
2047
2043
  const player_ident = battle.active(player);
@@ -2053,8 +2049,8 @@ pub const Effects = struct {
2053
2049
  side.active.stats = side_stored.stats;
2054
2050
  foe.active.stats = foe_stored.stats;
2055
2051
 
2056
- try log.activate(player_ident, .Haze);
2057
- try log.clearallboost();
2052
+ try log.activate(.{ player_ident, .Haze });
2053
+ try log.clearallboost(.{});
2058
2054
 
2059
2055
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2060
2056
  if (showdown) {
@@ -2062,24 +2058,26 @@ pub const Effects = struct {
2062
2058
  const p: Player = @enumFromInt(i);
2063
2059
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2064
2060
  if (p != player and Status.any(s.stored().status)) {
2065
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2061
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2066
2062
  s.stored().status = 0;
2063
+ options.chance.observe(.sleep, p, .None);
2067
2064
  } else if (showdown and s.stored().status == Status.TOX) {
2068
2065
  s.stored().status = Status.init(.PSN);
2069
- try log.status(battle.active(p), s.stored().status, .None);
2066
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2070
2067
  }
2071
- try clearVolatiles(battle, p, p != player, options);
2068
+ try clearVolatiles(battle, p, options);
2072
2069
  }
2073
2070
  } else {
2074
2071
  if (Status.any(foe_stored.status)) {
2075
2072
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2076
2073
  foe.last_selected_move = .SKIP_TURN;
2074
+ options.chance.observe(.sleep, player.foe(), .None);
2077
2075
  }
2078
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2076
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2079
2077
  foe_stored.status = 0;
2080
2078
  }
2081
- try clearVolatiles(battle, player, false, options);
2082
- try clearVolatiles(battle, player.foe(), true, options);
2079
+ try clearVolatiles(battle, player, options);
2080
+ try clearVolatiles(battle, player.foe(), options);
2083
2081
  }
2084
2082
  }
2085
2083
 
@@ -2091,7 +2089,7 @@ pub const Effects = struct {
2091
2089
  // GLITCH: HP recovery move failure glitches
2092
2090
  const delta = stored.stats.hp - stored.hp;
2093
2091
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2094
- return try options.log.fail(ident, .None);
2092
+ return options.log.fail(.{ ident, .None });
2095
2093
  }
2096
2094
 
2097
2095
  const rest = side.last_selected_move == .Rest;
@@ -2099,17 +2097,17 @@ pub const Effects = struct {
2099
2097
  // Adding the sleep status runs the sleep condition handler to roll duration
2100
2098
  if (showdown) battle.rng.advance(1);
2101
2099
  stored.status = Status.slf(2);
2102
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2100
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2103
2101
  stored.hp = stored.stats.hp;
2104
2102
  } else {
2105
2103
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2106
2104
  }
2107
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2105
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2108
2106
  }
2109
2107
 
2110
2108
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2111
2109
  battle.side(player).active.volatiles.Recharging = true;
2112
- try options.log.mustrecharge(battle.active(player));
2110
+ try options.log.mustrecharge(.{battle.active(player)});
2113
2111
  }
2114
2112
 
2115
2113
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2118,33 +2116,31 @@ pub const Effects = struct {
2118
2116
  if (showdown) {
2119
2117
  // Invulnerability trumps type immunity on Pokémon Showdown
2120
2118
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2121
- return options.log.immune(battle.active(player.foe()), .None);
2119
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2122
2120
  }
2123
2121
  if (!try checkHit(battle, player, move, options)) return;
2124
2122
  if (foe.active.volatiles.LeechSeed) return;
2125
2123
  } else {
2126
2124
  if (!try checkHit(battle, player, move, options)) return;
2127
2125
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2128
- try options.log.lastmiss();
2129
- return options.log.miss(battle.active(player));
2126
+ try options.log.lastmiss(.{});
2127
+ return options.log.miss(.{battle.active(player)});
2130
2128
  }
2131
2129
  }
2132
2130
 
2133
- try options.chance.commit(player, .hit);
2134
2131
  foe.active.volatiles.LeechSeed = true;
2135
2132
 
2136
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2133
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2137
2134
  }
2138
2135
 
2139
2136
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2140
2137
  var side = battle.side(player);
2138
+ const ident = battle.active(player);
2141
2139
 
2142
- if (side.active.volatiles.LightScreen) {
2143
- return options.log.fail(battle.active(player), .None);
2144
- }
2140
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2145
2141
  side.active.volatiles.LightScreen = true;
2146
2142
 
2147
- try options.log.start(battle.active(player), .LightScreen);
2143
+ try options.log.start(.{ ident, .LightScreen });
2148
2144
  }
2149
2145
 
2150
2146
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2172,17 +2168,15 @@ pub const Effects = struct {
2172
2168
  if (!has_mimic) {
2173
2169
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2174
2170
  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);
2171
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2177
2172
  }
2178
2173
  }
2179
2174
  if (!try checkHit(battle, player, move, options)) return;
2180
- try options.chance.commit(player, .hit);
2181
2175
 
2182
2176
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2183
2177
  side.active.move(oslot).id = foe.active.move(rslot).id;
2184
2178
 
2185
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2179
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2186
2180
  }
2187
2181
 
2188
2182
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2191,7 +2185,7 @@ pub const Effects = struct {
2191
2185
  if (side.active.volatiles.Mist) return;
2192
2186
  side.active.volatiles.Mist = true;
2193
2187
 
2194
- try options.log.start(battle.active(player), .Mist);
2188
+ try options.log.start(.{ battle.active(player), .Mist });
2195
2189
  }
2196
2190
 
2197
2191
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2217,25 +2211,25 @@ pub const Effects = struct {
2217
2211
 
2218
2212
  if (showdown) {
2219
2213
  // 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;
2214
+ if (immune and !foe.active.volatiles.Invulnerable) {
2215
+ return log.immune(.{ foe_ident, .None });
2216
+ } else if (!try checkHit(battle, player, move, options)) return;
2222
2217
  }
2223
2218
  if (Status.any(foe_stored.status)) {
2224
- return log.fail(
2219
+ return log.fail(.{
2225
2220
  foe_ident,
2226
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2227
- );
2221
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2222
+ });
2228
2223
  }
2229
2224
  if (!showdown) {
2230
- if (immune) return log.immune(foe_ident, .None);
2225
+ if (immune) return log.immune(.{ foe_ident, .None });
2231
2226
  if (!try checkHit(battle, player, move, options)) return;
2232
2227
  }
2233
- try options.chance.commit(player, .hit);
2234
2228
 
2235
2229
  foe_stored.status = Status.init(.PAR);
2236
2230
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2237
2231
 
2238
- try log.status(foe_ident, foe_stored.status, .None);
2232
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2239
2233
  }
2240
2234
 
2241
2235
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2257,12 +2251,12 @@ pub const Effects = struct {
2257
2251
  foe_stored.status = Status.init(.PAR);
2258
2252
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2259
2253
 
2260
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2254
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2261
2255
  }
2262
2256
 
2263
2257
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2264
2258
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2265
- try options.log.fieldactivate();
2259
+ try options.log.fieldactivate(.{});
2266
2260
  }
2267
2261
  }
2268
2262
 
@@ -2277,28 +2271,27 @@ pub const Effects = struct {
2277
2271
  return;
2278
2272
  } else if (foe.active.volatiles.Substitute) {
2279
2273
  if (move.effect != .Poison) return;
2280
- return log.fail(foe_ident, .None);
2274
+ return log.fail(.{ foe_ident, .None });
2281
2275
  } else if (Status.any(foe_stored.status)) {
2282
2276
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2283
2277
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2284
2278
  // 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))
2279
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2286
2280
  if (!showdown)
2287
- .Poison
2281
+ Fail.Poison
2288
2282
  else if (toxic == (foe_stored.status == Status.TOX))
2289
- if (toxic) .Toxic else .Poison
2283
+ if (toxic) Fail.Toxic else Fail.Poison
2290
2284
  else
2291
- .None
2285
+ Fail.None
2292
2286
  else
2293
- .None);
2287
+ Fail.None });
2294
2288
  } else if (foe.active.types.includes(.Poison)) {
2295
2289
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2296
- return log.immune(foe_ident, .None);
2290
+ return log.immune(.{ foe_ident, .None });
2297
2291
  }
2298
2292
 
2299
2293
  if (move.effect == .Poison) {
2300
2294
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2301
- try options.chance.commit(player, .hit);
2302
2295
  } else {
2303
2296
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2304
2297
  return;
@@ -2312,7 +2305,7 @@ pub const Effects = struct {
2312
2305
  foe.active.volatiles.toxic = 0;
2313
2306
  }
2314
2307
 
2315
- try log.status(foe_ident, foe_stored.status, .None);
2308
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2316
2309
  }
2317
2310
 
2318
2311
  fn rage(battle: anytype, player: Player) !void {
@@ -2332,22 +2325,23 @@ pub const Effects = struct {
2332
2325
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2333
2326
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2334
2327
 
2335
- try options.log.damageOf(
2328
+ try options.log.damage(.{
2336
2329
  battle.active(player),
2337
2330
  stored,
2338
2331
  .RecoilOf,
2339
2332
  battle.active(player.foe()),
2340
- );
2333
+ });
2341
2334
  if (showdown and stored.hp == 0) residual.* = false;
2342
2335
  }
2343
2336
 
2344
2337
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2345
2338
  var side = battle.side(player);
2339
+ const ident = battle.active(player);
2346
2340
 
2347
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2341
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2348
2342
  side.active.volatiles.Reflect = true;
2349
2343
 
2350
- try options.log.start(battle.active(player), .Reflect);
2344
+ try options.log.start(.{ ident, .Reflect });
2351
2345
  }
2352
2346
 
2353
2347
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2361,18 +2355,17 @@ pub const Effects = struct {
2361
2355
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2362
2356
  } else {
2363
2357
  if (Status.any(foe_stored.status)) {
2364
- return options.log.fail(
2358
+ return options.log.fail(.{
2365
2359
  foe_ident,
2366
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2367
- );
2360
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2361
+ });
2368
2362
  }
2369
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2363
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2370
2364
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2371
- try options.chance.commit(player, .hit);
2372
2365
  }
2373
2366
 
2374
2367
  // Sleep Clause Mod
2375
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2376
2369
  for (foe.pokemon) |p| {
2377
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2378
2371
  }
@@ -2380,19 +2373,20 @@ pub const Effects = struct {
2380
2373
  foe.active.volatiles.Recharging = false;
2381
2374
 
2382
2375
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2376
+ options.chance.observe(.sleep, player.foe(), .started);
2377
+
2383
2378
  const last = battle.side(player).last_selected_move;
2384
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2379
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2385
2380
  }
2386
2381
 
2387
2382
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2388
- try options.log.activate(battle.active(player), .Splash);
2383
+ try options.log.activate(.{ battle.active(player), .Splash });
2389
2384
  }
2390
2385
 
2391
2386
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2392
2387
  var side = battle.side(player);
2393
2388
  if (side.active.volatiles.Substitute) {
2394
- try options.log.fail(battle.active(player), .Substitute);
2395
- return;
2389
+ return options.log.fail(.{ battle.active(player), .Substitute });
2396
2390
  }
2397
2391
 
2398
2392
  assert(side.stored().stats.hp <= 1023);
@@ -2404,28 +2398,30 @@ pub const Effects = struct {
2404
2398
  // avoid having to convert to floating point)
2405
2399
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2406
2400
  if (side.stored().hp < required_hp) {
2407
- try options.log.fail(battle.active(player), .Weak);
2408
- return;
2401
+ return options.log.fail(.{ battle.active(player), .Weak });
2409
2402
  }
2410
2403
 
2411
2404
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2412
2405
  side.stored().hp -= hp;
2413
2406
  side.active.volatiles.substitute = hp + 1;
2414
2407
  side.active.volatiles.Substitute = true;
2415
- try options.log.start(battle.active(player), .Substitute);
2408
+ try options.log.start(.{ battle.active(player), .Substitute });
2416
2409
  if (hp > 0) {
2417
- try options.log.damage(battle.active(player), side.stored(), .None);
2410
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2418
2411
  if (showdown and side.stored().hp == 0) residual.* = false;
2419
2412
  }
2420
2413
  }
2421
2414
 
2422
2415
  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
2416
  // 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;
2417
+ if (showdown) {
2418
+ if (battle.side(player).last_selected_move == .Teleport) return;
2419
+ _ = try checkHit(battle, player, move, options);
2420
+ battle.last_damage = 0;
2421
+ } else {
2422
+ try options.log.fail(.{ battle.active(player), .None });
2423
+ try options.log.laststill(.{});
2424
+ }
2429
2425
  }
2430
2426
 
2431
2427
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2435,6 +2431,8 @@ pub const Effects = struct {
2435
2431
 
2436
2432
  volatiles.Thrashing = true;
2437
2433
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2434
+
2435
+ options.chance.observe(.attacking, player, .started);
2438
2436
  }
2439
2437
 
2440
2438
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2463,7 +2461,7 @@ pub const Effects = struct {
2463
2461
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2464
2462
  }
2465
2463
 
2466
- try options.log.transform(battle.active(player), foe_ident);
2464
+ try options.log.transform(.{ battle.active(player), foe_ident });
2467
2465
  }
2468
2466
 
2469
2467
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2473,13 +2471,14 @@ pub const Effects = struct {
2473
2471
  if (side.active.volatiles.Binding) return;
2474
2472
  side.active.volatiles.Binding = true;
2475
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2476
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2477
2475
  if (!showdown) {
2478
2476
  foe.active.volatiles.Recharging = false;
2479
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2480
2478
 
2481
2479
  side.active.volatiles.attacks =
2482
2480
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2481
+ options.chance.observe(.binding, player, .started);
2483
2482
  }
2484
2483
 
2485
2484
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2493,87 +2492,87 @@ pub const Effects = struct {
2493
2492
  switch (move.effect) {
2494
2493
  .AttackUp1, .AttackUp2, .Rage => {
2495
2494
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2496
- if (boosts.atk == 6) return try log.fail(ident, .None);
2495
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2497
2496
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2498
2497
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2499
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2498
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2500
2499
  if (stats.atk == MAX_STAT_VALUE) {
2501
2500
  boosts.atk -= 1;
2502
2501
  if (showdown) {
2503
- try log.boost(ident, reason, n);
2504
- try log.boost(ident, Boost.Attack, -1);
2502
+ try log.boost(.{ ident, reason, n });
2503
+ try log.boost(.{ ident, .Attack, -1 });
2505
2504
  if (move.effect == .Rage) return;
2506
2505
  }
2507
- return try log.fail(ident, .None);
2506
+ return log.fail(.{ ident, .None });
2508
2507
  }
2509
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2508
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2510
2509
  const stat = unmodifiedStats(battle, side).atk;
2511
2510
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2512
- try log.boost(ident, reason, n);
2511
+ try log.boost(.{ ident, reason, n });
2513
2512
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2514
2513
  if (showdown and move.effect == .Rage) return;
2515
2514
  },
2516
2515
  .DefenseUp1, .DefenseUp2 => {
2517
2516
  assert(boosts.def >= -6 and boosts.def <= 6);
2518
- if (boosts.def == 6) return try log.fail(ident, .None);
2517
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2519
2518
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2520
2519
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2521
2520
  if (stats.def == MAX_STAT_VALUE) {
2522
2521
  boosts.def -= 1;
2523
2522
  if (showdown) {
2524
- try log.boost(ident, .Defense, n);
2525
- try log.boost(ident, .Defense, -1);
2523
+ try log.boost(.{ ident, .Defense, n });
2524
+ try log.boost(.{ ident, .Defense, -1 });
2526
2525
  }
2527
- return try log.fail(ident, .None);
2526
+ return log.fail(.{ ident, .None });
2528
2527
  }
2529
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2528
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2530
2529
  const stat = unmodifiedStats(battle, side).def;
2531
2530
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2532
- try log.boost(ident, .Defense, n);
2531
+ try log.boost(.{ ident, .Defense, n });
2533
2532
  },
2534
2533
  .SpeedUp2 => {
2535
2534
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2536
- if (boosts.spe == 6) return try log.fail(ident, .None);
2535
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2537
2536
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2538
2537
  if (stats.spe == MAX_STAT_VALUE) {
2539
2538
  boosts.spe -= 1;
2540
2539
  if (showdown) {
2541
- try log.boost(ident, .Speed, 2);
2542
- try log.boost(ident, .Speed, -1);
2540
+ try log.boost(.{ ident, .Speed, 2 });
2541
+ try log.boost(.{ ident, .Speed, -1 });
2543
2542
  }
2544
- return try log.fail(ident, .None);
2543
+ return log.fail(.{ ident, .None });
2545
2544
  }
2546
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2545
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2547
2546
  const stat = unmodifiedStats(battle, side).spe;
2548
2547
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2549
- try log.boost(ident, .Speed, 2);
2548
+ try log.boost(.{ ident, .Speed, 2 });
2550
2549
  },
2551
2550
  .SpecialUp1, .SpecialUp2 => {
2552
2551
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2553
- if (boosts.spc == 6) return try log.fail(ident, .None);
2552
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2554
2553
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2555
2554
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2556
2555
  if (stats.spc == MAX_STAT_VALUE) {
2557
2556
  boosts.spc -= 1;
2558
2557
  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);
2558
+ try log.boost(.{ ident, .SpecialAttack, n });
2559
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2560
+ try log.boost(.{ ident, .SpecialDefense, n });
2561
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2563
2562
  }
2564
- return try log.fail(ident, .None);
2563
+ return log.fail(.{ ident, .None });
2565
2564
  }
2566
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2565
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2567
2566
  const stat = unmodifiedStats(battle, side).spc;
2568
2567
  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);
2568
+ try log.boost(.{ ident, .SpecialAttack, n });
2569
+ try log.boost(.{ ident, .SpecialDefense, n });
2571
2570
  },
2572
2571
  .EvasionUp1 => {
2573
2572
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2574
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2573
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2575
2574
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2576
- try log.boost(ident, .Evasion, 1);
2575
+ try log.boost(.{ ident, .Evasion, 1 });
2577
2576
  },
2578
2577
  else => unreachable,
2579
2578
  }
@@ -2588,7 +2587,7 @@ pub const Effects = struct {
2588
2587
  const foe_ident = battle.active(player.foe());
2589
2588
  const secondary = move.effect.isStatDownChance();
2590
2589
 
2591
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2590
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2592
2591
 
2593
2592
  if (secondary) {
2594
2593
  const proc = try Rolls.unboost(battle, player, options);
@@ -2596,7 +2595,6 @@ pub const Effects = struct {
2596
2595
  } else {
2597
2596
  // checkHit already checks for Invulnerable
2598
2597
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2599
- try options.chance.commit(player, .hit);
2600
2598
  }
2601
2599
 
2602
2600
  var stats = &foe.active.stats;
@@ -2606,82 +2604,82 @@ pub const Effects = struct {
2606
2604
  switch (move.effect) {
2607
2605
  .AttackDown1, .AttackDownChance => {
2608
2606
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2609
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2607
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2610
2608
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2611
2609
  if (stats.atk == 1) {
2612
2610
  boosts.atk += 1;
2613
2611
  if (showdown) {
2614
- try log.boost(foe_ident, .Attack, -1);
2615
- try log.boost(foe_ident, .Attack, 1);
2612
+ try log.boost(.{ foe_ident, .Attack, -1 });
2613
+ try log.boost(.{ foe_ident, .Attack, 1 });
2616
2614
  }
2617
- return try log.fail(foe_ident, .None);
2615
+ return log.fail(.{ foe_ident, .None });
2618
2616
  }
2619
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2617
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2620
2618
  const stat = unmodifiedStats(battle, foe).atk;
2621
2619
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2622
- try log.boost(foe_ident, .Attack, -1);
2620
+ try log.boost(.{ foe_ident, .Attack, -1 });
2623
2621
  },
2624
2622
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2625
2623
  assert(boosts.def >= -6 and boosts.def <= 6);
2626
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2624
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2627
2625
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2628
2626
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2629
2627
  if (stats.def == 1) {
2630
2628
  boosts.def += 1;
2631
2629
  if (showdown) {
2632
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2633
- try log.boost(foe_ident, .Defense, 1);
2630
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2631
+ try log.boost(.{ foe_ident, .Defense, 1 });
2634
2632
  }
2635
- return try log.fail(foe_ident, .None);
2633
+ return log.fail(.{ foe_ident, .None });
2636
2634
  }
2637
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2635
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2638
2636
  const stat = unmodifiedStats(battle, foe).def;
2639
2637
  stats.def = @max(1, stat * mod[0] / mod[1]);
2640
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2638
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2641
2639
  },
2642
2640
  .SpeedDown1, .SpeedDownChance => {
2643
2641
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2644
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2642
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2645
2643
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2646
2644
  if (stats.spe == 1) {
2647
2645
  boosts.spe += 1;
2648
2646
  if (showdown) {
2649
- try log.boost(foe_ident, .Speed, -1);
2650
- try log.boost(foe_ident, .Speed, 1);
2647
+ try log.boost(.{ foe_ident, .Speed, -1 });
2648
+ try log.boost(.{ foe_ident, .Speed, 1 });
2651
2649
  }
2652
- return try log.fail(foe_ident, .None);
2650
+ return log.fail(.{ foe_ident, .None });
2653
2651
  }
2654
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2652
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2655
2653
  const stat = unmodifiedStats(battle, foe).spe;
2656
2654
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2657
- try log.boost(foe_ident, .Speed, -1);
2655
+ try log.boost(.{ foe_ident, .Speed, -1 });
2658
2656
  assert(boosts.spe >= -6);
2659
2657
  },
2660
2658
  .SpecialDownChance => {
2661
2659
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2662
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2660
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2663
2661
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2664
2662
  if (stats.spc == 1) {
2665
2663
  boosts.spc += 1;
2666
2664
  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);
2665
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2666
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2667
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2668
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2671
2669
  }
2672
- return try log.fail(foe_ident, .None);
2670
+ return log.fail(.{ foe_ident, .None });
2673
2671
  }
2674
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2672
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2675
2673
  const stat = unmodifiedStats(battle, foe).spc;
2676
2674
  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);
2675
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2676
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2679
2677
  },
2680
2678
  .AccuracyDown1 => {
2681
2679
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2682
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2680
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2683
2681
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2684
- try log.boost(foe_ident, .Accuracy, -1);
2682
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2685
2683
  },
2686
2684
  else => unreachable,
2687
2685
  }
@@ -2705,40 +2703,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2705
2703
  }
2706
2704
  }
2707
2705
 
2708
- inline fn isForced(active: ActivePokemon) bool {
2706
+ fn isForced(active: anytype) bool {
2709
2707
  return active.volatiles.Recharging or active.volatiles.Rage or
2710
2708
  active.volatiles.Thrashing or active.volatiles.Charging;
2711
2709
  }
2712
2710
 
2713
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2711
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2714
2712
  var log = options.log;
2715
2713
  var side = battle.side(who);
2716
2714
  var volatiles = &side.active.volatiles;
2717
2715
  const ident = battle.active(who);
2718
2716
 
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);
2717
+ if (volatiles.disable_move != 0) {
2718
+ volatiles.disable_move = 0;
2719
+ volatiles.disable_duration = 0;
2720
+ options.chance.observe(.disable, who, .None);
2721
+ try log.end(.{ ident, .DisableSilent });
2725
2722
  }
2726
2723
  if (volatiles.Confusion) {
2727
2724
  // volatiles.confusion is left unchanged
2728
2725
  volatiles.Confusion = false;
2729
- try log.end(ident, .ConfusionSilent);
2726
+ options.chance.observe(.confusion, who, .None);
2727
+ try log.end(.{ ident, .ConfusionSilent });
2730
2728
  }
2731
2729
  if (volatiles.Mist) {
2732
2730
  volatiles.Mist = false;
2733
- try log.end(ident, .Mist);
2731
+ try log.end(.{ ident, .MistSilent });
2734
2732
  }
2735
2733
  if (volatiles.FocusEnergy) {
2736
2734
  volatiles.FocusEnergy = false;
2737
- try log.end(ident, .FocusEnergy);
2735
+ try log.end(.{ ident, .FocusEnergySilent });
2738
2736
  }
2739
2737
  if (volatiles.LeechSeed) {
2740
2738
  volatiles.LeechSeed = false;
2741
- try log.end(ident, .LeechSeed);
2739
+ try log.end(.{ ident, .LeechSeedSilent });
2742
2740
  }
2743
2741
  if (!showdown and volatiles.Toxic) {
2744
2742
  volatiles.Toxic = false;
@@ -2746,24 +2744,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2746
2744
  }
2747
2745
  if (volatiles.LightScreen) {
2748
2746
  volatiles.LightScreen = false;
2749
- try log.end(ident, .LightScreen);
2747
+ try log.end(.{ ident, .LightScreenSilent });
2750
2748
  }
2751
2749
  if (volatiles.Reflect) {
2752
2750
  volatiles.Reflect = false;
2753
- try log.end(ident, .Reflect);
2751
+ try log.end(.{ ident, .ReflectSilent });
2754
2752
  }
2755
2753
  if (showdown and volatiles.Toxic) {
2756
2754
  volatiles.Toxic = false;
2757
2755
  // Pokémon Showdown erroneously clears the toxic counter
2758
2756
  volatiles.toxic = 0;
2759
- try log.end(ident, .Toxic);
2757
+ try log.end(.{ ident, .ToxicSilent });
2760
2758
  }
2761
2759
  }
2762
2760
 
2761
+ fn decrement(
2762
+ comptime field: chance.Action.Field,
2763
+ player: Player,
2764
+ options: anytype,
2765
+ n: anytype,
2766
+ ) @TypeOf(n) {
2767
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2768
+ .started, .ended => 0,
2769
+ .continuing => if (n > 1) n - 1 else n,
2770
+ else => unreachable,
2771
+ } else n - 1;
2772
+ }
2773
+
2763
2774
  pub const Rolls = struct {
2764
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2775
+ fn speedTie(battle: anytype, options: anytype) !bool {
2765
2776
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2766
2777
  player == .P1
2778
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2779
+ battle.rng.speedTie()
2767
2780
  else if (showdown)
2768
2781
  battle.rng.range(u8, 0, 2) == 0
2769
2782
  else
@@ -2773,21 +2786,31 @@ pub const Rolls = struct {
2773
2786
  return p1;
2774
2787
  }
2775
2788
 
2776
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2789
+ fn criticalHit(
2790
+ battle: anytype,
2791
+ player: Player,
2792
+ rate: u8,
2793
+ spurious: bool,
2794
+ options: anytype,
2795
+ ) !bool {
2777
2796
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2778
2797
  val == .true
2798
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2799
+ battle.rng.criticalHit(player, rate)
2779
2800
  else if (showdown)
2780
2801
  battle.rng.chance(u8, rate, 256)
2781
2802
  else
2782
2803
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2783
2804
 
2784
- try options.chance.criticalHit(player, crit, rate);
2805
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2785
2806
  return crit;
2786
2807
  }
2787
2808
 
2788
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2809
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2789
2810
  const roll = if (options.calc.overridden(player, .damage)) |val|
2790
2811
  val
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2813
+ battle.rng.damage(player)
2791
2814
  else roll: {
2792
2815
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2793
2816
  while (true) {
@@ -2796,25 +2819,32 @@ pub const Rolls = struct {
2796
2819
  }
2797
2820
  };
2798
2821
 
2822
+ assert(roll >= 217 and roll <= 255);
2799
2823
  try options.chance.damage(player, roll);
2800
2824
  return roll;
2801
2825
  }
2802
2826
 
2803
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2827
+ const uN = if (pkmn.options.miss) u8 else u9;
2828
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2829
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2804
2830
  const ok = if (options.calc.overridden(player, .hit)) |val|
2805
2831
  val == .true
2832
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2833
+ battle.rng.hit(player, acc)
2806
2834
  else if (showdown)
2807
- battle.rng.chance(u8, accuracy, 256)
2835
+ battle.rng.chance(uN, acc, 256)
2808
2836
  else
2809
- battle.rng.next() < accuracy;
2837
+ battle.rng.next() < acc;
2810
2838
 
2811
- options.chance.hit(ok, accuracy);
2839
+ try options.chance.hit(player, ok, acc);
2812
2840
  return ok;
2813
2841
  }
2814
2842
 
2815
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2843
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2816
2844
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2817
2845
  val == .true
2846
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2847
+ battle.rng.confused(player)
2818
2848
  else if (showdown)
2819
2849
  !battle.rng.chance(u8, 128, 256)
2820
2850
  else
@@ -2824,9 +2854,11 @@ pub const Rolls = struct {
2824
2854
  return cfz;
2825
2855
  }
2826
2856
 
2827
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2857
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2828
2858
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2829
2859
  val == .true
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2861
+ battle.rng.paralyzed(player)
2830
2862
  else if (showdown)
2831
2863
  battle.rng.chance(u8, 63, 256)
2832
2864
  else
@@ -2836,9 +2868,11 @@ pub const Rolls = struct {
2836
2868
  return par;
2837
2869
  }
2838
2870
 
2839
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2871
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2840
2872
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2841
2873
  val == .true
2874
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2875
+ battle.rng.secondaryChance(player, 25)
2842
2876
  else if (showdown)
2843
2877
  battle.rng.chance(u8, 25, 256)
2844
2878
  else
@@ -2848,11 +2882,13 @@ pub const Rolls = struct {
2848
2882
  return proc;
2849
2883
  }
2850
2884
 
2851
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2885
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2852
2886
  const rate: u8 = if (low) 26 else 77;
2853
2887
 
2854
2888
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2855
2889
  val == .true
2890
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2891
+ battle.rng.secondaryChance(player, rate)
2856
2892
  else if (showdown)
2857
2893
  battle.rng.chance(u8, rate, 256)
2858
2894
  else
@@ -2862,11 +2898,13 @@ pub const Rolls = struct {
2862
2898
  return proc;
2863
2899
  }
2864
2900
 
2865
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2901
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2866
2902
  const rate: u8 = if (low) 52 else 103;
2867
2903
 
2868
2904
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2869
2905
  val == .true
2906
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2907
+ battle.rng.secondaryChance(player, rate)
2870
2908
  else if (showdown)
2871
2909
  battle.rng.chance(u8, rate, 256)
2872
2910
  else
@@ -2876,9 +2914,11 @@ pub const Rolls = struct {
2876
2914
  return proc;
2877
2915
  }
2878
2916
 
2879
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2917
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2880
2918
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2881
2919
  val == .true
2920
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2921
+ battle.rng.secondaryChance(player, 85)
2882
2922
  else if (showdown)
2883
2923
  battle.rng.chance(u8, 85, 256)
2884
2924
  else
@@ -2888,14 +2928,14 @@ pub const Rolls = struct {
2888
2928
  return proc;
2889
2929
  }
2890
2930
 
2891
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2931
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2892
2932
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2893
2933
  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: {
2934
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2935
+ battle.rng.metronome(player)
2936
+ else if (showdown)
2937
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2938
+ else move: {
2899
2939
  while (true) {
2900
2940
  const r = battle.rng.next();
2901
2941
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2908,9 +2948,11 @@ pub const Rolls = struct {
2908
2948
  return move;
2909
2949
  }
2910
2950
 
2911
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2951
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2912
2952
  const power = if (options.calc.overridden(player, .psywave)) |val|
2913
2953
  val - 1
2954
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2955
+ battle.rng.psywave(player)
2914
2956
  else if (showdown)
2915
2957
  battle.rng.range(u8, 0, max)
2916
2958
  else power: {
@@ -2920,15 +2962,19 @@ pub const Rolls = struct {
2920
2962
  }
2921
2963
  };
2922
2964
 
2965
+ assert(power < max);
2923
2966
  try options.chance.psywave(player, power, max);
2924
2967
  return power;
2925
2968
  }
2926
2969
 
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)
2970
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2971
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2972
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2973
+ else
2974
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2975
+ battle.rng.sleepDuration(player)
2930
2976
  else if (showdown)
2931
- @intCast(battle.rng.range(u8, 1, 8))
2977
+ battle.rng.range(u3, 1, 8)
2932
2978
  else duration: {
2933
2979
  while (true) {
2934
2980
  const r = battle.rng.next() & 7;
@@ -2936,64 +2982,83 @@ pub const Rolls = struct {
2936
2982
  }
2937
2983
  };
2938
2984
 
2939
- options.chance.duration(.sleep, player, player.foe(), duration);
2985
+ assert(duration >= 1 and duration <= 7);
2986
+ options.chance.duration(player, duration);
2940
2987
  return duration;
2941
2988
  }
2942
2989
 
2943
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2944
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2945
- val
2990
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2991
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2992
+ if (val >= 1 and val <= 8) val else null
2993
+ else
2994
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2995
+ battle.rng.disableDuration(player)
2946
2996
  else if (showdown)
2947
- @intCast(battle.rng.range(u8, 1, 9))
2997
+ battle.rng.range(u4, 1, 9)
2948
2998
  else
2949
2999
  @intCast((battle.rng.next() & 7) + 1);
2950
3000
 
2951
- options.chance.duration(.disable, player, player.foe(), duration);
3001
+ assert(duration >= 1 and duration <= 8);
3002
+ options.chance.duration(player, duration);
2952
3003
  return duration;
2953
3004
  }
2954
3005
 
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)
3006
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3007
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3008
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3009
+ else
3010
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3011
+ battle.rng.confusionDuration(player)
2958
3012
  else if (showdown)
2959
- @intCast(battle.rng.range(u8, 2, 6))
3013
+ battle.rng.range(u3, 2, 6)
2960
3014
  else
2961
3015
  @intCast((battle.rng.next() & 3) + 2);
2962
3016
 
2963
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3017
+ assert(duration >= 2 and duration <= 5);
3018
+ options.chance.duration(player, duration);
2964
3019
  return duration;
2965
3020
  }
2966
3021
 
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)
3022
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3023
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3024
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3025
+ else
3026
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3027
+ battle.rng.attackingDuration(player)
2970
3028
  else if (showdown)
2971
- @intCast(battle.rng.range(u4, 2, 4))
3029
+ battle.rng.range(u3, 2, 4)
2972
3030
  else
2973
3031
  @intCast((battle.rng.next() & 1) + 2);
2974
3032
 
2975
- options.chance.duration(.attacking, player, player, duration);
3033
+ assert(duration >= 2 and duration <= 3);
3034
+ options.chance.duration(player, duration);
2976
3035
  return duration;
2977
3036
  }
2978
3037
 
2979
- inline fn distribution(
3038
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3039
+
3040
+ fn distribution(
2980
3041
  battle: anytype,
2981
3042
  comptime effect: Move.Effect,
2982
3043
  player: Player,
2983
3044
  options: anytype,
2984
3045
  ) !u3 {
2985
3046
  const roll = if (effect == .Binding) .duration else .multi_hit;
2986
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2987
- @intCast(val)
3047
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3048
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3049
+ else
3050
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3051
+ battle.rng.distribution(player)
2988
3052
  else if (showdown)
2989
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3053
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2990
3054
  else n: {
2991
3055
  const r = (battle.rng.next() & 3);
2992
3056
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2993
3057
  };
2994
3058
 
3059
+ assert(n >= 2 and n <= 5);
2995
3060
  if (effect == .Binding) {
2996
- options.chance.duration(.binding, player, player, n);
3061
+ options.chance.duration(player, n);
2997
3062
  } else {
2998
3063
  try options.chance.multiHit(player, n);
2999
3064
  }
@@ -3008,15 +3073,15 @@ pub const Rolls = struct {
3008
3073
  options: anytype,
3009
3074
  ) !u4 {
3010
3075
  // TODO: consider throwing error instead of rerolling?
3011
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3012
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3013
3078
  val
3014
3079
  else
3015
3080
  null
3016
3081
  else
3017
- null;
3018
-
3019
- const slot: u4 = overridden orelse slot: {
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3083
+ battle.rng.moveSlot(player, moves, check_pp)
3084
+ else slot: {
3020
3085
  if (showdown) {
3021
3086
  if (check_pp == 0) {
3022
3087
  var i: usize = moves.len;
@@ -3032,10 +3097,9 @@ pub const Rolls = struct {
3032
3097
  while (i < moves.len and r > 0) : (i += 1) {
3033
3098
  if (moves[i].pp > 0) {
3034
3099
  r -= 1;
3035
- if (r == 0) break;
3100
+ if (r == 0) break :slot @intCast(i + 1);
3036
3101
  }
3037
3102
  }
3038
- break :slot @intCast(i + 1);
3039
3103
  }
3040
3104
  }
3041
3105
 
@@ -3057,11 +3121,11 @@ test "RNG agreement" {
3057
3121
  expected[i] = @intCast(i * 0x1000000);
3058
3122
  }
3059
3123
 
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 };
3124
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3125
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3126
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3127
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3128
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3065
3129
 
3066
3130
  for (0..expected.len) |i| {
3067
3131
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3081,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3081
3145
  },
3082
3146
  .Switch => {
3083
3147
  const side = battle.side(player);
3084
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3085
3149
  while (slot <= 6) : (slot += 1) {
3086
3150
  const id = side.order[slot - 1];
3087
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3088
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3089
3153
  n += 1;
3090
3154
  }
3091
3155
  if (n == 0) {
@@ -3097,11 +3161,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3097
3161
  const side = battle.side(player);
3098
3162
  const foe = battle.foe(player);
3099
3163
 
3100
- var active = side.active;
3164
+ const active = &side.active;
3101
3165
  const stored = side.stored();
3102
3166
 
3103
3167
  // 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.
3168
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3105
3169
  //
3106
3170
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3107
3171
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3109,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3109
3173
  // passing due to being forced into using a move. Instead of introducing another option
3110
3174
  // we simply repurpose Move with no move slot, even though pedantically this is not
3111
3175
  // strictly correct as the player would not have been presented the option to move or
3112
- // switch at all.
3176
+ // switch at all
3113
3177
  if (isForced(active)) {
3114
3178
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3115
3179
  n += 1;
3116
3180
  return n;
3117
3181
  }
3118
3182
 
3119
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3120
3184
  while (slot <= 6) : (slot += 1) {
3121
3185
  const id = side.order[slot - 1];
3122
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3123
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3124
3188
  n += 1;
3125
3189
  }
3126
3190
 
@@ -3128,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3128
3192
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3129
3193
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3130
3194
  // either disables all other moves in the case of limited or requires you to select a
3131
- // move normally if sleeping/frozen/bound.
3195
+ // move normally if sleeping/frozen/bound
3132
3196
  if (!showdown and (limited or foe.active.volatiles.Binding or
3133
3197
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3134
3198
  {
@@ -3141,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3141
3205
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3142
3206
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3143
3207
  // 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.
3208
+ // proc-ed move in their moveset) and disabling all other moves
3145
3209
  if (limited) {
3146
3210
  assert(showdown);
3147
3211
  assert(side.last_selected_move != .None);
@@ -3151,9 +3215,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3151
3215
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3152
3216
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3153
3217
  const struggle =
3154
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3218
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3155
3219
  const s = if (struggle) 0 else slot;
3156
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3157
3221
  n += 1;
3158
3222
  return n;
3159
3223
  }
@@ -3166,14 +3230,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3166
3230
  const m = active.moves[slot - 1];
3167
3231
  if (m.id == .None) break;
3168
3232
  if (m.pp == 0) continue;
3169
- if (active.volatiles.disabled_move == slot) continue;
3170
- out[n] = .{ .type = .Move, .data = slot };
3233
+ if (active.volatiles.disable_move == slot) continue;
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3171
3235
  n += 1;
3172
3236
  }
3173
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3174
3238
  if (n == before) {
3175
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3176
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3177
3241
  while (slot <= 4) : (slot += 1) {
3178
3242
  if (active.moves[slot - 1].pp != 0) return n;
3179
3243
  }