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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +51 -23
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/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 +53 -76
  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 +347 -104
  40. package/package.json +62 -123
  41. package/src/bin/install-pkmn-engine +31 -20
  42. package/src/bin/pkmn-debug +1 -1
  43. package/src/lib/bindings/c.zig +255 -0
  44. package/src/lib/{binding → bindings}/node.zig +9 -22
  45. package/src/lib/bindings/wasm.zig +59 -0
  46. package/src/lib/common/array.zig +77 -0
  47. package/src/lib/common/data.zig +12 -24
  48. package/src/lib/common/debug.zig +39 -8
  49. package/src/lib/common/js.zig +4 -2
  50. package/src/lib/common/optional.zig +21 -10
  51. package/src/lib/common/options.zig +6 -0
  52. package/src/lib/common/protocol.zig +808 -332
  53. package/src/lib/common/rational.zig +132 -36
  54. package/src/lib/common/rng.zig +22 -15
  55. package/src/lib/common/util.zig +50 -1
  56. package/src/lib/gen1/calc.zig +688 -162
  57. package/src/lib/gen1/chance.zig +436 -295
  58. package/src/lib/gen1/data/moves.zig +193 -183
  59. package/src/lib/gen1/data/species.zig +4 -6
  60. package/src/lib/gen1/data/types.zig +8 -9
  61. package/src/lib/gen1/data.zig +55 -70
  62. package/src/lib/gen1/helpers.zig +91 -562
  63. package/src/lib/gen1/mechanics.zig +659 -579
  64. package/src/lib/gen2/calc.zig +153 -0
  65. package/src/lib/gen2/chance.zig +902 -0
  66. package/src/lib/gen2/data/items.zig +13 -21
  67. package/src/lib/gen2/data/moves.zig +664 -572
  68. package/src/lib/gen2/data/species.zig +2 -4
  69. package/src/lib/gen2/data/types.zig +135 -33
  70. package/src/lib/gen2/data.zig +115 -91
  71. package/src/lib/gen2/generated.zig +801 -0
  72. package/src/lib/gen2/helpers.zig +54 -37
  73. package/src/lib/gen2/mechanics.zig +3580 -29
  74. package/src/lib/node.zig +18 -0
  75. package/src/lib/pkmn.zig +37 -15
  76. package/src/lib/wasm.zig +17 -0
  77. package/src/tools/display/dom.ts +95 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -400
  80. package/build/test/display.js.map +0 -1
  81. package/src/lib/binding/wasm.zig +0 -12
  82. package/src/lib/gen1/README.md +0 -892
  83. package/src/lib/gen2/README.md +0 -308
  84. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,38 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
-
10
- const Action = @import("chance.zig").Action;
11
-
12
- const data = @import("data.zig");
10
+ const std = @import("std");
13
11
 
14
12
  const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
- const showdown = pkmn.options.showdown;
24
-
25
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
26
15
  const Damage = protocol.Damage;
27
- const Heal = protocol.Heal;
28
-
29
- const Gen12 = rng.Gen12;
30
-
31
- const ActivePokemon = data.ActivePokemon;
32
16
  const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
18
+ const Fail = protocol.Fail;
19
+ const Gen12 = rng.Gen12;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
33
22
  const Move = data.Move;
34
23
  const MoveSlot = data.MoveSlot;
35
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
36
27
  const Side = data.Side;
37
28
  const Species = data.Species;
38
29
  const Stats = data.Stats;
@@ -59,34 +50,26 @@ const BOOSTS = &[_][2]u8{
59
50
 
60
51
  const MAX_STAT_VALUE = 999;
61
52
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
53
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
54
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
55
  if (battle.turn == 0) return start(battle, options);
71
56
 
72
57
  var s1 = false;
73
58
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
59
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
60
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
61
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
62
 
80
63
  var p1 = battle.side(.P1);
81
64
  var p2 = battle.side(.P2);
82
65
 
83
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
84
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
66
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
67
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
68
 
86
69
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
70
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
71
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
72
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
73
  }
91
74
 
92
75
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -99,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
82
  const p1 = battle.side(.P1);
100
83
  const p2 = battle.side(.P2);
101
84
 
102
- var p1_slot = findFirstAlive(p1);
85
+ const p1_slot = findFirstAlive(p1);
103
86
  assert(!showdown or p1_slot == 1);
104
87
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
88
 
106
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
107
90
  assert(!showdown or p2_slot == 1);
108
91
  if (p2_slot == 0) return Result.Win;
109
92
 
@@ -124,7 +107,6 @@ fn selectMove(
124
107
  choice: Choice,
125
108
  foe_choice: Choice,
126
109
  skip_turn: *bool,
127
- from: *?Move,
128
110
  ) ?Result {
129
111
  if (choice.type == .Pass) return null;
130
112
 
@@ -141,7 +123,6 @@ fn selectMove(
141
123
  return null;
142
124
  }
143
125
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
126
  if (showdown) {
146
127
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
128
  saveMove(battle, player, null);
@@ -151,7 +132,6 @@ fn selectMove(
151
132
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
133
  if (!showdown) volatiles.Flinch = false;
153
134
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
135
  if (showdown) {
156
136
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
137
  saveMove(battle, player, null);
@@ -172,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,7 +614,9 @@ 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
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +645,12 @@ fn beforeMove(
638
645
  volatiles.Thrashing = false;
639
646
  volatiles.Charging = false;
640
647
  volatiles.Binding = false;
641
- options.chance.clearDurations(player, null);
648
+
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
651
+
642
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
653
+ try log.cant(.{ ident, .Paralysis });
644
654
  return .done;
645
655
  }
646
656
 
@@ -656,28 +666,22 @@ fn beforeMove(
656
666
  volatiles.state +%= battle.last_damage;
657
667
  }
658
668
 
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);
669
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
670
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
671
 
668
672
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
673
+ try log.activate(.{ ident, .Bide });
670
674
  return .done;
671
675
  }
672
676
 
673
677
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
678
+ try log.end(.{ ident, .Bide });
675
679
 
676
680
  battle.last_damage = volatiles.state *% 2;
677
681
  volatiles.state = 0;
678
682
 
679
683
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
684
+ try log.fail(.{ ident, .None });
681
685
  return .done;
682
686
  }
683
687
 
@@ -697,47 +701,42 @@ fn beforeMove(
697
701
  }
698
702
 
699
703
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
704
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
705
 
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);
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
708
 
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
- }
709
+ if (volatiles.attacks == 0) {
710
+ const overwritten = volatiles.Confusion;
711
+
712
+ volatiles.Thrashing = false;
713
+ volatiles.Confusion = true;
714
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
715
+
716
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
717
+ // ended (the engine's protocol is always from the omniscient perspective and clients
718
+ // can be expected to recognize this pattern and pass the information only to the
719
+ // correct player)
720
+ try log.start(.{ battle.active(player), .ConfusionSilent });
721
+ options.chance.observe(.confusion, player, if (overwritten)
722
+ if (pkmn.options.overwrite) .overwritten else .continuing
723
+ else
724
+ .started);
725
+ if (!overwritten) options.calc.confusion(player);
721
726
  }
727
+
728
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
729
+ // used in useMove and doesn't have the notion of skipping canMove semantics
730
+ if (showdown) side.last_used_move = side.last_selected_move;
731
+
722
732
  return .skip_can;
723
733
  }
724
734
 
