@pkmn/engine 0.1.0-dev.c5fe6d07 → 0.1.0-dev.cdc837af

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 (79) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +46 -19
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +44 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +97 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +21 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +22 -0
  13. package/build/pkg/addon.js +35 -58
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/ids.json +12 -12
  16. package/build/pkg/data/layout.json +2 -2
  17. package/build/pkg/data/protocol.json +69 -12
  18. package/build/pkg/data.d.ts +27 -5
  19. package/build/pkg/data.js.map +1 -1
  20. package/build/pkg/gen1.js +3 -3
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +24 -10
  23. package/build/pkg/index.js +15 -10
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +123 -102
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +36 -68
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +141 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +141 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +349 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +18 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +259 -107
  39. package/package.json +72 -32
  40. package/src/bin/install-pkmn-engine +29 -19
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/common/array.zig +77 -0
  43. package/src/lib/common/data.zig +12 -24
  44. package/src/lib/common/debug.zig +39 -8
  45. package/src/lib/common/js.zig +4 -2
  46. package/src/lib/common/optional.zig +21 -10
  47. package/src/lib/common/options.zig +4 -0
  48. package/src/lib/common/protocol.zig +808 -332
  49. package/src/lib/common/rational.zig +132 -36
  50. package/src/lib/common/rng.zig +22 -15
  51. package/src/lib/common/util.zig +50 -1
  52. package/src/lib/gen1/calc.zig +688 -162
  53. package/src/lib/gen1/chance.zig +436 -295
  54. package/src/lib/gen1/data/moves.zig +193 -183
  55. package/src/lib/gen1/data/species.zig +4 -6
  56. package/src/lib/gen1/data/types.zig +8 -9
  57. package/src/lib/gen1/data.zig +55 -70
  58. package/src/lib/gen1/helpers.zig +91 -562
  59. package/src/lib/gen1/mechanics.zig +610 -561
  60. package/src/lib/gen2/calc.zig +153 -0
  61. package/src/lib/gen2/chance.zig +902 -0
  62. package/src/lib/gen2/data/items.zig +13 -21
  63. package/src/lib/gen2/data/moves.zig +664 -572
  64. package/src/lib/gen2/data/species.zig +2 -4
  65. package/src/lib/gen2/data/types.zig +135 -33
  66. package/src/lib/gen2/data.zig +115 -91
  67. package/src/lib/gen2/generated.zig +801 -0
  68. package/src/lib/gen2/helpers.zig +54 -37
  69. package/src/lib/gen2/mechanics.zig +3569 -29
  70. package/src/lib/{binding/node.zig → node.zig} +9 -14
  71. package/src/lib/pkmn.zig +36 -15
  72. package/src/lib/{binding/wasm.zig → wasm.zig} +1 -1
  73. package/src/tools/display/dom.ts +96 -0
  74. package/src/tools/display/ui.css +354 -0
  75. package/build/test/display.js +0 -400
  76. package/build/test/display.js.map +0 -1
  77. package/src/lib/gen1/README.md +0 -892
  78. package/src/lib/gen2/README.md +0 -308
  79. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,38 +1,28 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
5
+ const data = @import("data.zig");
6
+ const pkmn = @import("../pkmn.zig");
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
-
10
- const Action = @import("chance.zig").Action;
11
-
12
- const data = @import("data.zig");
9
+ const std = @import("std");
13
10
 
14
11
  const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
- const showdown = pkmn.options.showdown;
24
-
25
12
  const Boost = protocol.Boost;
13
+ const Choice = common.Choice;
26
14
  const Damage = protocol.Damage;
27
- const Heal = protocol.Heal;
28
-
29
- const Gen12 = rng.Gen12;
30
-
31
- const ActivePokemon = data.ActivePokemon;
32
15
  const Effectiveness = data.Effectiveness;
16
+ const expectEqual = std.testing.expectEqual;
17
+ const Fail = protocol.Fail;
18
+ const Gen12 = rng.Gen12;
19
+ const Heal = protocol.Heal;
20
+ const ID = common.ID;
33
21
  const Move = data.Move;
34
22
  const MoveSlot = data.MoveSlot;
35
- const Pokemon = data.Pokemon;
23
+ const Player = common.Player;
24
+ const Result = common.Result;
25
+ const showdown = pkmn.options.showdown;
36
26
  const Side = data.Side;
37
27
  const Species = data.Species;
38
28
  const Stats = data.Stats;
@@ -59,34 +49,26 @@ const BOOSTS = &[_][2]u8{
59
49
 
60
50
  const MAX_STAT_VALUE = 999;
61
51
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
52
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
53
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
54
  if (battle.turn == 0) return start(battle, options);
71
55
 
72
56
  var s1 = false;
73
57
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
58
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
59
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
60
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
61
 
80
62
  var p1 = battle.side(.P1);
81
63
  var p2 = battle.side(.P2);
82
64
 
83
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
84
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
65
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
66
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
67
 
86
68
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
69
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
70
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
71
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
72
  }
91
73
 
