@pkmn/engine 0.1.0-dev.f3aaee39 → 0.1.0-dev.f3b63b86

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