725
735
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
736
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
737
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
727
738
 
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
- );
739
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
740
  if (showdown or battle.last_damage != 0) {
742
741
  const sub = showdown and foe.active.volatiles.Substitute;
743
742
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +756,7 @@ fn canMove(
757
756
  mslot: u4,
758
757
  auto: bool,
759
758
  skip_pp: bool,
760
- from: ?Move,
759
+ from: Move,
761
760
  residual: *bool,
762
761
  options: anytype,
763
762
  ) !bool {
@@ -765,33 +764,21 @@ fn canMove(
765
764
  const player_ident = battle.active(player);
766
765
  const move = Move.get(side.last_selected_move);
767
766
 
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
767
  if (side.active.volatiles.Charging) {
774
768
  side.active.volatiles.Charging = false;
775
769
  side.active.volatiles.Invulnerable = false;
776
770
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
771
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
772
  setCounterable(battle, player, side, move);
779
773
  try Effects.charge(battle, player, options);
780
774
  return false;
781
775
  }
782
776
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
777
+ side.last_used_move = side.last_selected_move;
778
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
779
 
786
780
  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);
781
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
782
  setCounterable(battle, player, side, move);
796
783
 
797
784
  if (move.effect.onBegin()) {
@@ -801,9 +788,14 @@ fn canMove(
801
788
 
802
789
  if (move.effect == .Thrashing) {
803
790
  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);
791
+ } else if (move.effect == .Binding) {
792
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
793
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
794
+ if (showdown) {
795
+ battle.foe(player).active.volatiles.Recharging = false;
796
+ } else {
797
+ try Effects.binding(battle, player, false, options);
798
+ }
807
799
  }
808
800
 
809
801
  return true;
@@ -829,16 +821,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
821
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
822
  if (volatiles.Bide) return;
831
823
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
824
+ var move_slot = active.move(mslot);
825
+ assert(move_slot.pp > 0 or auto);
826
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
827
  if (volatiles.Transform) return;
835
828
 
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;
829
+ move_slot = side.stored().move(mslot);
830
+ assert(move_slot.pp > 0 or auto);
831
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
833
  }
840
834
 
841
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
842
836
  var active = &side.active;
843
837
  const volatiles = &active.volatiles;
844
838
 
@@ -846,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
846
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
847
841
  if (showdown and volatiles.Transform) return;
848
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
849
846
  assert(mslot > 0 and mslot <= 4);
850
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
847
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
851
848
  }
852
849
 
853
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -895,13 +892,13 @@ fn doMove(
895
892
  break :miss true;
896
893
  }
897
894
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
895
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
896
  };
900
897
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
898
 
902
899
  var late = showdown and move.effect != .Explode;
903
900
  const skip = status or immune;
904
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
905
902
  if (showdown and move.effect.isMulti()) {
906
903
  try Effects.multiHit(battle, player, move, options);
907
904
  hits = side.active.volatiles.attacks;
@@ -923,8 +920,9 @@ fn doMove(
923
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
924
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
922
  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)) {
923
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
924
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
925
+ if (!showdown) try options.chance.commit(player, .err);
928
926
  return @as(?Result, Result.Error);
929
927
  }
930
928
  if (battle.last_damage == 0) {
@@ -938,11 +936,12 @@ fn doMove(
938
936
  }
939
937
  }
940
938
 
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);
939
+ var missed: bool = undefined;
940
+ const zero = battle.last_damage == 0;
941
+ if (!showdown and !skip) {
942
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
943
+ miss = missed or zero;
944
+ }
946
945
 
947
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +965,37 @@ fn doMove(
966
965
  ohko = (!showdown or (!immune and !invulnerable)) and
967
966
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
967
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
968
+ try log.immune(.{ foe_ident, .OHKO });
970
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- try log.immune(foe_ident, .None);
970
+ try log.immune(.{ foe_ident, .None });
971
+ if (!showdown) {
972
+ if (move.effect == .Binding) {
973
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
974
+ // then simply clears it later on if the move happens to miss in moveHit.
975
+ // However, moveHit can return true and we'll still end up in the outer miss
976
+ // handling block because attacking into an immune Pokemon does zero damage
977
+ // (considered missing)
978
+ assert(zero);
979
+ // Thus we still need to actually save the hit result (but not the crit roll).
980
+ // Using .miss for this commit would accomplish what we want, but is somewhat
981
+ // misleading as its entirely possible the move actually did hit (and the target
982
+ // is now trapped) so we use the .binding value instead which is effectively
983
+ // identical, just doesn't claim to know about the actual results of the hit
984
+ // roll)
985
+ try options.chance.commit(player, .binding);
986
+ } else {
987
+ // The .glitch option allows for conditionally committing if we are in a
988
+ // division-by-zero freeze alternative timeline scenario
989
+ try options.chance.commit(player, .glitch);
990
+ }
991
+ }
972
992
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- try log.fail(foe_ident, .None);
993
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
994
+ try log.fail(.{ foe_ident, .None });
975
995
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- try log.miss(battle.active(player));
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
997
+ try log.lastmiss(.{});
998
+ try log.miss(.{battle.active(player)});
979
999
  }
980
1000
  if (move.effect == .JumpKick) {
981
1001
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +1008,15 @@ fn doMove(
988
1008
  try buildRage(battle, player.foe(), options);
989
1009
  } else if (showdown and move.effect == .Disable) {
990
1010
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
1011
  }
994
1012
  return null;
995
- } else {
1013
+ } else if (!showdown) {
996
1014
  try options.chance.commit(player, .hit);
997
1015
  }
998
1016
 
999
1017
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
1018
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
1019
+ // it up above before damage calculation)
1002
1020
  if (!showdown and move.effect.isMulti()) {
1003
1021
  try Effects.multiHit(battle, player, move, options);
1004
1022
  hits = side.active.volatiles.attacks;
@@ -1011,15 +1029,15 @@ fn doMove(
1011
1029
  var hit: u4 = 0;
1012
1030
  while (hit < hits) {
1013
1031
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1032
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1033
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1034
+ try log.supereffective(.{battle.active(player.foe())});
1017
1035
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1036
+ try log.resisted(.{battle.active(player.foe())});
1019
1037
  }
1020
1038
  }
1021
1039
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1040
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1041
  hit += 1;
1024
1042
  if (foe.stored().hp == 0) break;
1025
1043
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,33 +1054,31 @@ fn doMove(
1036
1054
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1056
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1040
- } else if (showdown) {
1057
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1058
+ } else if (showdown and move.effect == .Binding) {
1041
1059
  // 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;
1060
+ try Effects.binding(battle, player, rewrap, options);
1061
+ if (immune) {
1062
+ battle.last_damage = 0;
1063
+ assert(foe.stored().hp > 0);
1064
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1065
+ if (sub) {
1066
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1067
+ } else {
1068
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1069
+ try buildRage(battle, player.foe(), options);
1055
1070
  }
1071
+ return null;
1056
1072
  }
1057
1073
  }
1058
1074
 
1059
1075
  // 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.
1076
+ // if an effect was intended to "always happen" it will still get skipped
1061
1077
  if (nullified) return null;
1062
1078
 
1063
1079
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1080
  // 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.
1081
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1082
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1083
 
1068
1084
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1093,7 @@ fn doMove(
1077
1093
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1094
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1095
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1096
+ // puts the |-status| message before |-hitcount| instead of after
1081
1097
  if (move.effect == .Twineedle) {
1082
1098
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1099
  } else if (move.effect == .Disable) {
@@ -1096,12 +1112,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1112
  const side = battle.side(player);
1097
1113
 
1098
1114
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1115
+ var rate: u16 = Species.chance(side.stored().species);
1100
1116
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1117
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1118
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1119
 
1104
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1120
+ const spurious =
1121
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1122
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1105
1123
  }
1106
1124
 
1107
1125
  fn calcDamage(
@@ -1110,6 +1128,7 @@ fn calcDamage(
1110
1128
  target_player: Player,
1111
1129
  m: ?Move.Data,
1112
1130
  crit: bool,
1131
+ options: anytype,
1113
1132
  ) bool {
1114
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1134
  const cfz = m == null;
@@ -1121,28 +1140,8 @@ fn calcDamage(
1121
1140
 
1122
1141
  const special = move.type.special();
1123
1142
 
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
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1146
1145
 
1147
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1150,12 +1149,12 @@ fn calcDamage(
1150
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1151
1150
  atk = @max((atk / 4) & 255, 1);
1152
1151
  // 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);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1154
1153
  }
1155
1154
 
1156
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1155
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1157
1156
 
1158
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1157
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1159
1158
 
1160
1159
  if (def == 0) return false;
1161
1160
 
@@ -1167,11 +1166,41 @@ fn calcDamage(
1167
1166
  d = @min(997, d);
1168
1167
  d += 2;
1169
1168
 
1169
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1170
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1171
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1172
+ // that results in counter intuitive probabilities in the common case
1173
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1174
+ atk = attack(side, !crit, special);
1175
+ def = defense(target, !crit, special, false);
1176
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1177
+ }
1178
+
1170
1179
  battle.last_damage = @intCast(d);
1171
1180
 
1172
1181
  return true;
1173
1182
  }
1174
1183
 
1184
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1185
+ return if (crit)
1186
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1187
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1188
+ }
1189
+
1190
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1191
+ return if (crit)
1192
+ if (special) target.stored().stats.spc else target.stored().stats.def
1193
+ else
1194
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1195
+ if (special)
1196
+ target.active.stats.spc *
1197
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1198
+ else
1199
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1200
+ target.active.stats.def *
1201
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1202
+ }
1203
+
1175
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1205
  const side = battle.side(player);
1177
1206
  const foe = battle.foe(player);
@@ -1216,7 +1245,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1245
  const foe = battle.foe(player);
1217
1246
 
1218
1247
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1248
 
1221
1249
  battle.last_damage = switch (side.last_selected_move) {
1222
1250
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1227,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1255
  .Psywave => power: {
1228
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1229
1257
  // GLITCH: Psywave infinite glitch loop
1230
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1231
1259
  break :power try Rolls.psywave(battle, player, max, options);
1232
1260
  },
1233
1261
  else => unreachable,
1234
1262
  };
1235
1263
 
1236
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1264
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1237
1265
 
1238
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1239
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1246,7 +1274,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1274
  const foe = battle.foe(player);
1247
1275
 
1248
1276
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1277
+ try options.log.fail(.{ battle.active(player), .None });
1250
1278
  return null;
1251
1279
  }
1252
1280
 
@@ -1264,22 +1292,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1292
  foe_last_selected_move.type == .Fighting);