92
74
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -99,11 +81,11 @@ fn start(battle: anytype, options: anytype) !Result {
99
81
  const p1 = battle.side(.P1);
100
82
  const p2 = battle.side(.P2);
101
83
 
102
- var p1_slot = findFirstAlive(p1);
84
+ const p1_slot = findFirstAlive(p1);
103
85
  assert(!showdown or p1_slot == 1);
104
86
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
105
87
 
106
- var p2_slot = findFirstAlive(p2);
88
+ const p2_slot = findFirstAlive(p2);
107
89
  assert(!showdown or p2_slot == 1);
108
90
  if (p2_slot == 0) return Result.Win;
109
91
 
@@ -124,7 +106,6 @@ fn selectMove(
124
106
  choice: Choice,
125
107
  foe_choice: Choice,
126
108
  skip_turn: *bool,
127
- from: *?Move,
128
109
  ) ?Result {
129
110
  if (choice.type == .Pass) return null;
130
111
 
@@ -141,7 +122,6 @@ fn selectMove(
141
122
  return null;
142
123
  }
143
124
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
125
  if (showdown) {
146
126
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
127
  saveMove(battle, player, null);
@@ -151,7 +131,6 @@ fn selectMove(
151
131
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
132
  if (!showdown) volatiles.Flinch = false;
153
133
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
134
  if (showdown) {
156
135
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
136
  saveMove(battle, player, null);
@@ -172,15 +151,13 @@ fn selectMove(
172
151
  return null;
173
152
  }
174
153
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
154
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
155
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
156
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
157
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
181
158
  // set the selected move will not actually be used, it will just be reported as having
182
159
  // been used (this differs from how Pokémon Showdown works, but its impossible to
183
- // replicate the incorrect behavior with the correct mechanisms).
160
+ // replicate the incorrect behavior with the correct mechanisms)
184
161
  saveMove(battle, player, choice);
185
162
  } else {
186
163
  assert(choice.data == 0);
@@ -210,16 +187,13 @@ fn selectMove(
210
187
  if (choice.data == 0) {
211
188
  const struggle = ok: {
212
189
  for (side.active.moves, 0..) |move, i| {
213
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
190
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
214
191
  }
215
192
  break :ok true;
216
193
  };
217
-
218
194
  assert(struggle);
219
- saveMove(battle, player, choice);
220
- } else {
221
- saveMove(battle, player, choice);
222
195
  }
196
+ saveMove(battle, player, choice);
223
197
 
224
198
  return null;
225
199
  }
@@ -232,7 +206,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
232
206
  if (c.data == 0) {
233
207
  side.last_selected_move = .Struggle;
234
208
  } else {
235
- assert(showdown or side.active.volatiles.disabled_move != c.data);
209
+ assert(showdown or side.active.volatiles.disable_move != c.data);
236
210
  const move = side.active.move(c.data);
237
211
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
238
212
  // sometimes required...), but a 0 PP move can be used automatically
@@ -270,17 +244,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
270
244
  active.moves = incoming.moves;
271
245
 
272
246
  statusModify(incoming.status, &active.stats);
247
+ options.chance.switched(player, slot);
273
248
 
274
249
  foe.active.volatiles.Binding = false;
275
250
 
276
- try options.log.switched(battle.active(player), incoming);
277
- options.chance.switched(player, side.order[0], out);
251
+ try options.log.switched(.{ battle.active(player), incoming });
278
252
 
279
253
  if (showdown and incoming.status == Status.TOX) {
280
254
  incoming.status = Status.init(.PSN);
281
255
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
282
256
  // rather not clutter up turnOrder just for this (incorrect) log message
283
- try options.log.status(battle.active(player), incoming.status, .Silent);
257
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
284
258
  }
285
259
  }
286
260
 
@@ -343,12 +317,10 @@ fn doTurn(
343
317
  player_choice: Choice,
344
318
  player_rewrap: bool,
345
319
  player_skip: bool,
346
- player_from: ?Move,
347
320
  foe_player: Player,
348
321
  foe_choice: Choice,
349
322
  foe_rewrap: bool,
350
323
  foe_skip: bool,
351
- foe_from: ?Move,
352
324
  options: anytype,
353
325
  ) !?Result {
354
326
  assert(player_choice.type != .Pass);
@@ -361,19 +333,18 @@ fn doTurn(
361
333
  player_choice,
362
334
  player_rewrap,
363
335
  player_skip,
364
- player_from,
365
336
  &residual,
366
337
  options,
367
338
  )) |r| return r;
368
339
  if (!replace) {
369
340
  if (player_choice.type != .Switch) {
370
- if (try checkFaint(battle, foe_player, options)) |r| return r;
341
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
371
342
  }
372
343
  if (residual) try handleResidual(battle, player, options);
373
- if (try checkFaint(battle, player, options)) |r| return r;
344
+ if (try checkFaint(battle, player, false, options)) |r| return r;
374
345
  } else if (foe_choice.type == .Pass) return null;
375
346
 
376
- options.chance.clearPending();
347
+ if (!showdown) options.chance.clearPending();
377
348
 
378
349
  residual = true;
379
350
  replace = battle.side(foe_player).stored().hp == 0;
@@ -384,16 +355,15 @@ fn doTurn(
384
355
  foe_choice,
385
356
  foe_rewrap,
386
357
  foe_skip,
387
- foe_from,
388
358
  &residual,
389
359
  options,
390
360
  )) |r| return r;
391
361
  if (!replace) {
392
362
  if (!calc and foe_choice.type != .Switch) {
393
- if (try checkFaint(battle, player, options)) |r| return r;
363
+ if (try checkFaint(battle, player, true, options)) |r| return r;
394
364
  }
395
365
  if (residual) try handleResidual(battle, foe_player, options);
396
- if (try checkFaint(battle, foe_player, options)) |r| return r;
366
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
397
367
  }
398
368
 
399
369
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -412,7 +382,6 @@ fn executeMove(
412
382
  choice: Choice,
413
383
  rewrap: bool,
414
384
  skip: bool,
415
- from: ?Move,
416
385
  residual: *bool,
417
386
  options: anytype,
418
387
  ) !?Result {
@@ -429,7 +398,7 @@ fn executeMove(
429
398
  if (side.last_selected_move == .SKIP_TURN) {
430
399
  assert(!showdown);
431
400
  if (battle.foe(player).active.volatiles.Binding) {
432
- try options.log.cant(battle.active(player), .Bound);
401
+ try options.log.cant(.{ battle.active(player), .Bound });
433
402
  }
434
403
  return null;
435
404
  }
@@ -447,11 +416,13 @@ fn executeMove(
447
416
  mslot = @intCast(battle.lastMove(player).index);
448
417
  const stored = side.stored();
449
418
  // 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);
419
+ auto = isForced(&side.active) or
420
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
421
+ side.last_selected_move == .HyperBeam or
422
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
452
423
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
453
424
  // 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.
425
+ // index 1 and the current Pokémon has the same move in its first slot
455
426
  if (!auto) {
456
427
  // side.active.moves(slot) is safe to check even though the slot in question might not
457
428
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -470,7 +441,7 @@ fn executeMove(
470
441
 
471
442
  var skip_can = false;
472
443
  var skip_pp = false;
473
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
444
+ switch (try beforeMove(battle, player, skip, residual, options)) {
474
445
  .done => return null,
475
446
  .skip_can => skip_can = true,
476
447
  .skip_pp => skip_pp = true,
@@ -478,9 +449,9 @@ fn executeMove(
478
449
  .err => return @as(?Result, Result.Error),
479
450
  }
480
451
 
481
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
482
- return null;
483
- }
452
+ const can = skip_can or
453
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
454
+ if (!can) return null;
484
455
 
485
456
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
486
457
  }
@@ -491,7 +462,6 @@ fn beforeMove(
491
462
  battle: anytype,
492
463
  player: Player,
493
464
  skip: bool,
494
- from: ?Move,
495
465
  residual: *bool,
496
466
  options: anytype,
497
467
  ) !BeforeMove {
@@ -505,36 +475,41 @@ fn beforeMove(
505
475
 
506
476
  if (Status.is(stored.status, .SLP)) {
507
477
  const before = stored.status;
478
+ const slf = Status.is(stored.status, .EXT);
508
479
  // 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;
480
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
481
+ .started, .ended => stored.status = 0,
482
+ .continuing => if (Status.duration(stored.status) > 1) {
483
+ stored.status -= 1;
484
+ },
485
+ else => unreachable,
511
486
  } else {
512
487
  stored.status -= 1;
513
488
  }
514
489
 
515
490
  const duration = Status.duration(stored.status);
516
- if (!Status.is(stored.status, .EXT)) {
517
- try options.chance.sleep(player, duration);
518
- }
491
+ try options.chance.sleep(player, if (slf)
492
+ .None
493
+ else if (duration == 0) .ended else .continuing);
519
494
 
520
495
  if (duration == 0) {
521
- try log.curestatus(ident, before, .Message);
496
+ try log.curestatus(.{ ident, before, .Message });
522
497
  stored.status = 0; // clears EXT if present
523
498
  } else {
524
- try log.cant(ident, .Sleep);
499
+ try log.cant(.{ ident, .Sleep });
525
500
  }
526
501
  side.last_used_move = .None;
527
502
  return .done;
528
503
  }
529
504
 
530
505
  if (Status.is(stored.status, .FRZ)) {
531
- try log.cant(ident, .Freeze);
506
+ try log.cant(.{ ident, .Freeze });
532
507
  side.last_used_move = .None;
533
508
  return .done;
534
509
  }
535
510
 
536
511
  if (skip or foe.active.volatiles.Binding) {
537
- try log.cant(ident, .Bound);
512
+ try log.cant(.{ ident, .Bound });
538
513
  return .done;
539
514
  }
540
515
 
@@ -542,27 +517,27 @@ fn beforeMove(
542
517
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
543
518
  // Pokémon can sometimes flinch multiple times from the same original hit
544
519
  if (!showdown) volatiles.Flinch = false;
545
- try log.cant(ident, .Flinch);
520
+ try log.cant(.{ ident, .Flinch });
546
521
  return .done;
547
522
  }
548
523
 
549
524
  if (volatiles.Recharging) {
550
525
  volatiles.Recharging = false;
551
- try log.cant(ident, .Recharge);
526
+ try log.cant(.{ ident, .Recharge });
552
527
  return .done;
553
528
  }
554
529
 
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);
530
+ if (volatiles.disable_duration > 0) {
531
+ volatiles.disable_duration =
532
+ decrement(.disable, player, options, volatiles.disable_duration);
533
+ try options.chance.disable(
534
+ player,
535
+ if (volatiles.disable_duration == 0) .ended else .continuing,
536
+ );
562
537
 
563
- if (volatiles.disabled_duration == 0) {
564
- volatiles.disabled_move = 0;
565
- try log.end(ident, .Disable);
538
+ if (volatiles.disable_duration == 0) {
539
+ volatiles.disable_move = 0;
540
+ try log.end(.{ ident, .Disable });
566
541
  }
567
542
  }
568
543
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -577,19 +552,24 @@ fn beforeMove(
577
552
 
578
553
  if (volatiles.Confusion) {
579
554
  assert(volatiles.confusion > 0);
580
-
581
- if (options.calc.modify(player, .confusion)) |extend| {
582
- if (!extend) volatiles.confusion = 0;
555
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
556
+ .started, .ended => volatiles.confusion = 0,
557
+ .continuing => if (volatiles.confusion > 1) {
558
+ volatiles.confusion -= 1;
559
+ },
560
+ .overwritten => {},
561
+ else => unreachable,
583
562
  } else {
584
563
  volatiles.confusion -= 1;
585
564
  }
586
- try options.chance.confusion(player, volatiles.confusion);
565
+
566
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
587
567
 
588
568
  if (volatiles.confusion == 0) {
589
569
  volatiles.Confusion = false;
590
- try log.end(ident, .Confusion);
570
+ try log.end(.{ ident, .Confusion });
591
571
  } else {
592
- try log.activate(ident, .Confusion);
572
+ try log.activate(.{ ident, .Confusion });
593
573
 
594
574
  if (try Rolls.confused(battle, player, options)) {
595
575
  assert(!volatiles.MultiHit);
@@ -601,7 +581,9 @@ fn beforeMove(
601
581
  volatiles.Charging = false;
602
582
  volatiles.Binding = false;
603
583
  volatiles.Invulnerable = false;
604
- options.chance.clearDurations(player, null);
584
+
585
+ options.chance.observe(.attacking, player, .None);
586
+ options.chance.observe(.binding, player, .None);
605
587
  {
606
588
  // This feels (and is) disgusting but the cartridge literally just overwrites
607
589
  // the opponent's defense with the user's defense and resets it after. As a
@@ -609,7 +591,9 @@ fn beforeMove(
609
591
  const def = foe.active.stats.def;
610
592
  foe.active.stats.def = active.stats.def;
611
593
  defer foe.active.stats.def = def;
612
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
594
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
595
+ return .err;
596
+ }
613
597
  }
614
598
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
615
599
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -638,9 +622,12 @@ fn beforeMove(
638
622
  volatiles.Thrashing = false;
639
623
  volatiles.Charging = false;
640
624
  volatiles.Binding = false;
641
- options.chance.clearDurations(player, null);
625
+
626
+ options.chance.observe(.attacking, player, .None);
627
+ options.chance.observe(.binding, player, .None);
628
+
642
629
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
643
- try log.cant(ident, .Paralysis);
630
+ try log.cant(.{ ident, .Paralysis });
644
631
  return .done;
645
632
  }
646
633
 
@@ -656,28 +643,22 @@ fn beforeMove(
656
643
  volatiles.state +%= battle.last_damage;
657
644
  }
658
645
 
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);
646
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
647
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
667
648
 
668
649
  if (volatiles.attacks != 0) {
669
- try log.activate(ident, .Bide);
650
+ try log.activate(.{ ident, .Bide });
670
651
  return .done;
671
652
  }
672
653
 
673
654
  volatiles.Bide = false;
674
- try log.end(ident, .Bide);
655
+ try log.end(.{ ident, .Bide });
675
656
 
676
657
  battle.last_damage = volatiles.state *% 2;
677
658
  volatiles.state = 0;
678
659
 
679
660
  if (battle.last_damage == 0) {
680
- try log.fail(ident, .None);
661
+ try log.fail(.{ ident, .None });
681
662
  return .done;
682
663
  }
683
664
 
@@ -697,47 +678,42 @@ fn beforeMove(
697
678
  }
698
679
 
699
680
  if (volatiles.Thrashing) {
700
- assert(volatiles.attacks > 0);
701
- assert(from != null);
681
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
702
682
 
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);
683
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
684
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
709
685
 
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
- }
686
+ if (volatiles.attacks == 0) {
687
+ const overwritten = volatiles.Confusion;
688
+
689
+ volatiles.Thrashing = false;
690
+ volatiles.Confusion = true;
691
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
692
+
693
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
694
+ // ended (the engine's protocol is always from the omniscient perspective and clients
695
+ // can be expected to recognize this pattern and pass the information only to the
696
+ // correct player)
697
+ try log.start(.{ battle.active(player), .ConfusionSilent });
698
+ options.chance.observe(.confusion, player, if (overwritten)
699
+ if (pkmn.options.overwrite) .overwritten else .continuing
700
+ else
701
+ .started);
702
+ if (!overwritten) options.calc.confusion(player);
721
703
  }
704
+
705
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
706
+ // used in useMove and doesn't have the notion of skipping canMove semantics
707
+ if (showdown) side.last_used_move = side.last_selected_move;
708
+
722
709
  return .skip_can;
723
710
  }
724
711
 
725
712
  if (volatiles.Binding) {
726
- assert(volatiles.attacks > 0);
727
-
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);
713
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
714
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
734
715
 
735
- try log.move(
736
- battle.active(player),
737
- side.last_selected_move,
738
- battle.active(player.foe()),
739
- side.last_selected_move,
740
- );
716
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
741
717
  if (showdown or battle.last_damage != 0) {
742
718
  const sub = showdown and foe.active.volatiles.Substitute;
743
719
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -757,7 +733,7 @@ fn canMove(
757
733
  mslot: u4,
758
734
  auto: bool,
759
735
  skip_pp: bool,
760
- from: ?Move,
736
+ from: Move,
761
737
  residual: *bool,
762
738
  options: anytype,
763
739
  ) !bool {
@@ -765,33 +741,21 @@ fn canMove(
765
741
  const player_ident = battle.active(player);
766
742
  const move = Move.get(side.last_selected_move);
767
743
 
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
744
  if (side.active.volatiles.Charging) {
774
745
  side.active.volatiles.Charging = false;
775
746
  side.active.volatiles.Invulnerable = false;
776
747
  } else if (move.effect == .Charge) {
777
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
748
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
778
749
  setCounterable(battle, player, side, move);
779
750
  try Effects.charge(battle, player, options);
780
751
  return false;
781
752
  }
782
753
 
783
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
784
- if (!skip) decrementPP(side, mslot, auto);
754
+ side.last_used_move = side.last_selected_move;
755
+ if (!skip_pp) decrementPP(side, mslot, auto);
785
756
 
786
757
  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);
758
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
795
759
  setCounterable(battle, player, side, move);
796
760
 
797
761
  if (move.effect.onBegin()) {
@@ -829,16 +793,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
829
793
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
830
794
  if (volatiles.Bide) return;
831
795
 
832
- assert(active.move(mslot).pp > 0 or auto);
833
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
796
+ var move_slot = active.move(mslot);
797
+ assert(move_slot.pp > 0 or auto);
798
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
834
799
  if (volatiles.Transform) return;
835
800
 
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;
801
+ move_slot = side.stored().move(mslot);
802
+ assert(move_slot.pp > 0 or auto);
803
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
838
804
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
839
805
  }
840
806
 
841
- fn incrementPP(side: *Side, mslot: u4) void {
807
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
842
808
  var active = &side.active;
843
809
  const volatiles = &active.volatiles;
844
810
 
@@ -846,8 +812,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
846
812
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
847
813
  if (showdown and volatiles.Transform) return;
848
814
 
815
+ const n = options.calc.overridden(player, .pp) orelse 1;
816
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
817
+
849
818
  assert(mslot > 0 and mslot <= 4);
850
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
819
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
851
820
  }
852
821
 
853
822
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -895,13 +864,13 @@ fn doMove(
895
864
  break :miss true;
896
865
  }
897
866
  break :miss (move.target != .Self and
898
- !moveHit(battle, player, move, &immune, &mist, options));
867
+ !try moveHit(battle, player, move, &immune, &mist, options));
899
868
  };
900
869
  assert(!immune or miss or (showdown and move.effect == .Binding));
901
870
 
902
871
  var late = showdown and move.effect != .Explode;
903
872
  const skip = status or immune;
904
- if ((!showdown or (!skip or counter)) and !miss) blk: {
873
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
905
874
  if (showdown and move.effect.isMulti()) {
906
875
  try Effects.multiHit(battle, player, move, options);
907
876
  hits = side.active.volatiles.attacks;
@@ -923,8 +892,9 @@ fn doMove(
923
892
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
924
893
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
925
894
  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)) {
895
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
896
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
897
+ if (!showdown) try options.chance.commit(player, .err);
928
898
  return @as(?Result, Result.Error);
929
899
  }
930
900
  if (battle.last_damage == 0) {
@@ -938,11 +908,12 @@ fn doMove(
938
908
  }
939
909
  }
940
910
 
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);
911
+ var missed: bool = undefined;
912
+ const zero = battle.last_damage == 0;
913
+ if (!showdown and !skip) {
914
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
915
+ miss = missed or zero;
916
+ }
946
917
 
947
918
  assert(showdown or miss or battle.last_damage > 0 or skip);
948
919
  assert((!showdown and miss) or !(ohko and immune));
@@ -966,16 +937,37 @@ fn doMove(
966
937
  ohko = (!showdown or (!immune and !invulnerable)) and
967
938
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
968
939
  if (ohko) {
969
- try log.immune(foe_ident, .OHKO);
940
+ try log.immune(.{ foe_ident, .OHKO });
970
941
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
971
- try log.immune(foe_ident, .None);
942
+ try log.immune(.{ foe_ident, .None });
943
+ if (!showdown) {
944
+ if (move.effect == .Binding) {
945
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
946
+ // then simply clears it later on if the move happens to miss in moveHit.
947
+ // However, moveHit can return true and we'll still end up in the outer miss
948
+ // handling block because attacking into an immune Pokemon does zero damage
949
+ // (considered missing)
950
+ assert(zero);
951
+ // Thus we still need to actually save the hit result (but not the crit roll).
952
+ // Using .miss for this commit would accomplish what we want, but is somewhat
953
+ // misleading as its entirely possible the move actually did hit (and the target
954
+ // is now trapped) so we use the .binding value instead which is effectively
955
+ // identical, just doesn't claim to know about the actual results of the hit
956
+ // roll)
957
+ try options.chance.commit(player, .binding);
958
+ } else {
959
+ // The .glitch option allows for conditionally committing if we are in a
960
+ // division-by-zero freeze alternative timeline scenario
961
+ try options.chance.commit(player, .glitch);
962
+ }
963
+ }
972
964
  } else if (mist) {
973
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
974
- try log.fail(foe_ident, .None);
965
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
966
+ try log.fail(.{ foe_ident, .None });
975
967
  } else {
976
- if (showdown or !zero) try options.chance.commit(player, .miss);
977
- try log.lastmiss();
978
- try log.miss(battle.active(player));
968
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
969
+ try log.lastmiss(.{});
970
+ try log.miss(.{battle.active(player)});
979
971
  }
980
972
  if (move.effect == .JumpKick) {
981
973
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -988,17 +980,15 @@ fn doMove(
988
980
  try buildRage(battle, player.foe(), options);
989
981
  } else if (showdown and move.effect == .Disable) {
990
982
  try buildRage(battle, player.foe(), options);
991
- } else if (!showdown and immune and move.effect == .Binding) {
992
- try options.chance.commit(player, .binding);
993
983
  }
994
984
  return null;
995
- } else {
985
+ } else if (!showdown) {
996
986
  try options.chance.commit(player, .hit);
997
987
  }
998
988
 
999
989
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
1000
990
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
1001
- // it up above before damage calculation).
991
+ // it up above before damage calculation)
1002
992
  if (!showdown and move.effect.isMulti()) {
1003
993
  try Effects.multiHit(battle, player, move, options);
1004
994
  hits = side.active.volatiles.attacks;
@@ -1011,15 +1001,15 @@ fn doMove(
1011
1001
  var hit: u4 = 0;
1012
1002
  while (hit < hits) {
1013
1003
  if (hit == 0) {
1014
- if (crit) try log.crit(battle.active(player.foe()));
1004
+ if (crit) try log.crit(.{battle.active(player.foe())});
1015
1005
  if (effectiveness > Effectiveness.neutral) {
1016
- try log.supereffective(battle.active(player.foe()));
1006
+ try log.supereffective(.{battle.active(player.foe())});
1017
1007
  } else if (effectiveness < Effectiveness.neutral) {
1018
- try log.resisted(battle.active(player.foe()));
1008
+ try log.resisted(.{battle.active(player.foe())});
1019
1009
  }
1020
1010
  }
1021
1011
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1022
- if (hit == 0 and ohko) try log.ohko();
1012
+ if (hit == 0 and ohko) try log.ohko(.{});
1023
1013
  hit += 1;
1024
1014
  if (foe.stored().hp == 0) break;
1025
1015
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1036,33 +1026,31 @@ fn doMove(
1036
1026
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1037
1027
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1028
  }
1039
- try log.hitcount(battle.active(player.foe()), hit);
1040
- } else if (showdown) {
1029
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1030
+ } else if (showdown and move.effect == .Binding) {
1041
1031
  // 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;
1032
+ try Effects.binding(battle, player, rewrap, options);
1033
+ if (immune) {
1034
+ battle.last_damage = 0;
1035
+ assert(foe.stored().hp > 0);
1036
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1037
+ if (sub) {
1038
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1039
+ } else {
1040
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1041
+ try buildRage(battle, player.foe(), options);
1055
1042
  }
1043
+ return null;
1056
1044
  }
1057
1045
  }
1058
1046
 
1059
1047
  // 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.
1048
+ // if an effect was intended to "always happen" it will still get skipped
1061
1049
  if (nullified) return null;
1062
1050
 
1063
1051
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1064
1052
  // 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.
1053
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1066
1054
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1067
1055
 
1068
1056
  if (foe.stored().hp == 0) return null;
@@ -1077,7 +1065,7 @@ fn doMove(
1077
1065
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1078
1066
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1079
1067
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1080
- // puts the |-status| message before |-hitcount| instead of after.
1068
+ // puts the |-status| message before |-hitcount| instead of after
1081
1069
  if (move.effect == .Twineedle) {
1082
1070
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1083
1071
  } else if (move.effect == .Disable) {
@@ -1096,12 +1084,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1096
1084
  const side = battle.side(player);
1097
1085
 
1098
1086
  // Base speed is used for the critical hit calculation, even when Transform-ed
1099
- var rate = @as(u16, Species.chance(side.stored().species));
1087
+ var rate: u16 = Species.chance(side.stored().species);
1100
1088
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1101
1089
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1102
1090
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1103
1091
 
1104
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1092
+ const spurious =
1093
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1094
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1105
1095
  }
1106
1096
 
1107
1097
  fn calcDamage(
@@ -1110,6 +1100,7 @@ fn calcDamage(
1110
1100
  target_player: Player,
1111
1101
  m: ?Move.Data,
1112
1102
  crit: bool,
1103
+ options: anytype,
1113
1104
  ) bool {
1114
1105
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1115
1106
  const cfz = m == null;
@@ -1121,28 +1112,8 @@ fn calcDamage(
1121
1112
 
1122
1113
  const special = move.type.special();
1123
1114
 
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
1115
+ var atk = attack(side, crit, special);
1116
+ var def = defense(target, crit, special, cfz);
1146
1117
 
1147
1118
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1148
1119
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1153,9 +1124,9 @@ fn calcDamage(
1153
1124
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1154
1125
  }
1155
1126
 
1156
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1127
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1157
1128
 
1158
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1129
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1159
1130
 
1160
1131
  if (def == 0) return false;
1161
1132
 
@@ -1167,11 +1138,41 @@ fn calcDamage(
1167
1138
  d = @min(997, d);
1168
1139
  d += 2;
1169
1140
 
1141
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1142
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1143
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1144
+ // that results in counter intuitive probabilities in the common case
1145
+ if (pkmn.options.chance and !showdown) {
1146
+ atk = attack(side, !crit, special);
1147
+ def = defense(target, !crit, special, false);
1148
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1149
+ }
1150
+
1170
1151
  battle.last_damage = @intCast(d);
1171
1152
 
1172
1153
  return true;
1173
1154
  }
1174
1155
 
1156
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1157
+ return if (crit)
1158
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1159
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1160
+ }
1161
+
1162
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1163
+ return if (crit)
1164
+ if (special) target.stored().stats.spc else target.stored().stats.def
1165
+ else
1166
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1167
+ if (special)
1168
+ target.active.stats.spc *
1169
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1170
+ else
1171
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1172
+ target.active.stats.def *
1173
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1174
+ }
1175
+
1175
1176
  fn adjustDamage(battle: anytype, player: Player) u16 {
1176
1177
  const side = battle.side(player);
1177
1178
  const foe = battle.foe(player);
@@ -1216,7 +1217,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1216
1217
  const foe = battle.foe(player);
1217
1218
 
1218
1219
  if (!try checkHit(battle, player, move, options)) return null;
1219
- try options.chance.commit(player, .hit);
1220
1220
 
1221
1221
  battle.last_damage = switch (side.last_selected_move) {
1222
1222
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1246,7 +1246,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1246
1246
  const foe = battle.foe(player);
1247
1247
 
1248
1248
  if (battle.last_damage == 0) {
1249
- try options.log.fail(battle.active(player), .None);
1249
+ try options.log.fail(.{ battle.active(player), .None });
1250
1250
  return null;
1251
1251
  }
1252
1252
 
@@ -1264,22 +1264,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1264
1264
  foe_last_selected_move.type == .Fighting);
1265
1265
 
1266
1266
  if (!used and !selected) {
1267
- try options.log.fail(battle.active(player), .None);
1267
+ try options.log.fail(.{ battle.active(player), .None });
1268
1268
  return null;
1269
1269
  }
1270
1270
 
1271
1271
  if (!used or !selected) {
1272
1272
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1273
1273
  if (!showdown) return Result.Error;
1274
- try options.log.fail(battle.active(player), .None);
1274
+ try options.log.fail(.{ battle.active(player), .None });
1275
1275
  return null;
1276
1276
  }
1277
1277
 
1278
1278
  battle.last_damage *|= 2;
1279
1279
 
1280
- // Pokémon Showdown calls checkHit before Counter
1280
+ // Pokémon Showdown calls moveHit before Counter
1281
1281
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1282
- try options.chance.commit(player, .hit);
1283
1282
 
1284
1283
  const sub = showdown and foe.active.volatiles.Substitute;
1285
1284
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1307,20 +1306,20 @@ fn applyDamage(
1307
1306
  subbed.active.volatiles.substitute = 0;
1308
1307
  subbed.active.volatiles.Substitute = false;
1309
1308
  // 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);
1309
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1310
+ if (reason != .Confusion) options.calc.capped(sub_player);
1312
1311
  return true;
1313
1312
  } else {
1314
1313
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1315
1314
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1316
- try options.log.activate(battle.active(sub_player), .Substitute);
1315
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1317
1316
  return false;
1318
1317
  }
1319
1318
  }
1320
1319
 
1321
1320
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1322
1321
  target.stored().hp -= battle.last_damage;
1323
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1322
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1324
1323
  return false;
1325
1324
  }
1326
1325
 
@@ -1339,10 +1338,10 @@ fn mirrorMove(
1339
1338
  side.last_selected_move = foe.last_used_move;
1340
1339
 
1341
1340
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1342
- try options.log.fail(battle.active(player), .None);
1341
+ try options.log.fail(.{ battle.active(player), .None });
1343
1342
  return null;
1344
1343
  } else if (!showdown or foe.last_used_move != .Struggle) {
1345
- incrementPP(side, mslot);
1344
+ incrementPP(side, player, mslot, options);
1346
1345
  }
1347
1346
 
1348
1347
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1363,7 +1362,7 @@ fn metronome(
1363
1362
  var side = battle.side(player);
1364
1363
 
1365
1364
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1366
- incrementPP(side, mslot);
1365
+ incrementPP(side, player, mslot, options);
1367
1366
 
1368
1367
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1369
1368
  return null;
@@ -1375,20 +1374,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1375
1374
  var immune = false;
1376
1375
  var mist = false;
1377
1376
 
1378
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1377
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1378
+ if (!showdown) try options.chance.commit(player, .hit);
1379
+ return true;
1380
+ }
1379
1381
 
1380
1382
  assert(!immune);
1381
1383
  if (mist) {
1382
1384
  assert(!showdown);
1383
1385
  const foe_ident = battle.active(player.foe());
1384
- try options.log.activate(foe_ident, .Mist);
1385
- try options.log.fail(foe_ident, .None);
1386
+ try options.log.activate(.{ foe_ident, .Mist });
1387
+ try options.log.fail(.{ foe_ident, .None });
1386
1388
  } else {
1387
- try options.chance.commit(player, .miss);
1388
- try options.log.lastmiss();
1389
- try options.log.miss(battle.active(player));
1389
+ try options.log.lastmiss(.{});
1390
+ try options.log.miss(.{battle.active(player)});
1390
1391
  }
1391
1392
 
1393
+ if (!showdown) try options.chance.commit(player, .miss);
1392
1394
  return false;
1393
1395
  }
1394
1396
 
@@ -1399,7 +1401,7 @@ fn moveHit(
1399
1401
  immune: *bool,
1400
1402
  mist: *bool,
1401
1403
  options: anytype,
1402
- ) bool {
1404
+ ) !bool {
1403
1405
  var side = battle.side(player);
1404
1406
  const foe = battle.foe(player);
1405
1407
 
@@ -1438,7 +1440,7 @@ fn moveHit(
1438
1440
  const overwritten = overwrite and state > 0;
1439
1441
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1440
1442
 
1441
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1443
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1442
1444
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1443
1445
  accuracy = accuracy * boost[0] / boost[1];
1444
1446
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1450,7 +1452,7 @@ fn moveHit(
1450
1452
  if (overwrite) side.active.volatiles.state = accuracy;
1451
1453
 
1452
1454
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1453
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1455
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1454
1456
  };
1455
1457
 
1456
1458
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1461,13 +1463,17 @@ fn moveHit(
1461
1463
 
1462
1464
  if (!miss) return true;
1463
1465
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1466
+
1464
1467
  side.active.volatiles.Binding = false;
1468
+ options.chance.observe(.binding, player, .None);
1469
+
1465
1470
  return false;
1466
1471
  }
1467
1472
 
1468
1473
  fn checkFaint(
1469
1474
  battle: anytype,
1470
1475
  player: Player,
1476
+ cap: bool,
1471
1477
  options: anytype,
1472
1478
  ) @TypeOf(options.log).Error!?Result {
1473
1479
  const side = battle.side(player);
@@ -1477,33 +1483,32 @@ fn checkFaint(
1477
1483
  const foe_fainted = foe.stored().hp == 0;
1478
1484
 
1479
1485
  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;
1486
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1487
+ const more = player_out or foe_out;
1483
1488
 
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;
1489
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1490
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1486
1491
 
1487
1492
  assert(!side.active.volatiles.MultiHit);
1488
1493
  assert(!foe.active.volatiles.MultiHit);
1489
1494
 
1490
- if (tie) {
1491
- try options.log.tie();
1495
+ if (player_out and foe_out) {
1496
+ try options.log.tie(.{});
1492
1497
  return Result.Tie;
1493
1498
  } else if (player_out) {
1494
- try options.log.win(player.foe());
1499
+ try options.log.win(.{player.foe()});
1495
1500
  return if (player == .P1) Result.Lose else Result.Win;
1496
1501
  } else if (foe_out) {
1497
- try options.log.win(player);
1502
+ try options.log.win(.{player});
1498
1503
  return if (player == .P1) Result.Win else Result.Lose;
1499
1504
  }
1500
1505
 
1501
1506
  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 };
1507
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1508
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1504
1509
  }
1505
1510
 
1506
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1511
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1507
1512
  var side = battle.side(player);
1508
1513
  var foe = battle.foe(player);
1509
1514
  assert(side.stored().hp == 0);
@@ -1536,8 +1541,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1536
1541
  side.stored().stats.spe;
1537
1542
  }
1538
1543
 
1539
- try options.log.faint(battle.active(player), done);
1540
- options.calc.capped(player);
1544
+ try options.log.faint(.{ battle.active(player), done });
1545
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1546
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1547
+ if (cap) options.calc.capped(player);
1541
1548
  return null;
1542
1549
  }
1543
1550
 
@@ -1548,21 +1555,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1548
1555
  var volatiles = &side.active.volatiles;
1549
1556
 
1550
1557
  const brn = Status.is(stored.status, .BRN);
1551
- if (brn or Status.is(stored.status, .PSN)) blk: {
1558
+ if (brn or Status.is(stored.status, .PSN)) {
1552
1559
  var damage = @max(stored.stats.hp / 16, 1);
1553
-
1554
1560
  if (volatiles.Toxic) {
1555
1561
  volatiles.toxic += 1;
1556
1562
  damage *= volatiles.toxic;
1557
1563
  }
1564
+ damage = @min(damage, stored.hp);
1558
1565
 
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);
1566
+ if (!showdown or damage > 0) {
1567
+ stored.hp -= damage;
1568
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1569
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1570
+ }
1566
1571
  }
1567
1572
 
1568
1573
  if (volatiles.LeechSeed) {
@@ -1586,14 +1591,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1586
1591
  const amount = @min(damage, stored.hp);
1587
1592
  stored.hp -= amount;
1588
1593
 
1589
- // As above, Pokémon Showdown uses damageOf but its not relevant
1590
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1594
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1595
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1591
1596
 
1592
1597
  const before = foe_stored.hp;
1593
1598
  // Uncapped damage is added back to the foe
1594
1599
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1595
1600
  // 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);
1601
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1597
1602
  }
1598
1603
  }
1599
1604
 
@@ -1604,18 +1609,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1604
1609
  battle.turn += 1;
1605
1610
 
1606
1611
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1607
- try options.log.tie();
1612
+ try options.log.tie(.{});
1608
1613
  return Result.Tie;
1609
1614
  }
1610
1615
 
1611
1616
  if (showdown and battle.turn >= 1000) {
1612
- try options.log.tie();
1617
+ try options.log.tie(.{});
1613
1618
  return Result.Tie;
1614
1619
  } else if (battle.turn >= 65535) {
1615
1620
  return Result.Error;
1616
1621
  }
1617
1622
 
1618
- try options.log.turn(battle.turn);
1623
+ try options.log.turn(.{battle.turn});
1619
1624
 
1620
1625
  return Result.Default;
1621
1626
  }
@@ -1675,9 +1680,9 @@ fn checkEBC(battle: anytype) bool {
1675
1680
  }
1676
1681
 
1677
1682
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1678
- if (side.active.volatiles.disabled_move != 0) {
1683
+ if (side.active.volatiles.disable_move != 0) {
1679
1684
  // 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;
1685
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1681
1686
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1682
1687
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1683
1688
  m
@@ -1685,32 +1690,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1685
1690
  side.last_selected_move;
1686
1691
  if (m != .None and m == last) {
1687
1692
  side.active.volatiles.Charging = false;
1688
- try options.log.disabled(ident, last);
1693
+ try options.log.cant(.{ ident, .Disable, last });
1689
1694
  return true;
1690
1695
  }
1691
1696
  }
1692
1697
  return false;
1693
1698
  }
1694
1699
 
1695
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1700
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1696
1701
  const side = battle.side(who);
1697
1702
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1698
1703
  try Effects.boost(battle, who, Move.get(.Rage), options);
1699
1704
  }
1700
1705
  }
1701
1706
 
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(
1707
+ fn onBegin(
1714
1708
  battle: anytype,
1715
1709
  player: Player,
1716
1710
  move: Move.Data,
@@ -1718,7 +1712,6 @@ inline fn onBegin(
1718
1712
  residual: *bool,
1719
1713
  options: anytype,
1720
1714
  ) !void {
1721
- assert(move.effect.onBegin());
1722
1715
  return switch (move.effect) {
1723
1716
  .Confusion => Effects.confusion(battle, player, move, options),
1724
1717
  .Conversion => Effects.conversion(battle, player, options),
@@ -1740,8 +1733,7 @@ inline fn onBegin(
1740
1733
  };
1741
1734
  }
1742
1735
 
1743
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1744
- assert(move.effect.onEnd());
1736
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1745
1737
  return switch (move.effect) {
1746
1738
  .Bide => Effects.bide(battle, player, options),
1747
1739
  // zig fmt: off
@@ -1756,14 +1748,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1756
1748
  };
1757
1749
  }
1758
1750
 
1759
- inline fn alwaysHappens(
1751
+ fn alwaysHappens(
1760
1752
  battle: anytype,
1761
1753
  player: Player,
1762
1754
  move: Move.Data,
1763
1755
  residual: *bool,
1764
1756
  options: anytype,
1765
1757
  ) !void {
1766
- assert(move.effect.alwaysHappens());
1767
1758
  return switch (move.effect) {
1768
1759
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1769
1760
  .Explode => Effects.explode(battle, player),
@@ -1775,7 +1766,7 @@ inline fn alwaysHappens(
1775
1766
  };
1776
1767
  }
1777
1768
 
1778
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1769
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
1770
  return switch (move.effect) {
1780
1771
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1781
1772
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1799,10 +1790,12 @@ pub const Effects = struct {
1799
1790
 
1800
1791
  side.active.volatiles.Bide = true;
1801
1792
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1793
+
1802
1794
  side.active.volatiles.state = 0;
1803
1795
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1796
+ options.chance.observe(.attacking, player, .started);
1804
1797
 
1805
- try options.log.start(battle.active(player), .Bide);
1798
+ try options.log.start(.{ battle.active(player), .Bide });
1806
1799
  }
1807
1800
 
1808
1801
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,11 +1809,11 @@ pub const Effects = struct {
1816
1809
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1817
1810
  if (Status.is(foe_stored.status, .FRZ)) {
1818
1811
  assert(move.type == .Fire);
1819
- try options.log.curestatus(
1812
+ try options.log.curestatus(.{
1820
1813
  battle.active(player.foe()),
1821
1814
  foe_stored.status,
1822
1815
  .Message,
1823
- );
1816
+ });
1824
1817
  foe_stored.status = 0;
1825
1818
  }
1826
1819
  return;
@@ -1834,7 +1827,7 @@ pub const Effects = struct {
1834
1827
  foe_stored.status = Status.init(.BRN);
1835
1828
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1836
1829
 
1837
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1830
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1838
1831
  }
1839
1832
 
1840
1833
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1844,8 +1837,8 @@ pub const Effects = struct {
1844
1837
  volatiles.Charging = true;
1845
1838
  const move = side.last_selected_move;
1846
1839
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1847
- try options.log.laststill();
1848
- try options.log.prepare(battle.active(player), move);
1840
+ try options.log.laststill(.{});
1841
+ try options.log.prepare(.{ battle.active(player), move });
1849
1842
  }
1850
1843
 
1851
1844
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1856,42 +1849,39 @@ pub const Effects = struct {
1856
1849
  if (!try Rolls.confusionChance(battle, player, options)) return;
1857
1850
  } else {
1858
1851
  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
- }
1852
+ if (!try checkHit(battle, player, move, options)) return;
1853
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1864
1854
  } 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
- }
1855
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1856
+ if (!try checkHit(battle, player, move, options)) return;
1870
1857
  }
1871
- try options.chance.commit(player, .hit);
1872
1858
  }
1873
1859
 
1874
1860
  if (foe.active.volatiles.Confusion) return;
1875
1861
  foe.active.volatiles.Confusion = true;
1876
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1877
1862
 
1878
- try options.log.start(battle.active(player.foe()), .Confusion);
1863
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1864
+ options.chance.observe(.confusion, player.foe(), .started);
1865
+ options.calc.confusion(player.foe());
1866
+
1867
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1868
  }
1880
1869
 
1881
1870
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1882
1871
  const foe = battle.foe(player);
1883
1872
 
1884
1873
  if (foe.active.volatiles.Invulnerable) {
1885
- try options.log.lastmiss();
1886
- return options.log.miss(battle.active(player));
1874
+ try options.log.lastmiss(.{});
1875
+ return options.log.miss(.{battle.active(player)});
1887
1876
  }
1888
1877
 
1889
1878
  battle.side(player).active.types = foe.active.types;
1890
- return options.log.typechange(
1879
+ return options.log.start(.{
1891
1880
  battle.active(player),
1881
+ .TypeChange,
1892
1882
  foe.active.types,
1893
1883
  battle.active(player.foe()),
1894
- );
1884
+ });
1895
1885
  }
1896
1886
 
1897
1887
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1900,13 +1890,10 @@ pub const Effects = struct {
1900
1890
  const foe_ident = battle.active(player.foe());
1901
1891
 
1902
1892
  // 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
- }
1893
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1907
1894
 
1908
- if (volatiles.disabled_move != 0) {
1909
- try options.log.fail(foe_ident, .None);
1895
+ if (volatiles.disable_move != 0) {
1896
+ try options.log.fail(.{ foe_ident, .None });
1910
1897
  return null;
1911
1898
  }
1912
1899
 
@@ -1922,7 +1909,7 @@ pub const Effects = struct {
1922
1909
  // Technically this is still considered simply a "miss" on the cartridge,
1923
1910
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1924
1911
  if (n == 0) {
1925
- try options.log.fail(foe_ident, .None);
1912
+ try options.log.fail(.{ foe_ident, .None });
1926
1913
  return null;
1927
1914
  } else if (err) {
1928
1915
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1930,18 +1917,19 @@ pub const Effects = struct {
1930
1917
  return Result.Error;
1931
1918
  }
1932
1919
 
1933
- volatiles.disabled_move =
1920
+ volatiles.disable_move =
1934
1921
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1935
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1922
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1923
+ options.chance.observe(.disable, player.foe(), .started);
1936
1924
 
1937
- const id = foe.active.move(volatiles.disabled_move).id;
1938
- try options.log.startEffect(foe_ident, .Disable, id);
1925
+ const id = foe.active.move(volatiles.disable_move).id;
1926
+ try options.log.start(.{ foe_ident, .Disable, id });
1939
1927
  return null;
1940
1928
  }
1941
1929
 
1942
1930
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1943
- var side = battle.side(player);
1944
- var stored = side.stored();
1931
+ var stored = battle.side(player).stored();
1932
+ const ident = battle.active(player);
1945
1933
 
1946
1934
  if (battle.last_damage == 0) {
1947
1935
  assert(showdown);
@@ -1954,7 +1942,7 @@ pub const Effects = struct {
1954
1942
  if (stored.hp == stored.stats.hp) return;
1955
1943
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1956
1944
 
1957
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1945
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1958
1946
  }
1959
1947
 
1960
1948
  fn explode(battle: anytype, player: Player) !void {
@@ -1986,7 +1974,7 @@ pub const Effects = struct {
1986
1974
  if (side.active.volatiles.FocusEnergy) return;
1987
1975
  side.active.volatiles.FocusEnergy = true;
1988
1976
 
1989
- try options.log.start(battle.active(player), .FocusEnergy);
1977
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1990
1978
  }
1991
1979
 
1992
1980
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2008,7 +1996,7 @@ pub const Effects = struct {
2008
1996
  foe_stored.status = Status.init(.FRZ);
2009
1997
  // GLITCH: Hyper Beam recharging status is not cleared
2010
1998
 
2011
- try options.log.status(foe_ident, foe_stored.status, .None);
1999
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2012
2000
  }
2013
2001
 
2014
2002
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2016,7 +2004,7 @@ pub const Effects = struct {
2016
2004
  var side = battle.side(player);
2017
2005
  var foe = battle.foe(player);
2018
2006
 
2019
- var side_stored = side.stored();
2007
+ const side_stored = side.stored();
2020
2008
  var foe_stored = foe.stored();
2021
2009
 
2022
2010
  const player_ident = battle.active(player);
@@ -2028,8 +2016,8 @@ pub const Effects = struct {
2028
2016
  side.active.stats = side_stored.stats;
2029
2017
  foe.active.stats = foe_stored.stats;
2030
2018
 
2031
- try log.activate(player_ident, .Haze);
2032
- try log.clearallboost();
2019
+ try log.activate(.{ player_ident, .Haze });
2020
+ try log.clearallboost(.{});
2033
2021
 
2034
2022
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2035
2023
  if (showdown) {
@@ -2037,24 +2025,26 @@ pub const Effects = struct {
2037
2025
  const p: Player = @enumFromInt(i);
2038
2026
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2039
2027
  if (p != player and Status.any(s.stored().status)) {
2040
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2028
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2041
2029
  s.stored().status = 0;
2030
+ options.chance.observe(.sleep, p, .None);
2042
2031
  } else if (showdown and s.stored().status == Status.TOX) {
2043
2032
  s.stored().status = Status.init(.PSN);
2044
- try log.status(battle.active(p), s.stored().status, .None);
2033
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2045
2034
  }
2046
- try clearVolatiles(battle, p, p != player, options);
2035
+ try clearVolatiles(battle, p, options);
2047
2036
  }
2048
2037
  } else {
2049
2038
  if (Status.any(foe_stored.status)) {
2050
2039
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2051
2040
  foe.last_selected_move = .SKIP_TURN;
2041
+ options.chance.observe(.sleep, player.foe(), .None);
2052
2042
  }
2053
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2043
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2054
2044
  foe_stored.status = 0;
2055
2045
  }
2056
- try clearVolatiles(battle, player, false, options);
2057
- try clearVolatiles(battle, player.foe(), true, options);
2046
+ try clearVolatiles(battle, player, options);
2047
+ try clearVolatiles(battle, player.foe(), options);
2058
2048
  }
2059
2049
  }
2060
2050
 
@@ -2066,7 +2056,7 @@ pub const Effects = struct {
2066
2056
  // GLITCH: HP recovery move failure glitches
2067
2057
  const delta = stored.stats.hp - stored.hp;
2068
2058
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2069
- return try options.log.fail(ident, .None);
2059
+ return options.log.fail(.{ ident, .None });
2070
2060
  }
2071
2061
 
2072
2062
  const rest = side.last_selected_move == .Rest;
@@ -2074,17 +2064,17 @@ pub const Effects = struct {
2074
2064
  // Adding the sleep status runs the sleep condition handler to roll duration
2075
2065
  if (showdown) battle.rng.advance(1);
2076
2066
  stored.status = Status.slf(2);
2077
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2067
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2078
2068
  stored.hp = stored.stats.hp;
2079
2069
  } else {
2080
2070
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2081
2071
  }
2082
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2072
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2083
2073
  }
2084
2074
 
2085
2075
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2086
2076
  battle.side(player).active.volatiles.Recharging = true;
2087
- try options.log.mustrecharge(battle.active(player));
2077
+ try options.log.mustrecharge(.{battle.active(player)});
2088
2078
  }
2089
2079
 
2090
2080
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2093,33 +2083,31 @@ pub const Effects = struct {
2093
2083
  if (showdown) {
2094
2084
  // Invulnerability trumps type immunity on Pokémon Showdown
2095
2085
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2096
- return options.log.immune(battle.active(player.foe()), .None);
2086
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2097
2087
  }
2098
2088
  if (!try checkHit(battle, player, move, options)) return;
2099
2089
  if (foe.active.volatiles.LeechSeed) return;
2100
2090
  } else {
2101
2091
  if (!try checkHit(battle, player, move, options)) return;
2102
2092
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2103
- try options.log.lastmiss();
2104
- return options.log.miss(battle.active(player));
2093
+ try options.log.lastmiss(.{});
2094
+ return options.log.miss(.{battle.active(player)});
2105
2095
  }
2106
2096
  }
2107
2097
 
2108
- try options.chance.commit(player, .hit);
2109
2098
  foe.active.volatiles.LeechSeed = true;
2110
2099
 
2111
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2100
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2112
2101
  }
2113
2102
 
2114
2103
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2115
2104
  var side = battle.side(player);
2105
+ const ident = battle.active(player);
2116
2106
 
2117
- if (side.active.volatiles.LightScreen) {
2118
- return options.log.fail(battle.active(player), .None);
2119
- }
2107
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2120
2108
  side.active.volatiles.LightScreen = true;
2121
2109
 
2122
- try options.log.start(battle.active(player), .LightScreen);
2110
+ try options.log.start(.{ ident, .LightScreen });
2123
2111
  }
2124
2112
 
2125
2113
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2147,17 +2135,15 @@ pub const Effects = struct {
2147
2135
  if (!has_mimic) {
2148
2136
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2149
2137
  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);
2138
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2152
2139
  }
2153
2140
  }
2154
2141
  if (!try checkHit(battle, player, move, options)) return;
2155
- try options.chance.commit(player, .hit);
2156
2142
 
2157
2143
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2158
2144
  side.active.move(oslot).id = foe.active.move(rslot).id;
2159
2145
 
2160
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2146
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2161
2147
  }
2162
2148
 
2163
2149
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2166,7 +2152,7 @@ pub const Effects = struct {
2166
2152
  if (side.active.volatiles.Mist) return;
2167
2153
  side.active.volatiles.Mist = true;
2168
2154
 
2169
- try options.log.start(battle.active(player), .Mist);
2155
+ try options.log.start(.{ battle.active(player), .Mist });
2170
2156
  }
2171
2157
 
2172
2158
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2192,25 +2178,25 @@ pub const Effects = struct {
2192
2178
 
2193
2179
  if (showdown) {
2194
2180
  // 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;
2181
+ if (immune and !foe.active.volatiles.Invulnerable) {
2182
+ return log.immune(.{ foe_ident, .None });
2183
+ } else if (!try checkHit(battle, player, move, options)) return;
2197
2184
  }
2198
2185
  if (Status.any(foe_stored.status)) {
2199
- return log.fail(
2186
+ return log.fail(.{
2200
2187
  foe_ident,
2201
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2202
- );
2188
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2189
+ });
2203
2190
  }
2204
2191
  if (!showdown) {
2205
- if (immune) return log.immune(foe_ident, .None);
2192
+ if (immune) return log.immune(.{ foe_ident, .None });
2206
2193
  if (!try checkHit(battle, player, move, options)) return;
2207
2194
  }
2208
- try options.chance.commit(player, .hit);
2209
2195
 
2210
2196
  foe_stored.status = Status.init(.PAR);
2211
2197
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2212
2198
 
2213
- try log.status(foe_ident, foe_stored.status, .None);
2199
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2214
2200
  }
2215
2201
 
2216
2202
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2232,12 +2218,12 @@ pub const Effects = struct {
2232
2218
  foe_stored.status = Status.init(.PAR);
2233
2219
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2234
2220
 
2235
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2221
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2236
2222
  }
2237
2223
 
2238
2224
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2239
2225
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2240
- try options.log.fieldactivate();
2226
+ try options.log.fieldactivate(.{});
2241
2227
  }
2242
2228
  }
2243
2229
 
@@ -2252,28 +2238,27 @@ pub const Effects = struct {
2252
2238
  return;
2253
2239
  } else if (foe.active.volatiles.Substitute) {
2254
2240
  if (move.effect != .Poison) return;
2255
- return log.fail(foe_ident, .None);
2241
+ return log.fail(.{ foe_ident, .None });
2256
2242
  } else if (Status.any(foe_stored.status)) {
2257
2243
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2258
2244
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2259
2245
  // 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))
2246
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2261
2247
  if (!showdown)
2262
- .Poison
2248
+ Fail.Poison
2263
2249
  else if (toxic == (foe_stored.status == Status.TOX))
2264
- if (toxic) .Toxic else .Poison
2250
+ if (toxic) Fail.Toxic else Fail.Poison
2265
2251
  else
2266
- .None
2252
+ Fail.None
2267
2253
  else
2268
- .None);
2254
+ Fail.None });
2269
2255
  } else if (foe.active.types.includes(.Poison)) {
2270
2256
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2271
- return log.immune(foe_ident, .None);
2257
+ return log.immune(.{ foe_ident, .None });
2272
2258
  }
2273
2259
 
2274
2260
  if (move.effect == .Poison) {
2275
2261
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2276
- try options.chance.commit(player, .hit);
2277
2262
  } else {
2278
2263
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2279
2264
  return;
@@ -2287,7 +2272,7 @@ pub const Effects = struct {
2287
2272
  foe.active.volatiles.toxic = 0;
2288
2273
  }
2289
2274
 
2290
- try log.status(foe_ident, foe_stored.status, .None);
2275
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2291
2276
  }
2292
2277
 
2293
2278
  fn rage(battle: anytype, player: Player) !void {
@@ -2307,22 +2292,23 @@ pub const Effects = struct {
2307
2292
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2308
2293
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2309
2294
 
2310
- try options.log.damageOf(
2295
+ try options.log.damage(.{
2311
2296
  battle.active(player),
2312
2297
  stored,
2313
2298
  .RecoilOf,
2314
2299
  battle.active(player.foe()),
2315
- );
2300
+ });
2316
2301
  if (showdown and stored.hp == 0) residual.* = false;
2317
2302
  }
2318
2303
 
2319
2304
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2320
2305
  var side = battle.side(player);
2306
+ const ident = battle.active(player);
2321
2307
 
2322
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2308
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2323
2309
  side.active.volatiles.Reflect = true;
2324
2310
 
2325
- try options.log.start(battle.active(player), .Reflect);
2311
+ try options.log.start(.{ ident, .Reflect });
2326
2312
  }
2327
2313
 
2328
2314
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2336,14 +2322,13 @@ pub const Effects = struct {
2336
2322
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2337
2323
  } else {
2338
2324
  if (Status.any(foe_stored.status)) {
2339
- return options.log.fail(
2325
+ return options.log.fail(.{
2340
2326
  foe_ident,
2341
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2342
- );
2327
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2328
+ });
2343
2329
  }
2344
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2330
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2345
2331
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2346
- try options.chance.commit(player, .hit);
2347
2332
  }
2348
2333
 
2349
2334
  // Sleep Clause Mod
@@ -2355,19 +2340,20 @@ pub const Effects = struct {
2355
2340
  foe.active.volatiles.Recharging = false;
2356
2341
 
2357
2342
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2343
+ options.chance.observe(.sleep, player.foe(), .started);
2344
+
2358
2345
  const last = battle.side(player).last_selected_move;
2359
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2346
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2360
2347
  }
2361
2348
 
2362
2349
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2363
- try options.log.activate(battle.active(player), .Splash);
2350
+ try options.log.activate(.{ battle.active(player), .Splash });
2364
2351
  }
2365
2352
 
2366
2353
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2367
2354
  var side = battle.side(player);
2368
2355
  if (side.active.volatiles.Substitute) {
2369
- try options.log.fail(battle.active(player), .Substitute);
2370
- return;
2356
+ return options.log.fail(.{ battle.active(player), .Substitute });
2371
2357
  }
2372
2358
 
2373
2359
  assert(side.stored().stats.hp <= 1023);
@@ -2379,28 +2365,30 @@ pub const Effects = struct {
2379
2365
  // avoid having to convert to floating point)
2380
2366
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2381
2367
  if (side.stored().hp < required_hp) {
2382
- try options.log.fail(battle.active(player), .Weak);
2383
- return;
2368
+ return options.log.fail(.{ battle.active(player), .Weak });
2384
2369
  }
2385
2370
 
2386
2371
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2387
2372
  side.stored().hp -= hp;
2388
2373
  side.active.volatiles.substitute = hp + 1;
2389
2374
  side.active.volatiles.Substitute = true;
2390
- try options.log.start(battle.active(player), .Substitute);
2375
+ try options.log.start(.{ battle.active(player), .Substitute });
2391
2376
  if (hp > 0) {
2392
- try options.log.damage(battle.active(player), side.stored(), .None);
2377
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2393
2378
  if (showdown and side.stored().hp == 0) residual.* = false;
2394
2379
  }
2395
2380
  }
2396
2381
 
2397
2382
  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
2383
  // 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;
2384
+ if (showdown) {
2385
+ if (battle.side(player).last_selected_move == .Teleport) return;
2386
+ _ = try checkHit(battle, player, move, options);
2387
+ battle.last_damage = 0;
2388
+ } else {
2389
+ try options.log.fail(.{ battle.active(player), .None });
2390
+ try options.log.laststill(.{});
2391
+ }
2404
2392
  }