1265
1293
 
1266
1294
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1295
+ try options.log.fail(.{ battle.active(player), .None });
1268
1296
  return null;
1269
1297
  }
1270
1298
 
1271
1299
  if (!used or !selected) {
1272
1300
  // 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);
1301
+ if (!pkmn.options.mod) return Result.Error;
1302
+ try options.log.fail(.{ battle.active(player), .None });
1275
1303
  return null;
1276
1304
  }
1277
1305
 
1278
- battle.last_damage *|= 2;
1306
+ // NOTE: llvm/llvm-project#58557
1307
+ battle.last_damage = if (comptime builtin.target.isWasm())
1308
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1309
+ else
1310
+ battle.last_damage *| 2;
1279
1311
 
1280
- // Pokémon Showdown calls checkHit before Counter
1312
+ // Pokémon Showdown calls moveHit before Counter
1281
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1314
 
1284
1315
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1316
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1338,20 @@ fn applyDamage(
1307
1338
  subbed.active.volatiles.substitute = 0;
1308
1339
  subbed.active.volatiles.Substitute = false;
1309
1340
  // 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);
1341
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1312
1343
  return true;
1313
1344
  } else {
1314
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1346
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1347
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1348
  return false;
1318
1349
  }
1319
1350
  }
1320
1351
 
1321
1352
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1353
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1354
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1355
  return false;
1325
1356
  }
1326
1357
 
@@ -1339,10 +1370,10 @@ fn mirrorMove(
1339
1370
  side.last_selected_move = foe.last_used_move;
1340
1371
 
1341
1372
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1373
+ try options.log.fail(.{ battle.active(player), .None });
1343
1374
  return null;
1344
1375
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1376
+ incrementPP(side, player, mslot, options);
1346
1377
  }
1347
1378
 
1348
1379
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1394,7 @@ fn metronome(
1363
1394
  var side = battle.side(player);
1364
1395
 
1365
1396
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1397
+ incrementPP(side, player, mslot, options);
1367
1398
 
1368
1399
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1400
  return null;
@@ -1375,20 +1406,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1406
  var immune = false;
1376
1407
  var mist = false;
1377
1408
 
1378
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1409
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1410
+ if (!showdown) try options.chance.commit(player, .hit);
1411
+ return true;
1412
+ }
1379
1413
 
1380
1414
  assert(!immune);
1381
1415
  if (mist) {
1382
1416
  assert(!showdown);
1383
1417
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1418
+ try options.log.activate(.{ foe_ident, .Mist });
1419
+ try options.log.fail(.{ foe_ident, .None });
1386
1420
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1421
+ try options.log.lastmiss(.{});
1422
+ try options.log.miss(.{battle.active(player)});
1390
1423
  }
1391
1424
 
1425
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1426
  return false;
1393
1427
  }
1394
1428
 
@@ -1399,7 +1433,7 @@ fn moveHit(
1399
1433
  immune: *bool,
1400
1434
  mist: *bool,
1401
1435
  options: anytype,
1402
- ) bool {
1436
+ ) !bool {
1403
1437
  var side = battle.side(player);
1404
1438
  const foe = battle.foe(player);
1405
1439
 
@@ -1438,7 +1472,7 @@ fn moveHit(
1438
1472
  const overwritten = overwrite and state > 0;
1439
1473
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1474
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1475
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1476
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1477
  accuracy = accuracy * boost[0] / boost[1];
1444
1478
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1484,7 @@ fn moveHit(
1450
1484
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1485
 
1452
1486
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1487
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1488
  };
1455
1489
 
1456
1490
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1495,17 @@ fn moveHit(
1461
1495
 
1462
1496
  if (!miss) return true;
1463
1497
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1498
+
1464
1499
  side.active.volatiles.Binding = false;
1500
+ options.chance.observe(.binding, player, .None);
1501
+
1465
1502
  return false;
1466
1503
  }
1467
1504
 
1468
1505
  fn checkFaint(
1469
1506
  battle: anytype,
1470
1507
  player: Player,
1508
+ cap: bool,
1471
1509
  options: anytype,
1472
1510
  ) @TypeOf(options.log).Error!?Result {
1473
1511
  const side = battle.side(player);
@@ -1477,33 +1515,32 @@ fn checkFaint(
1477
1515
  const foe_fainted = foe.stored().hp == 0;
1478
1516
 
1479
1517
  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;
1518
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1519
+ const more = player_out or foe_out;
1483
1520
 
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;
1521
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1522
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1486
1523
 
1487
1524
  assert(!side.active.volatiles.MultiHit);
1488
1525
  assert(!foe.active.volatiles.MultiHit);
1489
1526
 
1490
- if (tie) {
1491
- try options.log.tie();
1527
+ if (player_out and foe_out) {
1528
+ try options.log.tie(.{});
1492
1529
  return Result.Tie;
1493
1530
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1531
+ try options.log.win(.{player.foe()});
1495
1532
  return if (player == .P1) Result.Lose else Result.Win;
1496
1533
  } else if (foe_out) {
1497
- try options.log.win(player);
1534
+ try options.log.win(.{player});
1498
1535
  return if (player == .P1) Result.Win else Result.Lose;
1499
1536
  }
1500
1537
 
1501
1538
  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 };
1539
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1540
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1541
  }
1505
1542
 
1506
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1543
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1507
1544
  var side = battle.side(player);
1508
1545
  var foe = battle.foe(player);
1509
1546
  assert(side.stored().hp == 0);
@@ -1512,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1512
1549
  assert(!foe_volatiles.MultiHit);
1513
1550
  if (foe_volatiles.Bide) {
1514
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1515
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1552
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1516
1553
  if (foe_volatiles.state != 0) return Result.Error;
1517
1554
  }
1518
1555
 
@@ -1536,8 +1573,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1573
  side.stored().stats.spe;
1537
1574
  }
1538
1575
 
1539
- try options.log.faint(battle.active(player), done);
1540
- options.calc.capped(player);
1576
+ try options.log.faint(.{ battle.active(player), done });
1577
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1578
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1579
+ if (cap) options.calc.capped(player);
1541
1580
  return null;
1542
1581
  }
1543
1582
 
@@ -1548,21 +1587,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1587
  var volatiles = &side.active.volatiles;
1549
1588
 
1550
1589
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1590
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1591
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1592
  if (volatiles.Toxic) {
1555
1593
  volatiles.toxic += 1;
1556
1594
  damage *= volatiles.toxic;
1557
1595
  }
1596
+ damage = @min(damage, stored.hp);
1558
1597
 
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);
1598
+ if (!showdown or damage > 0) {
1599
+ stored.hp -= damage;
1600
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1601
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1602
+ }
1566
1603
  }
1567
1604
 
1568
1605
  if (volatiles.LeechSeed) {
@@ -1586,14 +1623,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1623
  const amount = @min(damage, stored.hp);
1587
1624
  stored.hp -= amount;
1588
1625
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1626
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1627
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1628
 
1592
1629
  const before = foe_stored.hp;
1593
1630
  // Uncapped damage is added back to the foe
1594
1631
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1632
  // 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);
1633
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1634
  }
1598
1635
  }
1599
1636
 
@@ -1603,19 +1640,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1603
1640
 
1604
1641
  battle.turn += 1;
1605
1642
 
1606
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1644
+ try options.log.tie(.{});
1608
1645
  return Result.Tie;
1609
1646
  }
1610
1647
 
1611
- if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1649
+ try options.log.tie(.{});
1613
1650
  return Result.Tie;
1614
1651
  } else if (battle.turn >= 65535) {
1615
1652
  return Result.Error;
1616
1653
  }
1617
1654
 
1618
- try options.log.turn(battle.turn);
1655
+ try options.log.turn(.{battle.turn});
1619
1656
 
1620
1657
  return Result.Default;
1621
1658
  }
@@ -1675,9 +1712,9 @@ fn checkEBC(battle: anytype) bool {
1675
1712
  }
1676
1713
 
1677
1714
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1715
+ if (side.active.volatiles.disable_move != 0) {
1679
1716
  // 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;
1717
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1718
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1719
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1720
  m
@@ -1685,32 +1722,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1722
  side.last_selected_move;
1686
1723
  if (m != .None and m == last) {
1687
1724
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1725
+ try options.log.cant(.{ ident, .Disable, last });
1689
1726
  return true;
1690
1727
  }
1691
1728
  }
1692
1729
  return false;
1693
1730
  }
1694
1731
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1732
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1733
  const side = battle.side(who);
1697
1734
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1735
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1736
  }
1700
1737
  }
1701
1738
 
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(
1739
+ fn onBegin(
1714
1740
  battle: anytype,
1715
1741
  player: Player,
1716
1742
  move: Move.Data,
@@ -1718,7 +1744,6 @@ inline fn onBegin(
1718
1744
  residual: *bool,
1719
1745
  options: anytype,
1720
1746
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1747
  return switch (move.effect) {
1723
1748
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1749
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1765,7 @@ inline fn onBegin(
1740
1765
  };
1741
1766
  }
1742
1767
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1768
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1769
  return switch (move.effect) {
1746
1770
  .Bide => Effects.bide(battle, player, options),
1747
1771
  // zig fmt: off
@@ -1756,14 +1780,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1780
  };
1757
1781
  }
1758
1782
 
1759
- inline fn alwaysHappens(
1783
+ fn alwaysHappens(
1760
1784
  battle: anytype,
1761
1785
  player: Player,
1762
1786
  move: Move.Data,
1763
1787
  residual: *bool,
1764
1788
  options: anytype,
1765
1789
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1790
  return switch (move.effect) {
1768
1791
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1792
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1798,7 @@ inline fn alwaysHappens(
1775
1798
  };
1776
1799
  }
1777
1800
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1801
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1802
  return switch (move.effect) {
1780
1803
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1804
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1822,12 @@ pub const Effects = struct {
1799
1822
 
1800
1823
  side.active.volatiles.Bide = true;
1801
1824
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1825
+
1802
1826
  side.active.volatiles.state = 0;
1803
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1828
+ options.chance.observe(.attacking, player, .started);
1804
1829
 
1805
- try options.log.start(battle.active(player), .Bide);
1830
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1831
  }
1807
1832
 
1808
1833
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1841,11 @@ pub const Effects = struct {
1816
1841
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1842
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1843
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1844
+ try options.log.curestatus(.{
1820
1845
  battle.active(player.foe()),
1821
1846
  foe_stored.status,
1822
1847
  .Message,
1823
- );
1848
+ });
1824
1849
  foe_stored.status = 0;
1825
1850
  }
1826
1851
  return;
@@ -1834,7 +1859,7 @@ pub const Effects = struct {
1834
1859
  foe_stored.status = Status.init(.BRN);
1835
1860
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1861
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1862
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1863
  }
1839
1864
 
1840
1865
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1869,8 @@ pub const Effects = struct {
1844
1869
  volatiles.Charging = true;
1845
1870
  const move = side.last_selected_move;
1846
1871
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1872
+ try options.log.laststill(.{});
1873
+ try options.log.prepare(.{ battle.active(player), move });
1849
1874
  }
1850
1875
 
1851
1876
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1881,39 @@ pub const Effects = struct {
1856
1881
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1882
  } else {
1858
1883
  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
- }
1884
+ if (!try checkHit(battle, player, move, options)) return;
1885
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1886
  } 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
- }
1887
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1888
+ if (!try checkHit(battle, player, move, options)) return;
1870
1889
  }
1871
- try options.chance.commit(player, .hit);
1872
1890
  }
1873
1891
 
1874
1892
  if (foe.active.volatiles.Confusion) return;
1875
1893
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1894
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1895
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1896
+ options.chance.observe(.confusion, player.foe(), .started);
1897
+ options.calc.confusion(player.foe());
1898
+
1899
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1900
  }
1880
1901
 
1881
1902
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1903
  const foe = battle.foe(player);
1883
1904
 
1884
1905
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1906
+ try options.log.lastmiss(.{});
1907
+ return options.log.miss(.{battle.active(player)});
1887
1908
  }
1888
1909
 
1889
1910
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1911
+ return options.log.start(.{
1891
1912
  battle.active(player),
1913
+ .TypeChange,
1892
1914
  foe.active.types,
1893
1915
  battle.active(player.foe()),
1894
- );
1916
+ });
1895
1917
  }
1896
1918
 
1897
1919
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1922,10 @@ pub const Effects = struct {
1900
1922
  const foe_ident = battle.active(player.foe());
1901
1923
 
1902
1924
  // 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
- }
1925
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1926
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1927
+ if (volatiles.disable_move != 0) {
1928
+ try options.log.fail(.{ foe_ident, .None });
1910
1929
  return null;
1911
1930
  }
1912
1931
 
@@ -1921,27 +1940,27 @@ pub const Effects = struct {
1921
1940
 
1922
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
- if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1944
+ try options.log.fail(.{ foe_ident, .None });
1926
1945
  return null;
1927
1946
  } else if (err) {
1928
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1929
- assert(!showdown);
1930
1948
  return Result.Error;
1931
1949
  }
1932
1950
 