2405
2393
 
2406
2394
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2410,6 +2398,8 @@ pub const Effects = struct {
2410
2398
 
2411
2399
  volatiles.Thrashing = true;
2412
2400
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2401
+
2402
+ options.chance.observe(.attacking, player, .started);
2413
2403
  }
2414
2404
 
2415
2405
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2438,7 +2428,7 @@ pub const Effects = struct {
2438
2428
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2439
2429
  }
2440
2430
 
2441
- try options.log.transform(battle.active(player), foe_ident);
2431
+ try options.log.transform(.{ battle.active(player), foe_ident });
2442
2432
  }
2443
2433
 
2444
2434
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2455,6 +2445,7 @@ pub const Effects = struct {
2455
2445
 
2456
2446
  side.active.volatiles.attacks =
2457
2447
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2448
+ options.chance.observe(.binding, player, .started);
2458
2449
  }
2459
2450
 
2460
2451
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2468,87 +2459,87 @@ pub const Effects = struct {
2468
2459
  switch (move.effect) {
2469
2460
  .AttackUp1, .AttackUp2, .Rage => {
2470
2461
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2471
- if (boosts.atk == 6) return try log.fail(ident, .None);
2462
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2472
2463
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2473
2464
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2474
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2465
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2475
2466
  if (stats.atk == MAX_STAT_VALUE) {
2476
2467
  boosts.atk -= 1;
2477
2468
  if (showdown) {
2478
- try log.boost(ident, reason, n);
2479
- try log.boost(ident, Boost.Attack, -1);
2469
+ try log.boost(.{ ident, reason, n });
2470
+ try log.boost(.{ ident, .Attack, -1 });
2480
2471
  if (move.effect == .Rage) return;
2481
2472
  }
2482
- return try log.fail(ident, .None);
2473
+ return log.fail(.{ ident, .None });
2483
2474
  }
2484
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2475
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2485
2476
  const stat = unmodifiedStats(battle, side).atk;
2486
2477
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2487
- try log.boost(ident, reason, n);
2478
+ try log.boost(.{ ident, reason, n });
2488
2479
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2489
2480
  if (showdown and move.effect == .Rage) return;
2490
2481
  },
2491
2482
  .DefenseUp1, .DefenseUp2 => {
2492
2483
  assert(boosts.def >= -6 and boosts.def <= 6);
2493
- if (boosts.def == 6) return try log.fail(ident, .None);
2484
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2494
2485
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2495
2486
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2496
2487
  if (stats.def == MAX_STAT_VALUE) {
2497
2488
  boosts.def -= 1;
2498
2489
  if (showdown) {
2499
- try log.boost(ident, .Defense, n);
2500
- try log.boost(ident, .Defense, -1);
2490
+ try log.boost(.{ ident, .Defense, n });
2491
+ try log.boost(.{ ident, .Defense, -1 });
2501
2492
  }
2502
- return try log.fail(ident, .None);
2493
+ return log.fail(.{ ident, .None });
2503
2494
  }
2504
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2495
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2505
2496
  const stat = unmodifiedStats(battle, side).def;
2506
2497
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2507
- try log.boost(ident, .Defense, n);
2498
+ try log.boost(.{ ident, .Defense, n });
2508
2499
  },
2509
2500
  .SpeedUp2 => {
2510
2501
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2511
- if (boosts.spe == 6) return try log.fail(ident, .None);
2502
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2512
2503
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2513
2504
  if (stats.spe == MAX_STAT_VALUE) {
2514
2505
  boosts.spe -= 1;
2515
2506
  if (showdown) {
2516
- try log.boost(ident, .Speed, 2);
2517
- try log.boost(ident, .Speed, -1);
2507
+ try log.boost(.{ ident, .Speed, 2 });
2508
+ try log.boost(.{ ident, .Speed, -1 });
2518
2509
  }
2519
- return try log.fail(ident, .None);
2510
+ return log.fail(.{ ident, .None });
2520
2511
  }
2521
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2512
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2522
2513
  const stat = unmodifiedStats(battle, side).spe;
2523
2514
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2524
- try log.boost(ident, .Speed, 2);
2515
+ try log.boost(.{ ident, .Speed, 2 });
2525
2516
  },
2526
2517
  .SpecialUp1, .SpecialUp2 => {
2527
2518
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2528
- if (boosts.spc == 6) return try log.fail(ident, .None);
2519
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2529
2520
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2530
2521
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2531
2522
  if (stats.spc == MAX_STAT_VALUE) {
2532
2523
  boosts.spc -= 1;
2533
2524
  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);
2525
+ try log.boost(.{ ident, .SpecialAttack, n });
2526
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2527
+ try log.boost(.{ ident, .SpecialDefense, n });
2528
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2538
2529
  }
2539
- return try log.fail(ident, .None);
2530
+ return log.fail(.{ ident, .None });
2540
2531
  }
2541
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2532
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2542
2533
  const stat = unmodifiedStats(battle, side).spc;
2543
2534
  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);
2535
+ try log.boost(.{ ident, .SpecialAttack, n });
2536
+ try log.boost(.{ ident, .SpecialDefense, n });
2546
2537
  },
2547
2538
  .EvasionUp1 => {
2548
2539
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2549
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2540
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2550
2541
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2551
- try log.boost(ident, .Evasion, 1);
2542
+ try log.boost(.{ ident, .Evasion, 1 });
2552
2543
  },
2553
2544
  else => unreachable,
2554
2545
  }
@@ -2563,7 +2554,7 @@ pub const Effects = struct {
2563
2554
  const foe_ident = battle.active(player.foe());
2564
2555
  const secondary = move.effect.isStatDownChance();
2565
2556
 
2566
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2557
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2567
2558
 
2568
2559
  if (secondary) {
2569
2560
  const proc = try Rolls.unboost(battle, player, options);
@@ -2571,7 +2562,6 @@ pub const Effects = struct {
2571
2562
  } else {
2572
2563
  // checkHit already checks for Invulnerable
2573
2564
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2574
- try options.chance.commit(player, .hit);
2575
2565
  }
2576
2566
 
2577
2567
  var stats = &foe.active.stats;
@@ -2581,82 +2571,82 @@ pub const Effects = struct {
2581
2571
  switch (move.effect) {
2582
2572
  .AttackDown1, .AttackDownChance => {
2583
2573
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2584
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2574
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2585
2575
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2586
2576
  if (stats.atk == 1) {
2587
2577
  boosts.atk += 1;
2588
2578
  if (showdown) {
2589
- try log.boost(foe_ident, .Attack, -1);
2590
- try log.boost(foe_ident, .Attack, 1);
2579
+ try log.boost(.{ foe_ident, .Attack, -1 });
2580
+ try log.boost(.{ foe_ident, .Attack, 1 });
2591
2581
  }
2592
- return try log.fail(foe_ident, .None);
2582
+ return log.fail(.{ foe_ident, .None });
2593
2583
  }
2594
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2584
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2595
2585
  const stat = unmodifiedStats(battle, foe).atk;
2596
2586
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2597
- try log.boost(foe_ident, .Attack, -1);
2587
+ try log.boost(.{ foe_ident, .Attack, -1 });
2598
2588
  },
2599
2589
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2600
2590
  assert(boosts.def >= -6 and boosts.def <= 6);
2601
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2591
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2602
2592
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2603
2593
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2604
2594
  if (stats.def == 1) {
2605
2595
  boosts.def += 1;
2606
2596
  if (showdown) {
2607
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2608
- try log.boost(foe_ident, .Defense, 1);
2597
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2598
+ try log.boost(.{ foe_ident, .Defense, 1 });
2609
2599
  }
2610
- return try log.fail(foe_ident, .None);
2600
+ return log.fail(.{ foe_ident, .None });
2611
2601
  }
2612
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2602
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2613
2603
  const stat = unmodifiedStats(battle, foe).def;
2614
2604
  stats.def = @max(1, stat * mod[0] / mod[1]);
2615
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2605
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2616
2606
  },
2617
2607
  .SpeedDown1, .SpeedDownChance => {
2618
2608
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2619
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2609
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2620
2610
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2621
2611
  if (stats.spe == 1) {
2622
2612
  boosts.spe += 1;
2623
2613
  if (showdown) {
2624
- try log.boost(foe_ident, .Speed, -1);
2625
- try log.boost(foe_ident, .Speed, 1);
2614
+ try log.boost(.{ foe_ident, .Speed, -1 });
2615
+ try log.boost(.{ foe_ident, .Speed, 1 });
2626
2616
  }
2627
- return try log.fail(foe_ident, .None);
2617
+ return log.fail(.{ foe_ident, .None });
2628
2618
  }
2629
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2619
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2630
2620
  const stat = unmodifiedStats(battle, foe).spe;
2631
2621
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2632
- try log.boost(foe_ident, .Speed, -1);
2622
+ try log.boost(.{ foe_ident, .Speed, -1 });
2633
2623
  assert(boosts.spe >= -6);
2634
2624
  },
2635
2625
  .SpecialDownChance => {
2636
2626
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2637
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2627
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2638
2628
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2639
2629
  if (stats.spc == 1) {
2640
2630
  boosts.spc += 1;
2641
2631
  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);
2632
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2633
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2634
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2635
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2646
2636
  }
2647
- return try log.fail(foe_ident, .None);
2637
+ return log.fail(.{ foe_ident, .None });
2648
2638
  }
2649
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2639
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2650
2640
  const stat = unmodifiedStats(battle, foe).spc;
2651
2641
  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);
2642
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2643
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2654
2644
  },