1933
- volatiles.disabled_move =
1951
+ volatiles.disable_move =
1934
1952
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1953
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1954
+ options.chance.observe(.disable, player.foe(), .started);
1936
1955
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1956
+ const id = foe.active.move(volatiles.disable_move).id;
1957
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1958
  return null;
1940
1959
  }
1941
1960
 
1942
1961
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1962
+ var stored = battle.side(player).stored();
1963
+ const ident = battle.active(player);
1945
1964
 
1946
1965
  if (battle.last_damage == 0) {
1947
1966
  assert(showdown);
@@ -1954,7 +1973,7 @@ pub const Effects = struct {
1954
1973
  if (stored.hp == stored.stats.hp) return;
1955
1974
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1975
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1976
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1977
  }
1959
1978
 
1960
1979
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +2005,7 @@ pub const Effects = struct {
1986
2005
  if (side.active.volatiles.FocusEnergy) return;
1987
2006
  side.active.volatiles.FocusEnergy = true;
1988
2007
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
2008
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
2009
  }
1991
2010
 
1992
2011
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2003,12 +2022,12 @@ pub const Effects = struct {
2003
2022
  if (foe.active.types.includes(move.type)) return;
2004
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2005
2024
  // Freeze Clause Mod
2006
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2025
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2007
2026
 
2008
2027
  foe_stored.status = Status.init(.FRZ);
2009
2028
  // GLITCH: Hyper Beam recharging status is not cleared
2010
2029
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
2030
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2031
  }
2013
2032
 
2014
2033
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2035,7 @@ pub const Effects = struct {
2016
2035
  var side = battle.side(player);
2017
2036
  var foe = battle.foe(player);
2018
2037
 
2019
- var side_stored = side.stored();
2038
+ const side_stored = side.stored();
2020
2039
  var foe_stored = foe.stored();
2021
2040
 
2022
2041
  const player_ident = battle.active(player);
@@ -2028,8 +2047,8 @@ pub const Effects = struct {
2028
2047
  side.active.stats = side_stored.stats;
2029
2048
  foe.active.stats = foe_stored.stats;
2030
2049
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2050
+ try log.activate(.{ player_ident, .Haze });
2051
+ try log.clearallboost(.{});
2033
2052
 
2034
2053
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2054
  if (showdown) {
@@ -2037,24 +2056,26 @@ pub const Effects = struct {
2037
2056
  const p: Player = @enumFromInt(i);
2038
2057
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2058
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2059
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2060
  s.stored().status = 0;
2061
+ options.chance.observe(.sleep, p, .None);
2042
2062
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2063
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2064
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2065
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2066
+ try clearVolatiles(battle, p, options);
2047
2067
  }
2048
2068
  } else {
2049
2069
  if (Status.any(foe_stored.status)) {
2050
2070
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2071
  foe.last_selected_move = .SKIP_TURN;
2072
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2073
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2074
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2075
  foe_stored.status = 0;
2055
2076
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2077
+ try clearVolatiles(battle, player, options);
2078
+ try clearVolatiles(battle, player.foe(), options);
2058
2079
  }
2059
2080
  }
2060
2081
 
@@ -2066,7 +2087,7 @@ pub const Effects = struct {
2066
2087
  // GLITCH: HP recovery move failure glitches
2067
2088
  const delta = stored.stats.hp - stored.hp;
2068
2089
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2090
+ return options.log.fail(.{ ident, .None });
2070
2091
  }
2071
2092
 
2072
2093
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2095,17 @@ pub const Effects = struct {
2074
2095
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2096
  if (showdown) battle.rng.advance(1);
2076
2097
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2098
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2099
  stored.hp = stored.stats.hp;
2079
2100
  } else {
2080
2101
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2102
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2103
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2104
  }
2084
2105
 
2085
2106
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2107
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2108
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2109
  }
2089
2110
 
2090
2111
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2114,31 @@ pub const Effects = struct {
2093
2114
  if (showdown) {
2094
2115
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2116
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2117
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2118
  }
2098
2119
  if (!try checkHit(battle, player, move, options)) return;
2099
2120
  if (foe.active.volatiles.LeechSeed) return;
2100
2121
  } else {
2101
2122
  if (!try checkHit(battle, player, move, options)) return;
2102
2123
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2124
+ try options.log.lastmiss(.{});
2125
+ return options.log.miss(.{battle.active(player)});
2105
2126
  }
2106
2127
  }
2107
2128
 
2108
- try options.chance.commit(player, .hit);
2109
2129
  foe.active.volatiles.LeechSeed = true;
2110
2130
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2131
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2132
  }
2113
2133
 
2114
2134
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2135
  var side = battle.side(player);
2136
+ const ident = battle.active(player);
2116
2137
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2138
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2139
  side.active.volatiles.LightScreen = true;
2121
2140
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2141
+ try options.log.start(.{ ident, .LightScreen });
2123
2142
  }
2124
2143
 
2125
2144
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2166,15 @@ pub const Effects = struct {
2147
2166
  if (!has_mimic) {
2148
2167
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2168
  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);
2169
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2170
  }
2153
2171
  }
2154
2172
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2173
 
2157
2174
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2175
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2176
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2177
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2178
  }
2162
2179
 
2163
2180
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2183,7 @@ pub const Effects = struct {
2166
2183
  if (side.active.volatiles.Mist) return;
2167
2184
  side.active.volatiles.Mist = true;
2168
2185
 
2169
- try options.log.start(battle.active(player), .Mist);
2186
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2187
  }
2171
2188
 
2172
2189
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2209,25 @@ pub const Effects = struct {
2192
2209
 
2193
2210
  if (showdown) {
2194
2211
  // 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;
2212
+ if (immune and !foe.active.volatiles.Invulnerable) {
2213
+ return log.immune(.{ foe_ident, .None });
2214
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2215
  }
2198
2216
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2217
+ return log.fail(.{
2200
2218
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2219
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2220
+ });
2203
2221
  }
2204
2222
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2223
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2224
  if (!try checkHit(battle, player, move, options)) return;
2207
2225
  }
2208
- try options.chance.commit(player, .hit);
2209
2226
 
2210
2227
  foe_stored.status = Status.init(.PAR);
2211
2228
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2229
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2230
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2231
  }
2215
2232
 
2216
2233
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2249,12 @@ pub const Effects = struct {
2232
2249
  foe_stored.status = Status.init(.PAR);
2233
2250
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2251
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2252
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2253
  }
2237
2254
 
2238
2255
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2256
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2257
+ try options.log.fieldactivate(.{});
2241
2258
  }
2242
2259
  }
2243
2260
 
@@ -2252,28 +2269,27 @@ pub const Effects = struct {
2252
2269
  return;
2253
2270
  } else if (foe.active.volatiles.Substitute) {
2254
2271
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2272
+ return log.fail(.{ foe_ident, .None });
2256
2273
  } else if (Status.any(foe_stored.status)) {
2257
2274
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2275
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2276
  // 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))
2277
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2278
  if (!showdown)
2262
- .Poison
2279
+ Fail.Poison
2263
2280
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2281
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2282
  else
2266
- .None
2283
+ Fail.None
2267
2284
  else
2268
- .None);
2285
+ Fail.None });
2269
2286
  } else if (foe.active.types.includes(.Poison)) {
2270
2287
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2288
+ return log.immune(.{ foe_ident, .None });
2272
2289
  }
2273
2290
 
2274
2291
  if (move.effect == .Poison) {
2275
2292
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2293
  } else {
2278
2294
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2295
  return;
@@ -2287,7 +2303,7 @@ pub const Effects = struct {
2287
2303
  foe.active.volatiles.toxic = 0;
2288
2304
  }
2289
2305
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2306
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2307
  }
2292
2308
 