2655
2645
  .AccuracyDown1 => {
2656
2646
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2657
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2647
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2658
2648
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2659
- try log.boost(foe_ident, .Accuracy, -1);
2649
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2660
2650
  },
2661
2651
  else => unreachable,
2662
2652
  }
@@ -2680,40 +2670,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2680
2670
  }
2681
2671
  }
2682
2672
 
2683
- inline fn isForced(active: anytype) bool {
2673
+ fn isForced(active: anytype) bool {
2684
2674
  return active.volatiles.Recharging or active.volatiles.Rage or
2685
2675
  active.volatiles.Thrashing or active.volatiles.Charging;
2686
2676
  }
2687
2677
 
2688
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2678
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2689
2679
  var log = options.log;
2690
2680
  var side = battle.side(who);
2691
2681
  var volatiles = &side.active.volatiles;
2692
2682
  const ident = battle.active(who);
2693
2683
 
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);
2684
+ if (volatiles.disable_move != 0) {
2685
+ volatiles.disable_move = 0;
2686
+ volatiles.disable_duration = 0;
2687
+ options.chance.observe(.disable, who, .None);
2688
+ try log.end(.{ ident, .DisableSilent });
2700
2689
  }
2701
2690
  if (volatiles.Confusion) {
2702
2691
  // volatiles.confusion is left unchanged
2703
2692
  volatiles.Confusion = false;
2704
- try log.end(ident, .ConfusionSilent);
2693
+ options.chance.observe(.confusion, who, .None);
2694
+ try log.end(.{ ident, .ConfusionSilent });
2705
2695
  }
2706
2696
  if (volatiles.Mist) {
2707
2697
  volatiles.Mist = false;
2708
- try log.end(ident, .Mist);
2698
+ try log.end(.{ ident, .MistSilent });
2709
2699
  }
2710
2700
  if (volatiles.FocusEnergy) {
2711
2701
  volatiles.FocusEnergy = false;
2712
- try log.end(ident, .FocusEnergy);
2702
+ try log.end(.{ ident, .FocusEnergySilent });
2713
2703
  }
2714
2704
  if (volatiles.LeechSeed) {
2715
2705
  volatiles.LeechSeed = false;
2716
- try log.end(ident, .LeechSeed);
2706
+ try log.end(.{ ident, .LeechSeedSilent });
2717
2707
  }
2718
2708
  if (!showdown and volatiles.Toxic) {
2719
2709
  volatiles.Toxic = false;
@@ -2721,24 +2711,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2721
2711
  }
2722
2712
  if (volatiles.LightScreen) {
2723
2713
  volatiles.LightScreen = false;
2724
- try log.end(ident, .LightScreen);
2714
+ try log.end(.{ ident, .LightScreenSilent });
2725
2715
  }
2726
2716
  if (volatiles.Reflect) {
2727
2717
  volatiles.Reflect = false;
2728
- try log.end(ident, .Reflect);
2718
+ try log.end(.{ ident, .ReflectSilent });
2729
2719
  }