2293
2309
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2323,23 @@ pub const Effects = struct {
2307
2323
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2324
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2325
 
2310
- try options.log.damageOf(
2326
+ try options.log.damage(.{
2311
2327
  battle.active(player),
2312
2328
  stored,
2313
2329
  .RecoilOf,
2314
2330
  battle.active(player.foe()),
2315
- );
2331
+ });
2316
2332
  if (showdown and stored.hp == 0) residual.* = false;
2317
2333
  }
2318
2334
 
2319
2335
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2336
  var side = battle.side(player);
2337
+ const ident = battle.active(player);
2321
2338
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2339
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2340
  side.active.volatiles.Reflect = true;
2324
2341
 
2325
- try options.log.start(battle.active(player), .Reflect);
2342
+ try options.log.start(.{ ident, .Reflect });
2326
2343
  }
2327
2344
 
2328
2345
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,18 +2353,17 @@ pub const Effects = struct {
2336
2353
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2354
  } else {
2338
2355
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2356
+ return options.log.fail(.{
2340
2357
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2358
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2359
+ });
2343
2360
  }
2344
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2361
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2362
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2363
  }
2348
2364
 
2349
2365
  // Sleep Clause Mod
2350
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2351
2367
  for (foe.pokemon) |p| {
2352
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2353
2369
  }
@@ -2355,19 +2371,20 @@ pub const Effects = struct {
2355
2371
  foe.active.volatiles.Recharging = false;
2356
2372
 
2357
2373
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2374
+ options.chance.observe(.sleep, player.foe(), .started);
2375
+
2358
2376
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2377
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2378
  }
2361
2379
 
2362
2380
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2381
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2382
  }
2365
2383
 
2366
2384
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2385
  var side = battle.side(player);
2368
2386
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2387
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2388
  }
2372
2389
 
2373
2390
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2396,30 @@ pub const Effects = struct {
2379
2396
  // avoid having to convert to floating point)
2380
2397
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2398
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2399
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2400
  }
2385
2401
 
2386
2402
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2403
  side.stored().hp -= hp;
2388
2404
  side.active.volatiles.substitute = hp + 1;
2389
2405
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2406
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2407
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2408
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2409
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2410
  }
2395
2411
  }
2396
2412
 
2397
2413
  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
2414
  // 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;
2415
+ if (showdown) {
2416
+ if (battle.side(player).last_selected_move == .Teleport) return;
2417
+ _ = try checkHit(battle, player, move, options);
2418
+ battle.last_damage = 0;
2419
+ } else {
2420
+ try options.log.fail(.{ battle.active(player), .None });
2421
+ try options.log.laststill(.{});
2422
+ }
2404
2423
  }
2405
2424
 
2406
2425
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2429,8 @@ pub const Effects = struct {
2410
2429
 
2411
2430
  volatiles.Thrashing = true;
2412
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2432
+
2433
+ options.chance.observe(.attacking, player, .started);
2413
2434
  }
2414
2435
 
2415
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2459,7 @@ pub const Effects = struct {
2438
2459
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2460
  }
2440
2461
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2462
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2463
  }
2443
2464
 
2444
2465
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2448,13 +2469,14 @@ pub const Effects = struct {
2448
2469
  if (side.active.volatiles.Binding) return;
2449
2470
  side.active.volatiles.Binding = true;
2450
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2451
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2452
2473
  if (!showdown) {
2453
2474
  foe.active.volatiles.Recharging = false;
2454
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2455
2476
 
2456
2477
  side.active.volatiles.attacks =
2457
2478
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2479
+ options.chance.observe(.binding, player, .started);
2458
2480
  }
2459
2481
 
2460
2482
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2490,87 @@ pub const Effects = struct {
2468
2490
  switch (move.effect) {
2469
2491
  .AttackUp1, .AttackUp2, .Rage => {
2470
2492
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2493
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2494
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2495
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2496
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2497
  if (stats.atk == MAX_STAT_VALUE) {
2476
2498
  boosts.atk -= 1;
2477
2499
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2500
+ try log.boost(.{ ident, reason, n });
2501
+ try log.boost(.{ ident, .Attack, -1 });
2480
2502
  if (move.effect == .Rage) return;
2481
2503
  }
2482
- return try log.fail(ident, .None);
2504
+ return log.fail(.{ ident, .None });
2483
2505
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2506
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2507
  const stat = unmodifiedStats(battle, side).atk;
2486
2508
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2509
+ try log.boost(.{ ident, reason, n });
2488
2510
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2511
  if (showdown and move.effect == .Rage) return;
2490
2512
  },
2491
2513
  .DefenseUp1, .DefenseUp2 => {
2492
2514
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2515
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2516
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2517
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2518
  if (stats.def == MAX_STAT_VALUE) {
2497
2519
  boosts.def -= 1;
2498
2520
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2521
+ try log.boost(.{ ident, .Defense, n });
2522
+ try log.boost(.{ ident, .Defense, -1 });
2501
2523
  }
2502
- return try log.fail(ident, .None);
2524
+ return log.fail(.{ ident, .None });
2503
2525
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2526
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2527
  const stat = unmodifiedStats(battle, side).def;
2506
2528
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2529
+ try log.boost(.{ ident, .Defense, n });
2508
2530
  },
2509
2531
  .SpeedUp2 => {
2510
2532
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2533
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2534
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2535
  if (stats.spe == MAX_STAT_VALUE) {
2514
2536
  boosts.spe -= 1;
2515
2537
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2538
+ try log.boost(.{ ident, .Speed, 2 });
2539
+ try log.boost(.{ ident, .Speed, -1 });
2518
2540
  }
2519
- return try log.fail(ident, .None);
2541
+ return log.fail(.{ ident, .None });
2520
2542
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2543
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2544
  const stat = unmodifiedStats(battle, side).spe;
2523
2545
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2546
+ try log.boost(.{ ident, .Speed, 2 });
2525
2547
  },
2526
2548
  .SpecialUp1, .SpecialUp2 => {
2527
2549
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2550
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2551
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2552
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2553
  if (stats.spc == MAX_STAT_VALUE) {
2532
2554
  boosts.spc -= 1;
2533
2555
  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);
2556
+ try log.boost(.{ ident, .SpecialAttack, n });
2557
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2558
+ try log.boost(.{ ident, .SpecialDefense, n });
2559
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2538
2560
  }
2539
- return try log.fail(ident, .None);
2561
+ return log.fail(.{ ident, .None });
2540
2562
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2563
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2564
  const stat = unmodifiedStats(battle, side).spc;
2543
2565
  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);
2566
+ try log.boost(.{ ident, .SpecialAttack, n });
2567
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2568
  },
2547
2569
  .EvasionUp1 => {
2548
2570
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2571
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2572
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2573
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2574
  },
2553
2575
  else => unreachable,
2554
2576
  }
@@ -2563,7 +2585,7 @@ pub const Effects = struct {
2563
2585
  const foe_ident = battle.active(player.foe());
2564
2586
  const secondary = move.effect.isStatDownChance();
2565
2587
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2588
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2589
 
2568
2590
  if (secondary) {
2569
2591
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2593,6 @@ pub const Effects = struct {
2571
2593
  } else {
2572
2594
  // checkHit already checks for Invulnerable
2573
2595
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2596
  }
2576
2597
 
2577
2598
  var stats = &foe.active.stats;
@@ -2581,82 +2602,82 @@ pub const Effects = struct {
2581
2602
  switch (move.effect) {
2582
2603
  .AttackDown1, .AttackDownChance => {
2583
2604
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2605
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2606
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2607
  if (stats.atk == 1) {
2587
2608
  boosts.atk += 1;
2588
2609
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2610
+ try log.boost(.{ foe_ident, .Attack, -1 });
2611
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2612
  }
2592
- return try log.fail(foe_ident, .None);
2613
+ return log.fail(.{ foe_ident, .None });
2593
2614
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2615
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2616
  const stat = unmodifiedStats(battle, foe).atk;
2596
2617
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2618
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2619
  },
2599
2620
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2621
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2622
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2623
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2624
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2625
  if (stats.def == 1) {
2605
2626
  boosts.def += 1;
2606
2627
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2628
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2629
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2630
  }
2610
- return try log.fail(foe_ident, .None);
2631
+ return log.fail(.{ foe_ident, .None });
2611
2632
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2633
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2634
  const stat = unmodifiedStats(battle, foe).def;
2614
2635
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2636
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2637
  },
2617
2638
  .SpeedDown1, .SpeedDownChance => {
2618
2639
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2640
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2641
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2642
  if (stats.spe == 1) {
2622
2643
  boosts.spe += 1;
2623
2644
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2645
+ try log.boost(.{ foe_ident, .Speed, -1 });
2646
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2647
  }
2627
- return try log.fail(foe_ident, .None);
2648
+ return log.fail(.{ foe_ident, .None });
2628
2649
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2650
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2651
  const stat = unmodifiedStats(battle, foe).spe;
2631
2652
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2653
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2654
  assert(boosts.spe >= -6);
2634
2655
  },
2635
2656
  .SpecialDownChance => {
2636
2657
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2658
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2659
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2660
  if (stats.spc == 1) {
2640
2661
  boosts.spc += 1;
2641
2662
  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);
2663
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2664
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2665
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2666
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2667
  }
2647
- return try log.fail(foe_ident, .None);
2668
+ return log.fail(.{ foe_ident, .None });
2648
2669
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2670
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2671
  const stat = unmodifiedStats(battle, foe).spc;
2651
2672
  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);
2673
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2674
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2675
  },
2655
2676
  .AccuracyDown1 => {
2656
2677
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2678
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2679
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2680
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2681
  },
2661
2682
  else => unreachable,
2662
2683
  }
@@ -2680,40 +2701,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2701
  }
2681
2702
  }
2682
2703
 
2683
- inline fn isForced(active: anytype) bool {
2704
+ fn isForced(active: anytype) bool {
2684
2705
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2706
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2707
  }
2687
2708
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2709
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2710
  var log = options.log;
2690
2711
  var side = battle.side(who);
2691
2712
  var volatiles = &side.active.volatiles;
2692
2713
  const ident = battle.active(who);
2693
2714
 
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);
2715
+ if (volatiles.disable_move != 0) {
2716
+ volatiles.disable_move = 0;
2717
+ volatiles.disable_duration = 0;
2718
+ options.chance.observe(.disable, who, .None);
2719
+ try log.end(.{ ident, .DisableSilent });
2700
2720
  }
2701
2721
  if (volatiles.Confusion) {
2702
2722
  // volatiles.confusion is left unchanged
2703
2723
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2724
+ options.chance.observe(.confusion, who, .None);
2725
+ try log.end(.{ ident, .ConfusionSilent });
2705
2726
  }
2706
2727
  if (volatiles.Mist) {
2707
2728
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2729
+ try log.end(.{ ident, .MistSilent });
2709
2730
  }
2710
2731
  if (volatiles.FocusEnergy) {
2711
2732
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2733
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2734
  }
2714
2735
  if (volatiles.LeechSeed) {
2715
2736
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2737
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2738
  }
2718
2739
  if (!showdown and volatiles.Toxic) {
2719
2740
  volatiles.Toxic = false;
@@ -2721,24 +2742,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2742
  }
2722
2743
  if (volatiles.LightScreen) {
2723
2744
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2745
+ try log.end(.{ ident, .LightScreenSilent });
2725
2746
  }
2726
2747
  if (volatiles.Reflect) {
2727
2748
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2749
+ try log.end(.{ ident, .ReflectSilent });
2729
2750
  }
2730
2751
  if (showdown and volatiles.Toxic) {
2731
2752
  volatiles.Toxic = false;
2732
2753
  // Pokémon Showdown erroneously clears the toxic counter
2733
2754
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2755
+ try log.end(.{ ident, .ToxicSilent });
2735
2756
  }
2736
2757
  }
2737
2758
 
2759
+ fn decrement(
2760
+ comptime field: chance.Action.Field,
2761
+ player: Player,
2762
+ options: anytype,
2763
+ n: anytype,
2764
+ ) @TypeOf(n) {
2765
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2766
+ .started, .ended => 0,
2767
+ .continuing => if (n > 1) n - 1 else n,
2768
+ else => unreachable,
2769
+ } else n - 1;
2770
+ }
2771
+
2738
2772
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2773
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2774
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2775
  player == .P1
2776
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2777
+ battle.rng.speedTie()
2742
2778
  else if (showdown)
2743
2779
  battle.rng.range(u8, 0, 2) == 0
2744
2780
  else
@@ -2748,21 +2784,31 @@ pub const Rolls = struct {
2748
2784
  return p1;
2749
2785
  }
2750
2786
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2787
+ fn criticalHit(
2788
+ battle: anytype,
2789
+ player: Player,
2790
+ rate: u8,
2791
+ spurious: bool,
2792
+ options: anytype,
2793
+ ) !bool {
2752
2794
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2795
  val == .true
2796
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2797
+ battle.rng.criticalHit(player, rate)
2754
2798
  else if (showdown)
2755
2799
  battle.rng.chance(u8, rate, 256)
2756
2800
  else
2757
2801
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2802
 
2759
- try options.chance.criticalHit(player, crit, rate);
2803
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2804
  return crit;
2761
2805
  }
2762
2806
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2807
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2808
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2809
  val