2730
2720
  if (showdown and volatiles.Toxic) {
2731
2721
  volatiles.Toxic = false;
2732
2722
  // Pokémon Showdown erroneously clears the toxic counter
2733
2723
  volatiles.toxic = 0;
2734
- try log.end(ident, .Toxic);
2724
+ try log.end(.{ ident, .ToxicSilent });
2735
2725
  }
2736
2726
  }
2737
2727
 
2728
+ fn decrement(
2729
+ comptime field: chance.Action.Field,
2730
+ player: Player,
2731
+ options: anytype,
2732
+ n: anytype,
2733
+ ) @TypeOf(n) {
2734
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2735
+ .started, .ended => 0,
2736
+ .continuing => if (n > 1) n - 1 else n,
2737
+ else => unreachable,
2738
+ } else n - 1;
2739
+ }
2740
+
2738
2741
  pub const Rolls = struct {
2739
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2742
+ fn speedTie(battle: anytype, options: anytype) !bool {
2740
2743
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2741
2744
  player == .P1
2745
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2746
+ battle.rng.speedTie()
2742
2747
  else if (showdown)
2743
2748
  battle.rng.range(u8, 0, 2) == 0
2744
2749
  else
@@ -2748,21 +2753,31 @@ pub const Rolls = struct {
2748
2753
  return p1;
2749
2754
  }
2750
2755
 
2751
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2756
+ fn criticalHit(
2757
+ battle: anytype,
2758
+ player: Player,
2759
+ rate: u8,
2760
+ spurious: bool,
2761
+ options: anytype,
2762
+ ) !bool {
2752
2763
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2753
2764
  val == .true
2765
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2766
+ battle.rng.criticalHit(player, rate)
2754
2767
  else if (showdown)
2755
2768
  battle.rng.chance(u8, rate, 256)
2756
2769
  else
2757
2770
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2758
2771
 
2759
- try options.chance.criticalHit(player, crit, rate);
2772
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2760
2773
  return crit;
2761
2774
  }
2762
2775
 
2763
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2776
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2764
2777
  const roll = if (options.calc.overridden(player, .damage)) |val|
2765
2778
  val
2779
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2780
+ battle.rng.damage(player)
2766
2781
  else roll: {
2767
2782
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2768
2783
  while (true) {
@@ -2776,21 +2791,27 @@ pub const Rolls = struct {
2776
2791
  return roll;
2777
2792
  }
2778
2793
 
2779
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2794
+ const uN = if (pkmn.options.miss) u8 else u9;
2795
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2796
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2780
2797
  const ok = if (options.calc.overridden(player, .hit)) |val|
2781
2798
  val == .true
2799
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2800
+ battle.rng.hit(player, acc)
2782
2801
  else if (showdown)
2783
- battle.rng.chance(u8, accuracy, 256)
2802
+ battle.rng.chance(uN, acc, 256)
2784
2803
  else
2785
- battle.rng.next() < accuracy;
2804
+ battle.rng.next() < acc;
2786
2805
 
2787
- options.chance.hit(ok, accuracy);
2806
+ try options.chance.hit(player, ok, acc);
2788
2807
  return ok;
2789
2808
  }
2790
2809
 
2791
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2810
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2792
2811
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2793
2812
  val == .true
2813
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2814
+ battle.rng.confused(player)
2794
2815
  else if (showdown)
2795
2816
  !battle.rng.chance(u8, 128, 256)
2796
2817
  else
@@ -2800,9 +2821,11 @@ pub const Rolls = struct {
2800
2821
  return cfz;
2801
2822
  }
2802
2823
 
2803
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2824
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2804
2825
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2805
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2828
+ battle.rng.paralyzed(player)
2806
2829
  else if (showdown)
2807
2830
  battle.rng.chance(u8, 63, 256)
2808
2831
  else
@@ -2812,9 +2835,11 @@ pub const Rolls = struct {
2812
2835
  return par;
2813
2836
  }
2814
2837
 
2815
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2838
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2816
2839
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2840
  val == .true
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2842
+ battle.rng.secondaryChance(player, 25)
2818
2843
  else if (showdown)
2819
2844
  battle.rng.chance(u8, 25, 256)
2820
2845
  else
@@ -2824,11 +2849,13 @@ pub const Rolls = struct {
2824
2849
  return proc;
2825
2850
  }
2826
2851
 
2827
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2852
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2828
2853
  const rate: u8 = if (low) 26 else 77;
2829
2854
 
2830
2855
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2831
2856
  val == .true
2857
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2858
+ battle.rng.secondaryChance(player, rate)
2832
2859
  else if (showdown)
2833
2860
  battle.rng.chance(u8, rate, 256)
2834
2861
  else
@@ -2838,11 +2865,13 @@ pub const Rolls = struct {
2838
2865
  return proc;
2839
2866
  }
2840
2867
 
2841
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2868
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2842
2869
  const rate: u8 = if (low) 52 else 103;
2843
2870
 
2844
2871
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2845
2872
  val == .true
2873
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2874
+ battle.rng.secondaryChance(player, rate)
2846
2875
  else if (showdown)
2847
2876
  battle.rng.chance(u8, rate, 256)
2848
2877
  else
@@ -2852,9 +2881,11 @@ pub const Rolls = struct {
2852
2881
  return proc;
2853
2882
  }
2854
2883
 
2855
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2884
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2856
2885
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2857
2886
  val == .true
2887
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2888
+ battle.rng.secondaryChance(player, 85)
2858
2889
  else if (showdown)
2859
2890
  battle.rng.chance(u8, 85, 256)
2860
2891
  else
@@ -2864,14 +2895,14 @@ pub const Rolls = struct {
2864
2895
  return proc;
2865
2896
  }
2866
2897
 
2867
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2898
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2868
2899
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2869
2900
  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: {
2901
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2902
+ battle.rng.metronome(player)
2903
+ else if (showdown)
2904
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2905
+ else move: {
2875
2906
  while (true) {
2876
2907
  const r = battle.rng.next();
2877
2908
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2884,9 +2915,11 @@ pub const Rolls = struct {
2884
2915
  return move;
2885
2916
  }
2886
2917
 
2887
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2918
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2888
2919
  const power = if (options.calc.overridden(player, .psywave)) |val|
2889
2920
  val - 1
2921
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2922
+ battle.rng.psywave(player)
2890
2923
  else if (showdown)
2891
2924
  battle.rng.range(u8, 0, max)
2892
2925
  else power: {
@@ -2901,11 +2934,14 @@ pub const Rolls = struct {
2901
2934
  return power;
2902
2935
  }
2903
2936
 
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)
2937
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2938
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2939
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2940
+ else
2941
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2942
+ battle.rng.sleepDuration(player)
2907
2943
  else if (showdown)
2908
- @intCast(battle.rng.range(u8, 1, 8))
2944
+ battle.rng.range(u3, 1, 8)
2909
2945
  else duration: {
2910
2946
  while (true) {
2911
2947
  const r = battle.rng.next() & 7;
@@ -2914,60 +2950,74 @@ pub const Rolls = struct {
2914
2950
  };
2915
2951
 
2916
2952
  assert(duration >= 1 and duration <= 7);
2917
- options.chance.duration(.sleep, player, player.foe(), duration);
2953
+ options.chance.duration(player, duration);
2918
2954
  return duration;
2919
2955
  }
2920
2956
 
2921
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2922
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2923
- val
2957
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2958
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2959
+ if (val >= 1 and val <= 8) val else null
2960
+ else
2961
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2962
+ battle.rng.disableDuration(player)
2924
2963
  else if (showdown)
2925
- @intCast(battle.rng.range(u8, 1, 9))
2964
+ battle.rng.range(u4, 1, 9)
2926
2965
  else
2927
2966
  @intCast((battle.rng.next() & 7) + 1);
2928
2967
 
2929
2968
  assert(duration >= 1 and duration <= 8);
2930
- options.chance.duration(.disable, player, player.foe(), duration);
2969
+ options.chance.duration(player, duration);
2931
2970
  return duration;
2932
2971
  }
2933
2972
 
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)
2973
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2974
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2975
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
2976
+ else
2977
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2978
+ battle.rng.confusionDuration(player)
2937
2979
  else if (showdown)
2938
- @intCast(battle.rng.range(u8, 2, 6))
2980
+ battle.rng.range(u3, 2, 6)
2939
2981
  else
2940
2982
  @intCast((battle.rng.next() & 3) + 2);
2941
2983
 
2942
2984
  assert(duration >= 2 and duration <= 5);
2943
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2985
+ options.chance.duration(player, duration);
2944
2986
  return duration;
2945
2987
  }
2946
2988
 
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)
2989
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2990
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2991
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
2992
+ else
2993
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2994
+ battle.rng.attackingDuration(player)
2950
2995
  else if (showdown)
2951
- @intCast(battle.rng.range(u4, 2, 4))
2996
+ battle.rng.range(u3, 2, 4)
2952
2997
  else
2953
2998
  @intCast((battle.rng.next() & 1) + 2);
2954
2999
 
2955
3000
  assert(duration >= 2 and duration <= 3);
2956
- options.chance.duration(.attacking, player, player, duration);
3001
+ options.chance.duration(player, duration);
2957
3002
  return duration;
2958
3003
  }
2959
3004
 
2960
- inline fn distribution(
3005
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3006
+
3007
+ fn distribution(
2961
3008
  battle: anytype,
2962
3009
  comptime effect: Move.Effect,
2963
3010
  player: Player,
2964
3011
  options: anytype,
2965
3012
  ) !u3 {
2966
3013
  const roll = if (effect == .Binding) .duration else .multi_hit;
2967
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2968
- @intCast(val)
3014
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3015
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3018
+ battle.rng.distribution(player)
2969
3019
  else if (showdown)
2970
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3020
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2971
3021
  else n: {
2972
3022
  const r = (battle.rng.next() & 3);
2973
3023
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2975,7 +3025,7 @@ pub const Rolls = struct {
2975
3025
 
2976
3026
  assert(n >= 2 and n <= 5);
2977
3027
  if (effect == .Binding) {
2978
- options.chance.duration(.binding, player, player, n);
3028
+ options.chance.duration(player, n);
2979
3029
  } else {
2980
3030
  try options.chance.multiHit(player, n);
2981
3031
  }
@@ -2990,15 +3040,15 @@ pub const Rolls = struct {
2990
3040
  options: anytype,
2991
3041
  ) !u4 {
2992
3042
  // TODO: consider throwing error instead of rerolling?
2993
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3043
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2994
3044
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2995
3045
  val
2996
3046
  else
2997
3047
  null
2998
3048
  else
2999
- null;
3000
-
3001
- const slot: u4 = overridden orelse slot: {
3049
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3050
+ battle.rng.moveSlot(player, moves, check_pp)
3051
+ else slot: {
3002
3052
  if (showdown) {
3003
3053
  if (check_pp == 0) {
3004
3054
  var i: usize = moves.len;
@@ -3014,10 +3064,9 @@ pub const Rolls = struct {
3014
3064
  while (i < moves.len and r > 0) : (i += 1) {
3015
3065
  if (moves[i].pp > 0) {
3016
3066
  r -= 1;
3017
- if (r == 0) break;
3067
+ if (r == 0) break :slot @intCast(i + 1);
3018
3068
  }
3019
3069
  }
3020
- break :slot @intCast(i + 1);
3021
3070
  }
3022
3071
  }
3023
3072
 
@@ -3039,11 +3088,11 @@ test "RNG agreement" {
3039
3088
  expected[i] = @intCast(i * 0x1000000);
3040
3089
  }
3041
3090
 
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 };
3091
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3092
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3093
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3094
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3095
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3047
3096
 
3048
3097
  for (0..expected.len) |i| {
3049
3098
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3079,11 +3128,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3079
3128
  const side = battle.side(player);
3080
3129
  const foe = battle.foe(player);
3081
3130
 
3082
- var active = &side.active;
3131
+ const active = &side.active;
3083
3132
  const stored = side.stored();
3084
3133
 
3085
3134
  // 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.
3135
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3087
3136
  //
3088
3137
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3089
3138
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3091,7 +3140,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3091
3140
  // passing due to being forced into using a move. Instead of introducing another option
3092
3141
  // we simply repurpose Move with no move slot, even though pedantically this is not
3093
3142
  // strictly correct as the player would not have been presented the option to move or
3094
- // switch at all.
3143
+ // switch at all
3095
3144
  if (isForced(active)) {
3096
3145
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3097
3146
  n += 1;
@@ -3110,7 +3159,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3110
3159
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3111
3160
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3112
3161
  // either disables all other moves in the case of limited or requires you to select a
3113
- // move normally if sleeping/frozen/bound.
3162
+ // move normally if sleeping/frozen/bound
3114
3163
  if (!showdown and (limited or foe.active.volatiles.Binding or
3115
3164
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3116
3165
  {
@@ -3123,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3123
3172
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3124
3173
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3125
3174
  // 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.
3175
+ // proc-ed move in their moveset) and disabling all other moves
3127
3176
  if (limited) {
3128
3177
  assert(showdown);
3129
3178
  assert(side.last_selected_move != .None);
@@ -3133,7 +3182,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3133
3182
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3134
3183
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3135
3184
  const struggle =
3136
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3185
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3137
3186
  const s = if (struggle) 0 else slot;
3138
3187
  out[n] = .{ .type = .Move, .data = s };
3139
3188
  n += 1;
@@ -3148,7 +3197,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3148
3197
  const m = active.moves[slot - 1];
3149
3198
  if (m.id == .None) break;
3150
3199
  if (m.pp == 0) continue;
3151
- if (active.volatiles.disabled_move == slot) continue;
3200
+ if (active.volatiles.disable_move == slot) continue;
3152
3201
  out[n] = .{ .type = .Move, .data = slot };
3153
3202
  n += 1;
3154
3203
  }