2810
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2811
+ battle.rng.damage(player)
2766
2812
  else roll: {
2767
2813
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2814
  while (true) {
@@ -2776,21 +2822,27 @@ pub const Rolls = struct {
2776
2822
  return roll;
2777
2823
  }
2778
2824
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2825
+ const uN = if (pkmn.options.miss) u8 else u9;
2826
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2827
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2780
2828
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2829
  val == .true
2830
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2831
+ battle.rng.hit(player, acc)
2782
2832
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2833
+ battle.rng.chance(uN, acc, 256)
2784
2834
  else
2785
- battle.rng.next() < accuracy;
2835
+ battle.rng.next() < acc;
2786
2836
 
2787
- options.chance.hit(ok, accuracy);
2837
+ try options.chance.hit(player, ok, acc);
2788
2838
  return ok;
2789
2839
  }
2790
2840
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2841
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2842
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2843
  val == .true
2844
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2845
+ battle.rng.confused(player)
2794
2846
  else if (showdown)
2795
2847
  !battle.rng.chance(u8, 128, 256)
2796
2848
  else
@@ -2800,9 +2852,11 @@ pub const Rolls = struct {
2800
2852
  return cfz;
2801
2853
  }
2802
2854
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2855
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2856
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2857
  val == .true
2858
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2859
+ battle.rng.paralyzed(player)
2806
2860
  else if (showdown)
2807
2861
  battle.rng.chance(u8, 63, 256)
2808
2862
  else
@@ -2812,9 +2866,11 @@ pub const Rolls = struct {
2812
2866
  return par;
2813
2867
  }
2814
2868
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2869
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2870
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2871
  val == .true
2872
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2873
+ battle.rng.secondaryChance(player, 25)
2818
2874
  else if (showdown)
2819
2875
  battle.rng.chance(u8, 25, 256)
2820
2876
  else
@@ -2824,11 +2880,13 @@ pub const Rolls = struct {
2824
2880
  return proc;
2825
2881
  }
2826
2882
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2883
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2884
  const rate: u8 = if (low) 26 else 77;
2829
2885
 
2830
2886
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2887
  val == .true
2888
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2889
+ battle.rng.secondaryChance(player, rate)
2832
2890
  else if (showdown)
2833
2891
  battle.rng.chance(u8, rate, 256)
2834
2892
  else
@@ -2838,11 +2896,13 @@ pub const Rolls = struct {
2838
2896
  return proc;
2839
2897
  }
2840
2898
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2899
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2900
  const rate: u8 = if (low) 52 else 103;
2843
2901
 
2844
2902
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2903
  val == .true
2904
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2905
+ battle.rng.secondaryChance(player, rate)
2846
2906
  else if (showdown)
2847
2907
  battle.rng.chance(u8, rate, 256)
2848
2908
  else
@@ -2852,9 +2912,11 @@ pub const Rolls = struct {
2852
2912
  return proc;
2853
2913
  }
2854
2914
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2915
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2916
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2917
  val == .true
2918
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2919
+ battle.rng.secondaryChance(player, 85)
2858
2920
  else if (showdown)
2859
2921
  battle.rng.chance(u8, 85, 256)
2860
2922
  else
@@ -2864,14 +2926,14 @@ pub const Rolls = struct {
2864
2926
  return proc;
2865
2927
  }
2866
2928
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2929
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2930
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2931
  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: {
2932
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2933
+ battle.rng.metronome(player)
2934
+ else if (showdown)
2935
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2936
+ else move: {
2875
2937
  while (true) {
2876
2938
  const r = battle.rng.next();
2877
2939
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2946,11 @@ pub const Rolls = struct {
2884
2946
  return move;
2885
2947
  }
2886
2948
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2949
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2950
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2951
  val - 1
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2953
+ battle.rng.psywave(player)
2890
2954
  else if (showdown)
2891
2955
  battle.rng.range(u8, 0, max)
2892
2956
  else power: {
@@ -2901,11 +2965,14 @@ pub const Rolls = struct {
2901
2965
  return power;
2902
2966
  }
2903
2967
 
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)
2968
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2969
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2970
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2971
+ else
2972
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2973
+ battle.rng.sleepDuration(player)
2907
2974
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2975
+ battle.rng.range(u3, 1, 8)
2909
2976
  else duration: {
2910
2977
  while (true) {
2911
2978
  const r = battle.rng.next() & 7;
@@ -2914,60 +2981,74 @@ pub const Rolls = struct {
2914
2981
  };
2915
2982
 
2916
2983
  assert(duration >= 1 and duration <= 7);
2917
- options.chance.duration(.sleep, player, player.foe(), duration);
2984
+ options.chance.duration(player, duration);
2918
2985
  return duration;
2919
2986
  }
2920
2987
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
- val
2988
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2989
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2990
+ if (val >= 1 and val <= 8) val else null
2991
+ else
2992
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2993
+ battle.rng.disableDuration(player)
2924
2994
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2995
+ battle.rng.range(u4, 1, 9)
2926
2996
  else
2927
2997
  @intCast((battle.rng.next() & 7) + 1);
2928
2998
 
2929
2999
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
3000
+ options.chance.duration(player, duration);
2931
3001
  return duration;
2932
3002
  }
2933
3003
 
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)
3004
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3005
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3006
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3007
+ else
3008
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3009
+ battle.rng.confusionDuration(player)
2937
3010
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
3011
+ battle.rng.range(u3, 2, 6)
2939
3012
  else
2940
3013
  @intCast((battle.rng.next() & 3) + 2);
2941
3014
 
2942
3015
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3016
+ options.chance.duration(player, duration);
2944
3017
  return duration;
2945
3018
  }
2946
3019
 
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)
3020
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3021
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3022
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3023
+ else
3024
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3025
+ battle.rng.attackingDuration(player)
2950
3026
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
3027
+ battle.rng.range(u3, 2, 4)
2952
3028
  else
2953
3029
  @intCast((battle.rng.next() & 1) + 2);
2954
3030
 
2955
3031
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3032
+ options.chance.duration(player, duration);
2957
3033
  return duration;
2958
3034
  }
2959
3035
 
2960
- inline fn distribution(
3036
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3037
+
3038
+ fn distribution(
2961
3039
  battle: anytype,
2962
3040
  comptime effect: Move.Effect,
2963
3041
  player: Player,
2964
3042
  options: anytype,
2965
3043
  ) !u3 {
2966
3044
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
- @intCast(val)
3045
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3046
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3047
+ else
3048
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3049
+ battle.rng.distribution(player)
2969
3050
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3051
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3052
  else n: {
2972
3053
  const r = (battle.rng.next() & 3);
2973
3054
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3056,7 @@ pub const Rolls = struct {
2975
3056
 
2976
3057
  assert(n >= 2 and n <= 5);
2977
3058
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3059
+ options.chance.duration(player, n);
2979
3060
  } else {
2980
3061
  try options.chance.multiHit(player, n);
2981
3062
  }
@@ -2990,15 +3071,15 @@ pub const Rolls = struct {
2990
3071
  options: anytype,
2991
3072
  ) !u4 {
2992
3073
  // TODO: consider throwing error instead of rerolling?
2993
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2994
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2995
3076
  val
2996
3077
  else
2997
3078
  null
2998
3079
  else
2999
- null;
3000
-
3001
- const slot: u4 = overridden orelse slot: {
3080
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3081
+ battle.rng.moveSlot(player, moves, check_pp)
3082
+ else slot: {
3002
3083
  if (showdown) {
3003
3084
  if (check_pp == 0) {
3004
3085
  var i: usize = moves.len;
@@ -3014,10 +3095,9 @@ pub const Rolls = struct {
3014
3095
  while (i < moves.len and r > 0) : (i += 1) {
3015
3096
  if (moves[i].pp > 0) {
3016
3097
  r -= 1;
3017
- if (r == 0) break;
3098
+ if (r == 0) break :slot @intCast(i + 1);
3018
3099
  }
3019
3100
  }
3020
- break :slot @intCast(i + 1);
3021
3101
  }
3022
3102
  }
3023
3103
 
@@ -3039,11 +3119,11 @@ test "RNG agreement" {
3039
3119
  expected[i] = @intCast(i * 0x1000000);
3040
3120
  }
3041
3121
 
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 };
3122
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3123
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3124
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3125
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3126
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3127
 
3048
3128
  for (0..expected.len) |i| {
3049
3129
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3159,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3159
  const side = battle.side(player);
3080
3160
  const foe = battle.foe(player);
3081
3161
 
3082
- var active = &side.active;
3162
+ const active = &side.active;
3083
3163
  const stored = side.stored();
3084
3164
 
3085
3165
  // 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.
3166
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3167
  //
3088
3168
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3169
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3171,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3171
  // passing due to being forced into using a move. Instead of introducing another option
3092
3172
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3173
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3174
+ // switch at all
3095
3175
  if (isForced(active)) {
3096
3176
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3177
  n += 1;
@@ -3110,7 +3190,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3190
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3191
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3192
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3193
+ // move normally if sleeping/frozen/bound
3114
3194
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3195
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3196
  {
@@ -3123,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3203
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3204
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3205
  // 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.
3206
+ // proc-ed move in their moveset) and disabling all other moves
3127
3207
  if (limited) {
3128
3208
  assert(showdown);
3129
3209
  assert(side.last_selected_move != .None);
@@ -3133,7 +3213,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3213
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3214
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3215
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3216
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3217
  const s = if (struggle) 0 else slot;
3138
3218
  out[n] = .{ .type = .Move, .data = s };
3139
3219
  n += 1;
@@ -3148,14 +3228,14 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3228
  const m = active.moves[slot - 1];
3149
3229
  if (m.id == .None) break;
3150
3230
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3231
+ if (active.volatiles.disable_move == slot) continue;
3152
3232
  out[n] = .{ .type = .Move, .data = slot };
3153
3233
  n += 1;
3154
3234
  }
3155
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3156
3236
  if (n == before) {
3157
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3158
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3159
3239
  while (slot <= 4) : (slot += 1) {
3160
3240
  if (active.moves[slot - 1].pp != 0) return n;
3161
3241
  }