@pkmn/engine 0.1.0-dev.305a932f → 0.1.0-dev.30895c5a

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 (51) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data/layout.json +2 -2
  6. package/build/pkg/data.d.ts +4 -4
  7. package/build/pkg/data.js.map +1 -1
  8. package/build/pkg/gen1.js +2 -2
  9. package/build/pkg/gen1.js.map +1 -1
  10. package/build/pkg/index.d.ts +3 -2
  11. package/build/pkg/index.js.map +1 -1
  12. package/build/pkg/protocol.js +5 -5
  13. package/build/pkg/protocol.js.map +1 -1
  14. package/build/test/display.js +8 -3
  15. package/build/test/display.js.map +1 -1
  16. package/build/tools/debug.js +5 -4
  17. package/build/tools/debug.js.map +1 -1
  18. package/build.zig +100 -102
  19. package/package.json +34 -23
  20. package/src/bin/install-pkmn-engine +26 -18
  21. package/src/lib/common/data.zig +8 -8
  22. package/src/lib/common/debug.zig +2 -2
  23. package/src/lib/common/optional.zig +9 -2
  24. package/src/lib/common/options.zig +2 -0
  25. package/src/lib/common/protocol.zig +408 -384
  26. package/src/lib/common/rational.zig +119 -18
  27. package/src/lib/common/rng.zig +20 -11
  28. package/src/lib/gen1/calc.zig +506 -130
  29. package/src/lib/gen1/chance.zig +395 -299
  30. package/src/lib/gen1/data/moves.zig +24 -10
  31. package/src/lib/gen1/data/species.zig +3 -3
  32. package/src/lib/gen1/data/types.zig +5 -5
  33. package/src/lib/gen1/data.zig +39 -39
  34. package/src/lib/gen1/helpers.zig +89 -548
  35. package/src/lib/gen1/mechanics.zig +551 -492
  36. package/src/lib/gen2/calc.zig +135 -0
  37. package/src/lib/gen2/chance.zig +882 -0
  38. package/src/lib/gen2/data/items.zig +3 -3
  39. package/src/lib/gen2/data/moves.zig +32 -15
  40. package/src/lib/gen2/data/species.zig +1 -1
  41. package/src/lib/gen2/data/types.zig +75 -28
  42. package/src/lib/gen2/data.zig +37 -25
  43. package/src/lib/gen2/generated.zig +806 -0
  44. package/src/lib/gen2/helpers.zig +37 -7
  45. package/src/lib/gen2/mechanics.zig +2679 -358
  46. package/src/lib/{binding/node.zig → node.zig} +9 -12
  47. package/src/lib/pkmn.zig +27 -15
  48. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  49. package/src/test/display/pkmn.html.tmpl +5 -5
  50. package/src/lib/gen1/README.md +0 -894
  51. package/src/lib/gen2/README.md +0 -330
@@ -4,26 +4,27 @@ const pkmn = @import("../pkmn.zig");
4
4
 
5
5
  const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
+ const optional = @import("../common/optional.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
10
 
10
- const Action = @import("chance.zig").Action;
11
-
12
11
  const data = @import("data.zig");
12
+ const chance = @import("chance.zig");
13
13
 
14
14
  const assert = std.debug.assert;
15
15
 
16
16
  const expectEqual = std.testing.expectEqual;
17
17
 
18
+ const showdown = pkmn.options.showdown;
19
+
18
20
  const Choice = common.Choice;
19
21
  const ID = common.ID;
20
22
  const Player = common.Player;
21
23
  const Result = common.Result;
22
24
 
23
- const showdown = pkmn.options.showdown;
24
-
25
25
  const Boost = protocol.Boost;
26
26
  const Damage = protocol.Damage;
27
+ const Fail = protocol.Fail;
27
28
  const Heal = protocol.Heal;
28
29
 
29
30
  const Gen12 = rng.Gen12;
@@ -59,30 +60,26 @@ const BOOSTS = &[_][2]u8{
59
60
 
60
61
  const MAX_STAT_VALUE = 999;
61
62
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
63
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
64
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
66
65
  if (battle.turn == 0) return start(battle, options);
67
66
 
68
67
  var s1 = false;
69
68
  var s2 = false;
70
- var f1: ?Move = null;
71
- var f2: ?Move = null;
72
69
 
73
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
74
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
70
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
71
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
75
72
 
76
73
  var p1 = battle.side(.P1);
77
74
  var p2 = battle.side(.P2);
78
75
 
79
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
80
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
81
78
 
82
79
  if (try turnOrder(battle, c1, c2, options) == .P1) {
83
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
80
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
84
81
  } else {
85
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
82
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
86
83
  }
87
84
 
88
85
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -95,11 +92,11 @@ fn start(battle: anytype, options: anytype) !Result {
95
92
  const p1 = battle.side(.P1);
96
93
  const p2 = battle.side(.P2);
97
94
 
98
- var p1_slot = findFirstAlive(p1);
95
+ const p1_slot = findFirstAlive(p1);
99
96
  assert(!showdown or p1_slot == 1);
100
97
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
101
98
 
102
- var p2_slot = findFirstAlive(p2);
99
+ const p2_slot = findFirstAlive(p2);
103
100
  assert(!showdown or p2_slot == 1);
104
101
  if (p2_slot == 0) return Result.Win;
105
102
 
@@ -120,7 +117,6 @@ fn selectMove(
120
117
  choice: Choice,
121
118
  foe_choice: Choice,
122
119
  skip_turn: *bool,
123
- from: *?Move,
124
120
  ) ?Result {
125
121
  if (choice.type == .Pass) return null;
126
122
 
@@ -137,7 +133,6 @@ fn selectMove(
137
133
  return null;
138
134
  }
139
135
  if (volatiles.Rage) {
140
- from.* = side.last_used_move;
141
136
  if (showdown) {
142
137
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
143
138
  saveMove(battle, player, null);
@@ -147,7 +142,6 @@ fn selectMove(
147
142
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
148
143
  if (!showdown) volatiles.Flinch = false;
149
144
  if (volatiles.Thrashing or volatiles.Charging) {
150
- from.* = side.last_selected_move;
151
145
  if (showdown) {
152
146
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
153
147
  saveMove(battle, player, null);
@@ -168,15 +162,13 @@ fn selectMove(
168
162
  return null;
169
163
  }
170
164
  if (volatiles.Binding) {
171
- from.* = side.last_used_move;
172
165
  if (showdown) {
173
- if (foe_choice.type == .Switch) from.* = null;
174
166
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
175
167
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
176
168
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
177
169
  // set the selected move will not actually be used, it will just be reported as having
178
170
  // been used (this differs from how Pokémon Showdown works, but its impossible to
179
- // replicate the incorrect behavior with the correct mechanisms).
171
+ // replicate the incorrect behavior with the correct mechanisms)
180
172
  saveMove(battle, player, choice);
181
173
  } else {
182
174
  assert(choice.data == 0);
@@ -206,7 +198,7 @@ fn selectMove(
206
198
  if (choice.data == 0) {
207
199
  const struggle = ok: {
208
200
  for (side.active.moves, 0..) |move, i| {
209
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
201
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
210
202
  }
211
203
  break :ok true;
212
204
  };
@@ -225,7 +217,7 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
225
217
  if (c.data == 0) {
226
218
  side.last_selected_move = .Struggle;
227
219
  } else {
228
- assert(showdown or side.active.volatiles.disabled_move != c.data);
220
+ assert(showdown or side.active.volatiles.disable_move != c.data);
229
221
  const move = side.active.move(c.data);
230
222
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
231
223
  // sometimes required...), but a 0 PP move can be used automatically
@@ -263,17 +255,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
263
255
  active.moves = incoming.moves;
264
256
 
265
257
  statusModify(incoming.status, &active.stats);
258
+ options.chance.switched(player, slot);
266
259
 
267
260
  foe.active.volatiles.Binding = false;
268
261
 
269
- try options.log.switched(battle.active(player), incoming);
270
- options.chance.switched(player, side.order[0], out);
262
+ try options.log.switched(.{ battle.active(player), incoming });
271
263
 
272
264
  if (showdown and incoming.status == Status.TOX) {
273
265
  incoming.status = Status.init(.PSN);
274
266
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
275
267
  // rather not clutter up turnOrder just for this (incorrect) log message
276
- try options.log.status(battle.active(player), incoming.status, .Silent);
268
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
277
269
  }
278
270
  }
279
271
 
@@ -336,12 +328,10 @@ fn doTurn(
336
328
  player_choice: Choice,
337
329
  player_rewrap: bool,
338
330
  player_skip: bool,
339
- player_from: ?Move,
340
331
  foe_player: Player,
341
332
  foe_choice: Choice,
342
333
  foe_rewrap: bool,
343
334
  foe_skip: bool,
344
- foe_from: ?Move,
345
335
  options: anytype,
346
336
  ) !?Result {
347
337
  assert(player_choice.type != .Pass);
@@ -354,19 +344,18 @@ fn doTurn(
354
344
  player_choice,
355
345
  player_rewrap,
356
346
  player_skip,
357
- player_from,
358
347
  &residual,
359
348
  options,
360
349
  )) |r| return r;
361
350
  if (!replace) {
362
351
  if (player_choice.type != .Switch) {
363
- if (try checkFaint(battle, foe_player, options)) |r| return r;
352
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
364
353
  }
365
354
  if (residual) try handleResidual(battle, player, options);
366
- if (try checkFaint(battle, player, options)) |r| return r;
355
+ if (try checkFaint(battle, player, false, options)) |r| return r;
367
356
  } else if (foe_choice.type == .Pass) return null;
368
357
 
369
- options.chance.clearPending();
358
+ if (!showdown) options.chance.clearPending();
370
359
 
371
360
  residual = true;
372
361
  replace = battle.side(foe_player).stored().hp == 0;
@@ -377,16 +366,15 @@ fn doTurn(
377
366
  foe_choice,
378
367
  foe_rewrap,
379
368
  foe_skip,
380
- foe_from,
381
369
  &residual,
382
370
  options,
383
371
  )) |r| return r;
384
372
  if (!replace) {
385
373
  if (!calc and foe_choice.type != .Switch) {
386
- if (try checkFaint(battle, player, options)) |r| return r;
374
+ if (try checkFaint(battle, player, true, options)) |r| return r;
387
375
  }
388
376
  if (residual) try handleResidual(battle, foe_player, options);
389
- if (try checkFaint(battle, foe_player, options)) |r| return r;
377
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
390
378
  }
391
379
 
392
380
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -405,7 +393,6 @@ fn executeMove(
405
393
  choice: Choice,
406
394
  rewrap: bool,
407
395
  skip: bool,
408
- from: ?Move,
409
396
  residual: *bool,
410
397
  options: anytype,
411
398
  ) !?Result {
@@ -422,7 +409,7 @@ fn executeMove(
422
409
  if (side.last_selected_move == .SKIP_TURN) {
423
410
  assert(!showdown);
424
411
  if (battle.foe(player).active.volatiles.Binding) {
425
- try options.log.cant(battle.active(player), .Bound);
412
+ try options.log.cant(.{ battle.active(player), .Bound });
426
413
  }
427
414
  return null;
428
415
  }
@@ -440,11 +427,13 @@ fn executeMove(
440
427
  mslot = @intCast(battle.lastMove(player).index);
441
428
  const stored = side.stored();
442
429
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
443
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
444
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
430
+ auto = isForced(&side.active) or
431
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
432
+ side.last_selected_move == .HyperBeam or
433
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
445
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
446
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
447
- // index 1 and the current Pokémon has the same move in its first slot.
436
+ // index 1 and the current Pokémon has the same move in its first slot
448
437
  if (!auto) {
449
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
450
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -463,7 +452,7 @@ fn executeMove(
463
452
 
464
453
  var skip_can = false;
465
454
  var skip_pp = false;
466
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
455
+ switch (try beforeMove(battle, player, skip, residual, options)) {
467
456
  .done => return null,
468
457
  .skip_can => skip_can = true,
469
458
  .skip_pp => skip_pp = true,
@@ -471,9 +460,9 @@ fn executeMove(
471
460
  .err => return @as(?Result, Result.Error),
472
461
  }
473
462
 
474
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
475
- return null;
476
- }
463
+ const can = skip_can or
464
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
465
+ if (!can) return null;
477
466
 
478
467
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
479
468
  }
@@ -484,7 +473,6 @@ fn beforeMove(
484
473
  battle: anytype,
485
474
  player: Player,
486
475
  skip: bool,
487
- from: ?Move,
488
476
  residual: *bool,
489
477
  options: anytype,
490
478
  ) !BeforeMove {
@@ -498,36 +486,41 @@ fn beforeMove(
498
486
 
499
487
  if (Status.is(stored.status, .SLP)) {
500
488
  const before = stored.status;
489
+ const slf = Status.is(stored.status, .EXT);
501
490
  // Even if the EXT bit is set this will still correctly modify the sleep duration
502
- if (options.calc.modify(player, .sleep)) |extend| {
503
- if (!extend) stored.status = 0;
491
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
492
+ .started, .ended => stored.status = 0,
493
+ .continuing => if (Status.duration(stored.status) > 1) {
494
+ stored.status -= 1;
495
+ },
496
+ else => unreachable,
504
497
  } else {
505
498
  stored.status -= 1;
506
499
  }
507
500
 
508
501
  const duration = Status.duration(stored.status);
509
- if (!Status.is(stored.status, .EXT)) {
510
- try options.chance.sleep(player, duration);
511
- }
502
+ try options.chance.sleep(player, if (slf)
503
+ .None
504
+ else if (duration == 0) .ended else .continuing);
512
505
 
513
506
  if (duration == 0) {
514
- try log.curestatus(ident, before, .Message);
507
+ try log.curestatus(.{ ident, before, .Message });
515
508
  stored.status = 0; // clears EXT if present
516
509
  } else {
517
- try log.cant(ident, .Sleep);
510
+ try log.cant(.{ ident, .Sleep });
518
511
  }
519
512
  side.last_used_move = .None;
520
513
  return .done;
521
514
  }
522
515
 
523
516
  if (Status.is(stored.status, .FRZ)) {
524
- try log.cant(ident, .Freeze);
517
+ try log.cant(.{ ident, .Freeze });
525
518
  side.last_used_move = .None;
526
519
  return .done;
527
520
  }
528
521
 
529
522
  if (skip or foe.active.volatiles.Binding) {
530
- try log.cant(ident, .Bound);
523
+ try log.cant(.{ ident, .Bound });
531
524
  return .done;
532
525
  }
533
526
 
@@ -535,27 +528,27 @@ fn beforeMove(
535
528
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
536
529
  // Pokémon can sometimes flinch multiple times from the same original hit
537
530
  if (!showdown) volatiles.Flinch = false;
538
- try log.cant(ident, .Flinch);
531
+ try log.cant(.{ ident, .Flinch });
539
532
  return .done;
540
533
  }
541
534
 
542
535
  if (volatiles.Recharging) {
543
536
  volatiles.Recharging = false;
544
- try log.cant(ident, .Recharge);
537
+ try log.cant(.{ ident, .Recharge });
545
538
  return .done;
546
539
  }
547
540
 
548
- if (volatiles.disabled_duration > 0) {
549
- if (options.calc.modify(player, .disable)) |extend| {
550
- if (!extend) volatiles.disabled_duration = 0;
551
- } else {
552
- volatiles.disabled_duration -= 1;
553
- }
554
- try options.chance.disable(player, volatiles.disabled_duration);
541
+ if (volatiles.disable_duration > 0) {
542
+ volatiles.disable_duration =
543
+ decrement(.disable, player, options, volatiles.disable_duration);
544
+ try options.chance.disable(
545
+ player,
546
+ if (volatiles.disable_duration == 0) .ended else .continuing,
547
+ );
555
548
 
556
- if (volatiles.disabled_duration == 0) {
557
- volatiles.disabled_move = 0;
558
- try log.end(ident, .Disable);
549
+ if (volatiles.disable_duration == 0) {
550
+ volatiles.disable_move = 0;
551
+ try log.end(.{ ident, .Disable });
559
552
  }
560
553
  }
561
554
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -570,19 +563,25 @@ fn beforeMove(
570
563
 
571
564
  if (volatiles.Confusion) {
572
565
  assert(volatiles.confusion > 0);
573
-
574
- if (options.calc.modify(player, .confusion)) |extend| {
575
- if (!extend) volatiles.confusion = 0;
566
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
+ .started => volatiles.confusion -= 1,
568
+ .continuing => if (volatiles.confusion > 1) {
569
+ volatiles.confusion -= 1;
570
+ },
571
+ .ended => volatiles.confusion = 0,
572
+ .overwritten => {},
573
+ else => unreachable,
576
574
  } else {
577
575
  volatiles.confusion -= 1;
578
576
  }
579
- try options.chance.confusion(player, volatiles.confusion);
577
+
578
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
580
579
 
581
580
  if (volatiles.confusion == 0) {
582
581
  volatiles.Confusion = false;
583
- try log.end(ident, .Confusion);
582
+ try log.end(.{ ident, .Confusion });
584
583
  } else {
585
- try log.activate(ident, .Confusion);
584
+ try log.activate(.{ ident, .Confusion });
586
585
 
587
586
  if (try Rolls.confused(battle, player, options)) {
588
587
  assert(!volatiles.MultiHit);
@@ -594,7 +593,9 @@ fn beforeMove(
594
593
  volatiles.Charging = false;
595
594
  volatiles.Binding = false;
596
595
  volatiles.Invulnerable = false;
597
- options.chance.clearDurations(player, null);
596
+
597
+ options.chance.observe(.attacking, player, .None);
598
+ options.chance.observe(.binding, player, .None);
598
599
  {
599
600
  // This feels (and is) disgusting but the cartridge literally just overwrites
600
601
  // the opponent's defense with the user's defense and resets it after. As a
@@ -602,7 +603,9 @@ fn beforeMove(
602
603
  const def = foe.active.stats.def;
603
604
  foe.active.stats.def = active.stats.def;
604
605
  defer foe.active.stats.def = def;
605
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
606
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
607
+ return .err;
608
+ }
606
609
  }
607
610
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
608
611
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -631,9 +634,12 @@ fn beforeMove(
631
634
  volatiles.Thrashing = false;
632
635
  volatiles.Charging = false;
633
636
  volatiles.Binding = false;
634
- options.chance.clearDurations(player, null);
637
+
638
+ options.chance.observe(.attacking, player, .None);
639
+ options.chance.observe(.binding, player, .None);
640
+
635
641
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
636
- try log.cant(ident, .Paralysis);
642
+ try log.cant(.{ ident, .Paralysis });
637
643
  return .done;
638
644
  }
639
645
 
@@ -649,28 +655,22 @@ fn beforeMove(
649
655
  volatiles.state +%= battle.last_damage;
650
656
  }
651
657
 
652
- assert(volatiles.attacks > 0);
653
-
654
- if (options.calc.modify(player, .attacking)) |extend| {
655
- if (!extend) volatiles.attacks = 0;
656
- } else {
657
- volatiles.attacks -= 1;
658
- }
659
- try options.chance.attacking(player, volatiles.attacks);
658
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
659
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
660
660
 
661
661
  if (volatiles.attacks != 0) {
662
- try log.activate(ident, .Bide);
662
+ try log.activate(.{ ident, .Bide });
663
663
  return .done;
664
664
  }
665
665
 
666
666
  volatiles.Bide = false;
667
- try log.end(ident, .Bide);
667
+ try log.end(.{ ident, .Bide });
668
668
 
669
669
  battle.last_damage = volatiles.state *% 2;
670
670
  volatiles.state = 0;
671
671
 
672
672
  if (battle.last_damage == 0) {
673
- try log.fail(ident, .None);
673
+ try log.fail(.{ ident, .None });
674
674
  return .done;
675
675
  }
676
676
 
@@ -690,47 +690,38 @@ fn beforeMove(
690
690
  }
691
691
 
692
692
  if (volatiles.Thrashing) {
693
- assert(volatiles.attacks > 0);
694
- assert(from != null);
693
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
695
694
 
696
- if (options.calc.modify(player, .attacking)) |extend| {
697
- if (!extend) volatiles.attacks = 0;
698
- } else {
699
- volatiles.attacks -= 1;
700
- }
701
- try options.chance.attacking(player, volatiles.attacks);
695
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
696
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
702
697
 
703
- if (!showdown and handleThrashing(battle, active, player, options)) {
704
- try log.start(battle.active(player), .ConfusionSilent);
705
- }
706
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
707
- if (showdown) {
708
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
709
- // used in useMove and doesn't have the notion of skipping canMove semantics
710
- side.last_used_move = side.last_selected_move;
711
- if (handleThrashing(battle, active, player, options)) {
712
- try log.start(battle.active(player), .ConfusionSilent);
713
- }
698
+ if (volatiles.attacks == 0) {
699
+ const overwritten = volatiles.Confusion;
700
+
701
+ volatiles.Thrashing = false;
702
+ volatiles.Confusion = true;
703
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
704
+
705
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
706
+ // ended (the engine's protocol is always from the omniscient perspective and clients
707
+ // can be expected to recognize this pattern and pass the information only to the
708
+ // correct player)
709
+ try log.start(.{ battle.active(player), .ConfusionSilent });
710
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
714
711
  }
712
+
713
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
714
+ // used in useMove and doesn't have the notion of skipping canMove semantics
715
+ if (showdown) side.last_used_move = side.last_selected_move;
716
+
715
717
  return .skip_can;
716
718
  }
717
719
 
718
720
  if (volatiles.Binding) {
719
- assert(volatiles.attacks > 0);
721
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
722
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
720
723
 
721
- if (options.calc.modify(player, .binding)) |extend| {
722
- if (!extend) volatiles.attacks = 0;
723
- } else {
724
- volatiles.attacks -= 1;
725
- }
726
- try options.chance.binding(player, volatiles.attacks);
727
-
728
- try log.move(
729
- battle.active(player),
730
- side.last_selected_move,
731
- battle.active(player.foe()),
732
- side.last_selected_move,
733
- );
724
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
734
725
  if (showdown or battle.last_damage != 0) {
735
726
  const sub = showdown and foe.active.volatiles.Substitute;
736
727
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -750,7 +741,7 @@ fn canMove(
750
741
  mslot: u4,
751
742
  auto: bool,
752
743
  skip_pp: bool,
753
- from: ?Move,
744
+ from: Move,
754
745
  residual: *bool,
755
746
  options: anytype,
756
747
  ) !bool {
@@ -758,33 +749,21 @@ fn canMove(
758
749
  const player_ident = battle.active(player);
759
750
  const move = Move.get(side.last_selected_move);
760
751
 
761
- const foe_last = battle.foe(player).last_used_move;
762
- const special = from != null and (from.? == .Metronome and
763
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
764
-
765
- var skip = skip_pp;
766
752
  if (side.active.volatiles.Charging) {
767
753
  side.active.volatiles.Charging = false;
768
754
  side.active.volatiles.Invulnerable = false;
769
755
  } else if (move.effect == .Charge) {
770
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
756
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
771
757
  setCounterable(battle, player, side, move);
772
758
  try Effects.charge(battle, player, options);
773
759
  return false;
774
760
  }
775
761
 
776
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
777
- if (!skip) decrementPP(side, mslot, auto);
762
+ side.last_used_move = side.last_selected_move;
763
+ if (!skip_pp) decrementPP(side, mslot, auto);
778
764
 
779
765
  const target = if (move.target == .Self) player else player.foe();
780
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
781
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
782
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
783
- // meaning sometimes we lose this information. from is still set because it not being null is
784
- // relevant for control flow, but we don't have anything useful to put in the log. This is
785
- // regrettable, but this information is more "nice to have" than required
786
- const f = if (from != null and from.? == .None) null else from;
787
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
766
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
788
767
  setCounterable(battle, player, side, move);
789
768
 
790
769
  if (move.effect.onBegin()) {
@@ -822,16 +801,18 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
822
801
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
823
802
  if (volatiles.Bide) return;
824
803
 
825
- assert(active.move(mslot).pp > 0 or auto);
826
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
804
+ var move_slot = active.move(mslot);
805
+ assert(move_slot.pp > 0 or auto);
806
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
827
807
  if (volatiles.Transform) return;
828
808
 
829
- assert(side.stored().move(mslot).pp > 0 or auto);
830
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
809
+ move_slot = side.stored().move(mslot);
810
+ assert(move_slot.pp > 0 or auto);
811
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
831
812
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
832
813
  }
833
814
 
834
- fn incrementPP(side: *Side, mslot: u4) void {
815
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
835
816
  var active = &side.active;
836
817
  const volatiles = &active.volatiles;
837
818
 
@@ -839,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
839
820
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
840
821
  if (showdown and volatiles.Transform) return;
841
822
 
823
+ const n = options.calc.overridden(player, .pp) orelse 1;
824
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
825
+
842
826
  assert(mslot > 0 and mslot <= 4);
843
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
827
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
844
828
  }
845
829
 
846
830
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -888,7 +872,7 @@ fn doMove(
888
872
  break :miss true;
889
873
  }
890
874
  break :miss (move.target != .Self and
891
- !moveHit(battle, player, move, &immune, &mist, options));
875
+ !try moveHit(battle, player, move, &immune, &mist, options));
892
876
  };
893
877
  assert(!immune or miss or (showdown and move.effect == .Binding));
894
878
 
@@ -917,7 +901,8 @@ fn doMove(
917
901
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
918
902
  battle.last_damage = if (ohko) 65535 else 0;
919
903
  if (showdown) break :blk; // skip adjustDamage / randomizeDamage
920
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
904
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
905
+ if (!showdown) try options.chance.commit(player, .err);
921
906
  return @as(?Result, Result.Error);
922
907
  }
923
908
  if (battle.last_damage == 0) {
@@ -931,11 +916,12 @@ fn doMove(
931
916
  }
932
917
  }
933
918
 
934
- var zero = battle.last_damage == 0;
935
- miss = if (showdown or skip)
936
- miss
937
- else
938
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
919
+ var missed: bool = undefined;
920
+ const zero = battle.last_damage == 0;
921
+ if (!showdown and !skip) {
922
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
923
+ miss = missed or zero;
924
+ }
939
925
 
940
926
  assert(showdown or miss or battle.last_damage > 0 or skip);
941
927
  assert((!showdown and miss) or !(ohko and immune));
@@ -959,16 +945,37 @@ fn doMove(
959
945
  ohko = (!showdown or (!immune and !invulnerable)) and
960
946
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
961
947
  if (ohko) {
962
- try log.immune(foe_ident, .OHKO);
948
+ try log.immune(.{ foe_ident, .OHKO });
963
949
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
964
- try log.immune(foe_ident, .None);
950
+ try log.immune(.{ foe_ident, .None });
951
+ if (!showdown) {
952
+ if (move.effect == .Binding) {
953
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
954
+ // then simply clears it later on if the move happens to miss in moveHit.
955
+ // However, moveHit can return true and we'll still end up in the outer miss
956
+ // handling block because attacking into an immune Pokemon does zero damage
957
+ // (considered missing)
958
+ assert(zero);
959
+ // Thus we still need to actually save the hit result (but not the crit roll).
960
+ // Using .miss for this commit would accomplish what we want, but is somewhat
961
+ // misleading as its entirely possible the move actually did hit (and the target
962
+ // is now trapped) so we use the .binding value instead which is effectively
963
+ // identical, just doesn't claim to know about the actual results of the hit
964
+ // roll)
965
+ try options.chance.commit(player, .binding);
966
+ } else {
967
+ // The .glitch option allows for conditionally committing if we are in a
968
+ // division-by-zero freeze alternative timeline scenario
969
+ try options.chance.commit(player, .glitch);
970
+ }
971
+ }
965
972
  } else if (mist) {
966
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
967
- try log.fail(foe_ident, .None);
973
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
974
+ try log.fail(.{ foe_ident, .None });
968
975
  } else {
969
- if (showdown or !zero) try options.chance.commit(player, .miss);
970
- try log.lastmiss();
971
- try log.miss(battle.active(player));
976
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
977
+ try log.lastmiss(.{});
978
+ try log.miss(.{battle.active(player)});
972
979
  }
973
980
  if (move.effect == .JumpKick) {
974
981
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -981,17 +988,15 @@ fn doMove(
981
988
  try buildRage(battle, player.foe(), options);
982
989
  } else if (showdown and move.effect == .Disable) {
983
990
  try buildRage(battle, player.foe(), options);
984
- } else if (!showdown and immune and move.effect == .Binding) {
985
- try options.chance.commit(player, .binding);
986
991
  }
987
992
  return null;
988
- } else {
993
+ } else if (!showdown) {
989
994
  try options.chance.commit(player, .hit);
990
995
  }
991
996
 
992
997
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
993
998
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
994
- // it up above before damage calculation).
999
+ // it up above before damage calculation)
995
1000
  if (!showdown and move.effect.isMulti()) {
996
1001
  try Effects.multiHit(battle, player, move, options);
997
1002
  hits = side.active.volatiles.attacks;
@@ -1004,15 +1009,15 @@ fn doMove(
1004
1009
  var hit: u4 = 0;
1005
1010
  while (hit < hits) {
1006
1011
  if (hit == 0) {
1007
- if (crit) try log.crit(battle.active(player.foe()));
1012
+ if (crit) try log.crit(.{battle.active(player.foe())});
1008
1013
  if (effectiveness > Effectiveness.neutral) {
1009
- try log.supereffective(battle.active(player.foe()));
1014
+ try log.supereffective(.{battle.active(player.foe())});
1010
1015
  } else if (effectiveness < Effectiveness.neutral) {
1011
- try log.resisted(battle.active(player.foe()));
1016
+ try log.resisted(.{battle.active(player.foe())});
1012
1017
  }
1013
1018
  }
1014
1019
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
1015
- if (hit == 0 and ohko) try log.ohko();
1020
+ if (hit == 0 and ohko) try log.ohko(.{});
1016
1021
  hit += 1;
1017
1022
  if (foe.stored().hp == 0) break;
1018
1023
  if (!late and (!sub or move.effect == .Explode)) {
@@ -1029,7 +1034,7 @@ fn doMove(
1029
1034
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1030
1035
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1031
1036
  }
1032
- try log.hitcount(battle.active(player.foe()), hit);
1037
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1033
1038
  } else if (showdown) {
1034
1039
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1035
1040
  if (move.effect == .Binding) {
@@ -1039,9 +1044,9 @@ fn doMove(
1039
1044
  assert(foe.stored().hp > 0);
1040
1045
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1041
1046
  if (sub) {
1042
- try log.activate(battle.active(player.foe()), .Substitute);
1047
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1043
1048
  } else {
1044
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1049
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1045
1050
  try buildRage(battle, player.foe(), options);
1046
1051
  }
1047
1052
  return null;
@@ -1050,12 +1055,12 @@ fn doMove(
1050
1055
  }
1051
1056
 
1052
1057
  // Substitute being broken nullifies the move's effect completely so even
1053
- // if an effect was intended to "always happen" it will still get skipped.
1058
+ // if an effect was intended to "always happen" it will still get skipped
1054
1059
  if (nullified) return null;
1055
1060
 
1056
1061
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1057
1062
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1058
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1063
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1059
1064
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1060
1065
 
1061
1066
  if (foe.stored().hp == 0) return null;
@@ -1070,7 +1075,7 @@ fn doMove(
1070
1075
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1071
1076
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1072
1077
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1073
- // puts the |-status| message before |-hitcount| instead of after.
1078
+ // puts the |-status| message before |-hitcount| instead of after
1074
1079
  if (move.effect == .Twineedle) {
1075
1080
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1076
1081
  } else if (move.effect == .Disable) {
@@ -1089,12 +1094,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1089
1094
  const side = battle.side(player);
1090
1095
 
1091
1096
  // Base speed is used for the critical hit calculation, even when Transform-ed
1092
- var rate = @as(u16, Species.chance(side.stored().species));
1097
+ var rate: u16 = Species.chance(side.stored().species);
1093
1098
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1094
1099
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1095
1100
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1096
1101
 
1097
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1102
+ const spurious =
1103
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1104
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1098
1105
  }
1099
1106
 
1100
1107
  fn calcDamage(
@@ -1103,6 +1110,7 @@ fn calcDamage(
1103
1110
  target_player: Player,
1104
1111
  m: ?Move.Data,
1105
1112
  crit: bool,
1113
+ options: anytype,
1106
1114
  ) bool {
1107
1115
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1108
1116
  const cfz = m == null;
@@ -1114,28 +1122,8 @@ fn calcDamage(
1114
1122
 
1115
1123
  const special = move.type.special();
1116
1124
 
1117
- // zig fmt: off
1118
- var atk: u32 =
1119
- if (crit)
1120
- if (special) side.stored().stats.spc
1121
- else side.stored().stats.atk
1122
- else
1123
- if (special) side.active.stats.spc
1124
- else side.active.stats.atk;
1125
- var def: u32 =
1126
- if (crit)
1127
- if (special) target.stored().stats.spc
1128
- else target.stored().stats.def
1129
- else
1130
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1131
- if (special)
1132
- target.active.stats.spc *
1133
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1134
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1135
- else
1136
- target.active.stats.def *
1137
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1138
- // zig fmt: on
1125
+ var atk = attack(side, crit, special);
1126
+ var def = defense(target, crit, special, cfz);
1139
1127
 
1140
1128
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1141
1129
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1146,9 +1134,9 @@ fn calcDamage(
1146
1134
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1147
1135
  }
1148
1136
 
1149
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1137
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1150
1138
 
1151
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1139
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1152
1140
 
1153
1141
  if (def == 0) return false;
1154
1142
 
@@ -1160,11 +1148,41 @@ fn calcDamage(
1160
1148
  d = @min(997, d);
1161
1149
  d += 2;
1162
1150
 
1151
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1152
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1153
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1154
+ // that results in counter intuitive probabilities in the common case
1155
+ if (pkmn.options.chance and !showdown) {
1156
+ atk = attack(side, !crit, special);
1157
+ def = defense(target, !crit, special, false);
1158
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1159
+ }
1160
+
1163
1161
  battle.last_damage = @intCast(d);
1164
1162
 
1165
1163
  return true;
1166
1164
  }
1167
1165
 
1166
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1167
+ return if (crit)
1168
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1169
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1170
+ }
1171
+
1172
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1173
+ return if (crit)
1174
+ if (special) target.stored().stats.spc else target.stored().stats.def
1175
+ else
1176
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1177
+ if (special)
1178
+ target.active.stats.spc *
1179
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1180
+ else
1181
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1182
+ target.active.stats.def *
1183
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1184
+ }
1185
+
1168
1186
  fn adjustDamage(battle: anytype, player: Player) u16 {
1169
1187
  const side = battle.side(player);
1170
1188
  const foe = battle.foe(player);
@@ -1209,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1227
  const foe = battle.foe(player);
1210
1228
 
1211
1229
  if (!try checkHit(battle, player, move, options)) return null;
1212
- try options.chance.commit(player, .hit);
1213
1230
 
1214
1231
  battle.last_damage = switch (side.last_selected_move) {
1215
1232
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1239,7 +1256,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1239
1256
  const foe = battle.foe(player);
1240
1257
 
1241
1258
  if (battle.last_damage == 0) {
1242
- try options.log.fail(battle.active(player), .None);
1259
+ try options.log.fail(.{ battle.active(player), .None });
1243
1260
  return null;
1244
1261
  }
1245
1262
 
@@ -1257,22 +1274,21 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1257
1274
  foe_last_selected_move.type == .Fighting);
1258
1275
 
1259
1276
  if (!used and !selected) {
1260
- try options.log.fail(battle.active(player), .None);
1277
+ try options.log.fail(.{ battle.active(player), .None });
1261
1278
  return null;
1262
1279
  }
1263
1280
 
1264
1281
  if (!used or !selected) {
1265
1282
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1266
1283
  if (!showdown) return Result.Error;
1267
- try options.log.fail(battle.active(player), .None);
1284
+ try options.log.fail(.{ battle.active(player), .None });
1268
1285
  return null;
1269
1286
  }
1270
1287
 
1271
1288
  battle.last_damage *|= 2;
1272
1289
 
1273
- // Pokémon Showdown calls checkHit before Counter
1290
+ // Pokémon Showdown calls moveHit before Counter
1274
1291
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1275
- try options.chance.commit(player, .hit);
1276
1292
 
1277
1293
  const sub = showdown and foe.active.volatiles.Substitute;
1278
1294
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1300,20 +1316,20 @@ fn applyDamage(
1300
1316
  subbed.active.volatiles.substitute = 0;
1301
1317
  subbed.active.volatiles.Substitute = false;
1302
1318
  // battle.last_damage is not updated with the amount of HP the Substitute had
1303
- try options.log.end(battle.active(sub_player), .Substitute);
1304
- options.calc.capped(target_player);
1319
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1320
+ options.calc.capped(sub_player);
1305
1321
  return true;
1306
1322
  } else {
1307
1323
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1308
1324
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1309
- try options.log.activate(battle.active(sub_player), .Substitute);
1325
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1310
1326
  return false;
1311
1327
  }
1312
1328
  }
1313
1329
 
1314
1330
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1315
1331
  target.stored().hp -= battle.last_damage;
1316
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1332
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1317
1333
  return false;
1318
1334
  }
1319
1335
 
@@ -1332,10 +1348,10 @@ fn mirrorMove(
1332
1348
  side.last_selected_move = foe.last_used_move;
1333
1349
 
1334
1350
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1335
- try options.log.fail(battle.active(player), .None);
1351
+ try options.log.fail(.{ battle.active(player), .None });
1336
1352
  return null;
1337
1353
  } else if (!showdown or foe.last_used_move != .Struggle) {
1338
- incrementPP(side, mslot);
1354
+ incrementPP(side, player, mslot, options);
1339
1355
  }
1340
1356
 
1341
1357
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1356,7 +1372,7 @@ fn metronome(
1356
1372
  var side = battle.side(player);
1357
1373
 
1358
1374
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1359
- incrementPP(side, mslot);
1375
+ incrementPP(side, player, mslot, options);
1360
1376
 
1361
1377
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1362
1378
  return null;
@@ -1368,20 +1384,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1368
1384
  var immune = false;
1369
1385
  var mist = false;
1370
1386
 
1371
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1387
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1388
+ if (!showdown) try options.chance.commit(player, .hit);
1389
+ return true;
1390
+ }
1372
1391
 
1373
1392
  assert(!immune);
1374
1393
  if (mist) {
1375
1394
  assert(!showdown);
1376
1395
  const foe_ident = battle.active(player.foe());
1377
- try options.log.activate(foe_ident, .Mist);
1378
- try options.log.fail(foe_ident, .None);
1396
+ try options.log.activate(.{ foe_ident, .Mist });
1397
+ try options.log.fail(.{ foe_ident, .None });
1379
1398
  } else {
1380
- try options.chance.commit(player, .miss);
1381
- try options.log.lastmiss();
1382
- try options.log.miss(battle.active(player));
1399
+ try options.log.lastmiss(.{});
1400
+ try options.log.miss(.{battle.active(player)});
1383
1401
  }
1384
1402
 
1403
+ if (!showdown) try options.chance.commit(player, .miss);
1385
1404
  return false;
1386
1405
  }
1387
1406
 
@@ -1392,7 +1411,7 @@ fn moveHit(
1392
1411
  immune: *bool,
1393
1412
  mist: *bool,
1394
1413
  options: anytype,
1395
- ) bool {
1414
+ ) !bool {
1396
1415
  var side = battle.side(player);
1397
1416
  const foe = battle.foe(player);
1398
1417
 
@@ -1431,7 +1450,7 @@ fn moveHit(
1431
1450
  const overwritten = overwrite and state > 0;
1432
1451
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1433
1452
 
1434
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1453
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1435
1454
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1436
1455
  accuracy = accuracy * boost[0] / boost[1];
1437
1456
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1443,7 +1462,7 @@ fn moveHit(
1443
1462
  if (overwrite) side.active.volatiles.state = accuracy;
1444
1463
 
1445
1464
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1446
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1465
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1447
1466
  };
1448
1467
 
1449
1468
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1454,13 +1473,17 @@ fn moveHit(
1454
1473
 
1455
1474
  if (!miss) return true;
1456
1475
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1476
+
1457
1477
  side.active.volatiles.Binding = false;
1478
+ options.chance.observe(.binding, player, .None);
1479
+
1458
1480
  return false;
1459
1481
  }
1460
1482
 
1461
1483
  fn checkFaint(
1462
1484
  battle: anytype,
1463
1485
  player: Player,
1486
+ cap: bool,
1464
1487
  options: anytype,
1465
1488
  ) @TypeOf(options.log).Error!?Result {
1466
1489
  const side = battle.side(player);
@@ -1470,24 +1493,23 @@ fn checkFaint(
1470
1493
  const foe_fainted = foe.stored().hp == 0;
1471
1494
 
1472
1495
  const player_out = findFirstAlive(side) == 0;
1473
- const foe_out = findFirstAlive(foe) == 0;
1474
- const tie = player_out and foe_out;
1475
- const more = tie or player_out or foe_out;
1496
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1497
+ const more = player_out or foe_out;
1476
1498
 
1477
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1478
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1499
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1500
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1479
1501
 
1480
1502
  assert(!side.active.volatiles.MultiHit);
1481
1503
  assert(!foe.active.volatiles.MultiHit);
1482
1504
 
1483
- if (tie) {
1484
- try options.log.tie();
1505
+ if (player_out and foe_out) {
1506
+ try options.log.tie(.{});
1485
1507
  return Result.Tie;
1486
1508
  } else if (player_out) {
1487
- try options.log.win(player.foe());
1509
+ try options.log.win(.{player.foe()});
1488
1510
  return if (player == .P1) Result.Lose else Result.Win;
1489
1511
  } else if (foe_out) {
1490
- try options.log.win(player);
1512
+ try options.log.win(.{player});
1491
1513
  return if (player == .P1) Result.Win else Result.Lose;
1492
1514
  }
1493
1515
 
@@ -1496,7 +1518,7 @@ fn checkFaint(
1496
1518
  return .{ .p1 = foe_choice, .p2 = .Switch };
1497
1519
  }
1498
1520
 
1499
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1521
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1500
1522
  var side = battle.side(player);
1501
1523
  var foe = battle.foe(player);
1502
1524
  assert(side.stored().hp == 0);
@@ -1529,8 +1551,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1529
1551
  side.stored().stats.spe;
1530
1552
  }
1531
1553
 
1532
- try options.log.faint(battle.active(player), done);
1533
- options.calc.capped(player);
1554
+ try options.log.faint(.{ battle.active(player), done });
1555
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1556
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1557
+ if (cap) options.calc.capped(player);
1534
1558
  return null;
1535
1559
  }
1536
1560
 
@@ -1551,8 +1575,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1551
1575
 
1552
1576
  if (!showdown or damage > 0) {
1553
1577
  stored.hp -= damage;
1554
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1555
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1578
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1579
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1556
1580
  }
1557
1581
  }
1558
1582
 
@@ -1577,14 +1601,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1577
1601
  const amount = @min(damage, stored.hp);
1578
1602
  stored.hp -= amount;
1579
1603
 
1580
- // As above, Pokémon Showdown uses damageOf but its not relevant
1581
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1604
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1605
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1582
1606
 
1583
1607
  const before = foe_stored.hp;
1584
1608
  // Uncapped damage is added back to the foe
1585
1609
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1586
1610
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1587
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1611
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1588
1612
  }
1589
1613
  }
1590
1614
 
@@ -1595,18 +1619,18 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1595
1619
  battle.turn += 1;
1596
1620
 
1597
1621
  if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1598
- try options.log.tie();
1622
+ try options.log.tie(.{});
1599
1623
  return Result.Tie;
1600
1624
  }
1601
1625
 
1602
1626
  if (showdown and battle.turn >= 1000) {
1603
- try options.log.tie();
1627
+ try options.log.tie(.{});
1604
1628
  return Result.Tie;
1605
1629
  } else if (battle.turn >= 65535) {
1606
1630
  return Result.Error;
1607
1631
  }
1608
1632
 
1609
- try options.log.turn(battle.turn);
1633
+ try options.log.turn(.{battle.turn});
1610
1634
 
1611
1635
  return Result.Default;
1612
1636
  }
@@ -1666,9 +1690,9 @@ fn checkEBC(battle: anytype) bool {
1666
1690
  }
1667
1691
 
1668
1692
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1669
- if (side.active.volatiles.disabled_move != 0) {
1693
+ if (side.active.volatiles.disable_move != 0) {
1670
1694
  // A Pokémon that transforms after being disabled may end up with less move slots
1671
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1695
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1672
1696
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1673
1697
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1674
1698
  m
@@ -1676,32 +1700,21 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1676
1700
  side.last_selected_move;
1677
1701
  if (m != .None and m == last) {
1678
1702
  side.active.volatiles.Charging = false;
1679
- try options.log.disabled(ident, last);
1703
+ try options.log.cant(.{ ident, .Disable, last });
1680
1704
  return true;
1681
1705
  }
1682
1706
  }
1683
1707
  return false;
1684
1708
  }
1685
1709
 
1686
- inline fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1710
+ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1687
1711
  const side = battle.side(who);
1688
1712
  if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1689
1713
  try Effects.boost(battle, who, Move.get(.Rage), options);
1690
1714
  }
1691
1715
  }
1692
1716
 
1693
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1694
- var volatiles = &active.volatiles;
1695
- assert(volatiles.Thrashing);
1696
- if (volatiles.attacks > 0) return false;
1697
-
1698
- volatiles.Thrashing = false;
1699
- volatiles.Confusion = true;
1700
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1701
- return true;
1702
- }
1703
-
1704
- inline fn onBegin(
1717
+ fn onBegin(
1705
1718
  battle: anytype,
1706
1719
  player: Player,
1707
1720
  move: Move.Data,
@@ -1709,7 +1722,6 @@ inline fn onBegin(
1709
1722
  residual: *bool,
1710
1723
  options: anytype,
1711
1724
  ) !void {
1712
- assert(move.effect.onBegin());
1713
1725
  return switch (move.effect) {
1714
1726
  .Confusion => Effects.confusion(battle, player, move, options),
1715
1727
  .Conversion => Effects.conversion(battle, player, options),
@@ -1731,8 +1743,7 @@ inline fn onBegin(
1731
1743
  };
1732
1744
  }
1733
1745
 
1734
- inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1735
- assert(move.effect.onEnd());
1746
+ fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1736
1747
  return switch (move.effect) {
1737
1748
  .Bide => Effects.bide(battle, player, options),
1738
1749
  // zig fmt: off
@@ -1747,14 +1758,13 @@ inline fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anyty
1747
1758
  };
1748
1759
  }
1749
1760
 
1750
- inline fn alwaysHappens(
1761
+ fn alwaysHappens(
1751
1762
  battle: anytype,
1752
1763
  player: Player,
1753
1764
  move: Move.Data,
1754
1765
  residual: *bool,
1755
1766
  options: anytype,
1756
1767
  ) !void {
1757
- assert(move.effect.alwaysHappens());
1758
1768
  return switch (move.effect) {
1759
1769
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1760
1770
  .Explode => Effects.explode(battle, player),
@@ -1766,7 +1776,7 @@ inline fn alwaysHappens(
1766
1776
  };
1767
1777
  }
1768
1778
 
1769
- inline fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1779
+ fn moveEffect(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1770
1780
  return switch (move.effect) {
1771
1781
  .BurnChance1, .BurnChance2 => Effects.burnChance(battle, player, move, options),
1772
1782
  .ConfusionChance => Effects.confusion(battle, player, move, options),
@@ -1790,10 +1800,12 @@ pub const Effects = struct {
1790
1800
 
1791
1801
  side.active.volatiles.Bide = true;
1792
1802
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1803
+
1793
1804
  side.active.volatiles.state = 0;
1794
1805
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1806
+ options.chance.observe(.attacking, player, .started);
1795
1807
 
1796
- try options.log.start(battle.active(player), .Bide);
1808
+ try options.log.start(.{ battle.active(player), .Bide });
1797
1809
  }
1798
1810
 
1799
1811
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1807,11 +1819,11 @@ pub const Effects = struct {
1807
1819
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1808
1820
  if (Status.is(foe_stored.status, .FRZ)) {
1809
1821
  assert(move.type == .Fire);
1810
- try options.log.curestatus(
1822
+ try options.log.curestatus(.{
1811
1823
  battle.active(player.foe()),
1812
1824
  foe_stored.status,
1813
1825
  .Message,
1814
- );
1826
+ });
1815
1827
  foe_stored.status = 0;
1816
1828
  }
1817
1829
  return;
@@ -1825,7 +1837,7 @@ pub const Effects = struct {
1825
1837
  foe_stored.status = Status.init(.BRN);
1826
1838
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1827
1839
 
1828
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1840
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1829
1841
  }
1830
1842
 
1831
1843
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1835,8 +1847,8 @@ pub const Effects = struct {
1835
1847
  volatiles.Charging = true;
1836
1848
  const move = side.last_selected_move;
1837
1849
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1838
- try options.log.laststill();
1839
- try options.log.prepare(battle.active(player), move);
1850
+ try options.log.laststill(.{});
1851
+ try options.log.prepare(.{ battle.active(player), move });
1840
1852
  }
1841
1853
 
1842
1854
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1847,42 +1859,38 @@ pub const Effects = struct {
1847
1859
  if (!try Rolls.confusionChance(battle, player, options)) return;
1848
1860
  } else {
1849
1861
  if (showdown) {
1850
- if (!try checkHit(battle, player, move, options)) {
1851
- return;
1852
- } else if (sub) {
1853
- return options.log.fail(battle.active(player.foe()), .None);
1854
- }
1862
+ if (!try checkHit(battle, player, move, options)) return;
1863
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1855
1864
  } else {
1856
- if (sub) {
1857
- return options.log.fail(battle.active(player.foe()), .None);
1858
- } else if (!try checkHit(battle, player, move, options)) {
1859
- return;
1860
- }
1865
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1866
+ if (!try checkHit(battle, player, move, options)) return;
1861
1867
  }
1862
- try options.chance.commit(player, .hit);
1863
1868
  }
1864
1869
 
1865
1870
  if (foe.active.volatiles.Confusion) return;
1866
1871
  foe.active.volatiles.Confusion = true;
1867
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1868
1872
 
1869
- try options.log.start(battle.active(player.foe()), .Confusion);
1873
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1874
+ options.chance.observe(.confusion, player.foe(), .started);
1875
+
1876
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1870
1877
  }
1871
1878
 
1872
1879
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1873
1880
  const foe = battle.foe(player);
1874
1881
 
1875
1882
  if (foe.active.volatiles.Invulnerable) {
1876
- try options.log.lastmiss();
1877
- return options.log.miss(battle.active(player));
1883
+ try options.log.lastmiss(.{});
1884
+ return options.log.miss(.{battle.active(player)});
1878
1885
  }
1879
1886
 
1880
1887
  battle.side(player).active.types = foe.active.types;
1881
- return options.log.typechange(
1888
+ return options.log.start(.{
1882
1889
  battle.active(player),
1890
+ .TypeChange,
1883
1891
  foe.active.types,
1884
1892
  battle.active(player.foe()),
1885
- );
1893
+ });
1886
1894
  }
1887
1895
 
1888
1896
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1891,13 +1899,10 @@ pub const Effects = struct {
1891
1899
  const foe_ident = battle.active(player.foe());
1892
1900
 
1893
1901
  // Pokémon Showdown handles hit/miss earlier in doMove
1894
- if (!showdown) {
1895
- if (!try checkHit(battle, player, move, options)) return null;
1896
- try options.chance.commit(player, .hit);
1897
- }
1902
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1898
1903
 
1899
- if (volatiles.disabled_move != 0) {
1900
- try options.log.fail(foe_ident, .None);
1904
+ if (volatiles.disable_move != 0) {
1905
+ try options.log.fail(.{ foe_ident, .None });
1901
1906
  return null;
1902
1907
  }
1903
1908
 
@@ -1913,7 +1918,7 @@ pub const Effects = struct {
1913
1918
  // Technically this is still considered simply a "miss" on the cartridge,
1914
1919
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1915
1920
  if (n == 0) {
1916
- try options.log.fail(foe_ident, .None);
1921
+ try options.log.fail(.{ foe_ident, .None });
1917
1922
  return null;
1918
1923
  } else if (err) {
1919
1924
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
@@ -1921,18 +1926,19 @@ pub const Effects = struct {
1921
1926
  return Result.Error;
1922
1927
  }
1923
1928
 
1924
- volatiles.disabled_move =
1929
+ volatiles.disable_move =
1925
1930
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1926
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1931
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1932
+ options.chance.observe(.disable, player.foe(), .started);
1927
1933
 
1928
- const id = foe.active.move(volatiles.disabled_move).id;
1929
- try options.log.startEffect(foe_ident, .Disable, id);
1934
+ const id = foe.active.move(volatiles.disable_move).id;
1935
+ try options.log.start(.{ foe_ident, .Disable, id });
1930
1936
  return null;
1931
1937
  }
1932
1938
 
1933
1939
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1934
- var side = battle.side(player);
1935
- var stored = side.stored();
1940
+ var stored = battle.side(player).stored();
1941
+ const ident = battle.active(player);
1936
1942
 
1937
1943
  if (battle.last_damage == 0) {
1938
1944
  assert(showdown);
@@ -1945,7 +1951,7 @@ pub const Effects = struct {
1945
1951
  if (stored.hp == stored.stats.hp) return;
1946
1952
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1947
1953
 
1948
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1954
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1949
1955
  }
1950
1956
 
1951
1957
  fn explode(battle: anytype, player: Player) !void {
@@ -1977,7 +1983,7 @@ pub const Effects = struct {
1977
1983
  if (side.active.volatiles.FocusEnergy) return;
1978
1984
  side.active.volatiles.FocusEnergy = true;
1979
1985
 
1980
- try options.log.start(battle.active(player), .FocusEnergy);
1986
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1981
1987
  }
1982
1988
 
1983
1989
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1999,7 +2005,7 @@ pub const Effects = struct {
1999
2005
  foe_stored.status = Status.init(.FRZ);
2000
2006
  // GLITCH: Hyper Beam recharging status is not cleared
2001
2007
 
2002
- try options.log.status(foe_ident, foe_stored.status, .None);
2008
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
2003
2009
  }
2004
2010
 
2005
2011
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -2007,7 +2013,7 @@ pub const Effects = struct {
2007
2013
  var side = battle.side(player);
2008
2014
  var foe = battle.foe(player);
2009
2015
 
2010
- var side_stored = side.stored();
2016
+ const side_stored = side.stored();
2011
2017
  var foe_stored = foe.stored();
2012
2018
 
2013
2019
  const player_ident = battle.active(player);
@@ -2019,8 +2025,8 @@ pub const Effects = struct {
2019
2025
  side.active.stats = side_stored.stats;
2020
2026
  foe.active.stats = foe_stored.stats;
2021
2027
 
2022
- try log.activate(player_ident, .Haze);
2023
- try log.clearallboost();
2028
+ try log.activate(.{ player_ident, .Haze });
2029
+ try log.clearallboost(.{});
2024
2030
 
2025
2031
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
2026
2032
  if (showdown) {
@@ -2028,24 +2034,26 @@ pub const Effects = struct {
2028
2034
  const p: Player = @enumFromInt(i);
2029
2035
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
2030
2036
  if (p != player and Status.any(s.stored().status)) {
2031
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2037
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2032
2038
  s.stored().status = 0;
2039
+ options.chance.observe(.sleep, p, .None);
2033
2040
  } else if (showdown and s.stored().status == Status.TOX) {
2034
2041
  s.stored().status = Status.init(.PSN);
2035
- try log.status(battle.active(p), s.stored().status, .None);
2042
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2036
2043
  }
2037
- try clearVolatiles(battle, p, p != player, options);
2044
+ try clearVolatiles(battle, p, options);
2038
2045
  }
2039
2046
  } else {
2040
2047
  if (Status.any(foe_stored.status)) {
2041
2048
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2042
2049
  foe.last_selected_move = .SKIP_TURN;
2050
+ options.chance.observe(.sleep, player.foe(), .None);
2043
2051
  }
2044
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2052
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2045
2053
  foe_stored.status = 0;
2046
2054
  }
2047
- try clearVolatiles(battle, player, false, options);
2048
- try clearVolatiles(battle, player.foe(), true, options);
2055
+ try clearVolatiles(battle, player, options);
2056
+ try clearVolatiles(battle, player.foe(), options);
2049
2057
  }
2050
2058
  }
2051
2059
 
@@ -2057,7 +2065,7 @@ pub const Effects = struct {
2057
2065
  // GLITCH: HP recovery move failure glitches
2058
2066
  const delta = stored.stats.hp - stored.hp;
2059
2067
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2060
- return try options.log.fail(ident, .None);
2068
+ return options.log.fail(.{ ident, .None });
2061
2069
  }
2062
2070
 
2063
2071
  const rest = side.last_selected_move == .Rest;
@@ -2065,17 +2073,17 @@ pub const Effects = struct {
2065
2073
  // Adding the sleep status runs the sleep condition handler to roll duration
2066
2074
  if (showdown) battle.rng.advance(1);
2067
2075
  stored.status = Status.slf(2);
2068
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2076
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2069
2077
  stored.hp = stored.stats.hp;
2070
2078
  } else {
2071
2079
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2072
2080
  }
2073
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2081
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2074
2082
  }
2075
2083
 
2076
2084
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2077
2085
  battle.side(player).active.volatiles.Recharging = true;
2078
- try options.log.mustrecharge(battle.active(player));
2086
+ try options.log.mustrecharge(.{battle.active(player)});
2079
2087
  }
2080
2088
 
2081
2089
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2084,33 +2092,31 @@ pub const Effects = struct {
2084
2092
  if (showdown) {
2085
2093
  // Invulnerability trumps type immunity on Pokémon Showdown
2086
2094
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2087
- return options.log.immune(battle.active(player.foe()), .None);
2095
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2088
2096
  }
2089
2097
  if (!try checkHit(battle, player, move, options)) return;
2090
2098
  if (foe.active.volatiles.LeechSeed) return;
2091
2099
  } else {
2092
2100
  if (!try checkHit(battle, player, move, options)) return;
2093
2101
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2094
- try options.log.lastmiss();
2095
- return options.log.miss(battle.active(player));
2102
+ try options.log.lastmiss(.{});
2103
+ return options.log.miss(.{battle.active(player)});
2096
2104
  }
2097
2105
  }
2098
2106
 
2099
- try options.chance.commit(player, .hit);
2100
2107
  foe.active.volatiles.LeechSeed = true;
2101
2108
 
2102
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2109
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2103
2110
  }
2104
2111
 
2105
2112
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2106
2113
  var side = battle.side(player);
2114
+ const ident = battle.active(player);
2107
2115
 
2108
- if (side.active.volatiles.LightScreen) {
2109
- return options.log.fail(battle.active(player), .None);
2110
- }
2116
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2111
2117
  side.active.volatiles.LightScreen = true;
2112
2118
 
2113
- try options.log.start(battle.active(player), .LightScreen);
2119
+ try options.log.start(.{ ident, .LightScreen });
2114
2120
  }
2115
2121
 
2116
2122
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2138,17 +2144,15 @@ pub const Effects = struct {
2138
2144
  if (!has_mimic) {
2139
2145
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2140
2146
  if (!try checkHit(battle, player, move, options)) return;
2141
- try options.chance.commit(player, .hit);
2142
- return try options.log.fail(battle.active(player.foe()), .None);
2147
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2143
2148
  }
2144
2149
  }
2145
2150
  if (!try checkHit(battle, player, move, options)) return;
2146
- try options.chance.commit(player, .hit);
2147
2151
 
2148
2152
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2149
2153
  side.active.move(oslot).id = foe.active.move(rslot).id;
2150
2154
 
2151
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2155
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2152
2156
  }
2153
2157
 
2154
2158
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2157,7 +2161,7 @@ pub const Effects = struct {
2157
2161
  if (side.active.volatiles.Mist) return;
2158
2162
  side.active.volatiles.Mist = true;
2159
2163
 
2160
- try options.log.start(battle.active(player), .Mist);
2164
+ try options.log.start(.{ battle.active(player), .Mist });
2161
2165
  }
2162
2166
 
2163
2167
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2183,25 +2187,25 @@ pub const Effects = struct {
2183
2187
 
2184
2188
  if (showdown) {
2185
2189
  // Invulnerability trumps type immunity on Pokémon Showdown
2186
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2187
- if (!try checkHit(battle, player, move, options)) return;
2190
+ if (immune and !foe.active.volatiles.Invulnerable) {
2191
+ return log.immune(.{ foe_ident, .None });
2192
+ } else if (!try checkHit(battle, player, move, options)) return;
2188
2193
  }
2189
2194
  if (Status.any(foe_stored.status)) {
2190
- return log.fail(
2195
+ return log.fail(.{
2191
2196
  foe_ident,
2192
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2193
- );
2197
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2198
+ });
2194
2199
  }
2195
2200
  if (!showdown) {
2196
- if (immune) return log.immune(foe_ident, .None);
2201
+ if (immune) return log.immune(.{ foe_ident, .None });
2197
2202
  if (!try checkHit(battle, player, move, options)) return;
2198
2203
  }
2199
- try options.chance.commit(player, .hit);
2200
2204
 
2201
2205
  foe_stored.status = Status.init(.PAR);
2202
2206
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2203
2207
 
2204
- try log.status(foe_ident, foe_stored.status, .None);
2208
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2205
2209
  }
2206
2210
 
2207
2211
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2223,12 +2227,12 @@ pub const Effects = struct {
2223
2227
  foe_stored.status = Status.init(.PAR);
2224
2228
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2225
2229
 
2226
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2230
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2227
2231
  }
2228
2232
 
2229
2233
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2230
2234
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2231
- try options.log.fieldactivate();
2235
+ try options.log.fieldactivate(.{});
2232
2236
  }
2233
2237
  }
2234
2238
 
@@ -2243,28 +2247,27 @@ pub const Effects = struct {
2243
2247
  return;
2244
2248
  } else if (foe.active.volatiles.Substitute) {
2245
2249
  if (move.effect != .Poison) return;
2246
- return log.fail(foe_ident, .None);
2250
+ return log.fail(.{ foe_ident, .None });
2247
2251
  } else if (Status.any(foe_stored.status)) {
2248
2252
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2249
2253
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2250
2254
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2251
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2255
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2252
2256
  if (!showdown)
2253
- .Poison
2257
+ Fail.Poison
2254
2258
  else if (toxic == (foe_stored.status == Status.TOX))
2255
- if (toxic) .Toxic else .Poison
2259
+ if (toxic) Fail.Toxic else Fail.Poison
2256
2260
  else
2257
- .None
2261
+ Fail.None
2258
2262
  else
2259
- .None);
2263
+ Fail.None });
2260
2264
  } else if (foe.active.types.includes(.Poison)) {
2261
2265
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2262
- return log.immune(foe_ident, .None);
2266
+ return log.immune(.{ foe_ident, .None });
2263
2267
  }
2264
2268
 
2265
2269
  if (move.effect == .Poison) {
2266
2270
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2267
- try options.chance.commit(player, .hit);
2268
2271
  } else {
2269
2272
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2270
2273
  return;
@@ -2278,7 +2281,7 @@ pub const Effects = struct {
2278
2281
  foe.active.volatiles.toxic = 0;
2279
2282
  }
2280
2283
 
2281
- try log.status(foe_ident, foe_stored.status, .None);
2284
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2282
2285
  }
2283
2286
 
2284
2287
  fn rage(battle: anytype, player: Player) !void {
@@ -2298,22 +2301,23 @@ pub const Effects = struct {
2298
2301
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2299
2302
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2300
2303
 
2301
- try options.log.damageOf(
2304
+ try options.log.damage(.{
2302
2305
  battle.active(player),
2303
2306
  stored,
2304
2307
  .RecoilOf,
2305
2308
  battle.active(player.foe()),
2306
- );
2309
+ });
2307
2310
  if (showdown and stored.hp == 0) residual.* = false;
2308
2311
  }
2309
2312
 
2310
2313
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2311
2314
  var side = battle.side(player);
2315
+ const ident = battle.active(player);
2312
2316
 
2313
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2317
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2314
2318
  side.active.volatiles.Reflect = true;
2315
2319
 
2316
- try options.log.start(battle.active(player), .Reflect);
2320
+ try options.log.start(.{ ident, .Reflect });
2317
2321
  }
2318
2322
 
2319
2323
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2327,14 +2331,13 @@ pub const Effects = struct {
2327
2331
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2328
2332
  } else {
2329
2333
  if (Status.any(foe_stored.status)) {
2330
- return options.log.fail(
2334
+ return options.log.fail(.{
2331
2335
  foe_ident,
2332
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2333
- );
2336
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2337
+ });
2334
2338
  }
2335
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2339
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2336
2340
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2337
- try options.chance.commit(player, .hit);
2338
2341
  }
2339
2342
 
2340
2343
  // Sleep Clause Mod
@@ -2346,19 +2349,20 @@ pub const Effects = struct {
2346
2349
  foe.active.volatiles.Recharging = false;
2347
2350
 
2348
2351
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2352
+ options.chance.observe(.sleep, player.foe(), .started);
2353
+
2349
2354
  const last = battle.side(player).last_selected_move;
2350
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2355
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2351
2356
  }
2352
2357
 
2353
2358
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2354
- try options.log.activate(battle.active(player), .Splash);
2359
+ try options.log.activate(.{ battle.active(player), .Splash });
2355
2360
  }
2356
2361
 
2357
2362
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2358
2363
  var side = battle.side(player);
2359
2364
  if (side.active.volatiles.Substitute) {
2360
- try options.log.fail(battle.active(player), .Substitute);
2361
- return;
2365
+ return options.log.fail(.{ battle.active(player), .Substitute });
2362
2366
  }
2363
2367
 
2364
2368
  assert(side.stored().stats.hp <= 1023);
@@ -2370,17 +2374,16 @@ pub const Effects = struct {
2370
2374
  // avoid having to convert to floating point)
2371
2375
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2372
2376
  if (side.stored().hp < required_hp) {
2373
- try options.log.fail(battle.active(player), .Weak);
2374
- return;
2377
+ return options.log.fail(.{ battle.active(player), .Weak });
2375
2378
  }
2376
2379
 
2377
2380
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2378
2381
  side.stored().hp -= hp;
2379
2382
  side.active.volatiles.substitute = hp + 1;
2380
2383
  side.active.volatiles.Substitute = true;
2381
- try options.log.start(battle.active(player), .Substitute);
2384
+ try options.log.start(.{ battle.active(player), .Substitute });
2382
2385
  if (hp > 0) {
2383
- try options.log.damage(battle.active(player), side.stored(), .None);
2386
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2384
2387
  if (showdown and side.stored().hp == 0) residual.* = false;
2385
2388
  }
2386
2389
  }
@@ -2389,13 +2392,11 @@ pub const Effects = struct {
2389
2392
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2390
2393
  if (showdown) {
2391
2394
  if (battle.side(player).last_selected_move == .Teleport) return;
2392
- if (try checkHit(battle, player, move, options)) {
2393
- try options.chance.commit(player, .hit);
2394
- }
2395
+ _ = try checkHit(battle, player, move, options);
2395
2396
  battle.last_damage = 0;
2396
2397
  } else {
2397
- try options.log.fail(battle.active(player), .None);
2398
- try options.log.laststill();
2398
+ try options.log.fail(.{ battle.active(player), .None });
2399
+ try options.log.laststill(.{});
2399
2400
  }
2400
2401
  }
2401
2402
 
@@ -2406,6 +2407,8 @@ pub const Effects = struct {
2406
2407
 
2407
2408
  volatiles.Thrashing = true;
2408
2409
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2410
+
2411
+ options.chance.observe(.attacking, player, .started);
2409
2412
  }
2410
2413
 
2411
2414
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2434,7 +2437,7 @@ pub const Effects = struct {
2434
2437
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2435
2438
  }
2436
2439
 
2437
- try options.log.transform(battle.active(player), foe_ident);
2440
+ try options.log.transform(.{ battle.active(player), foe_ident });
2438
2441
  }
2439
2442
 
2440
2443
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2451,6 +2454,7 @@ pub const Effects = struct {
2451
2454
 
2452
2455
  side.active.volatiles.attacks =
2453
2456
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2457
+ options.chance.observe(.binding, player, .started);
2454
2458
  }
2455
2459
 
2456
2460
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2464,87 +2468,87 @@ pub const Effects = struct {
2464
2468
  switch (move.effect) {
2465
2469
  .AttackUp1, .AttackUp2, .Rage => {
2466
2470
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2467
- if (boosts.atk == 6) return try log.fail(ident, .None);
2471
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2468
2472
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2469
2473
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2470
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2474
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2471
2475
  if (stats.atk == MAX_STAT_VALUE) {
2472
2476
  boosts.atk -= 1;
2473
2477
  if (showdown) {
2474
- try log.boost(ident, reason, n);
2475
- try log.boost(ident, Boost.Attack, -1);
2478
+ try log.boost(.{ ident, reason, n });
2479
+ try log.boost(.{ ident, .Attack, -1 });
2476
2480
  if (move.effect == .Rage) return;
2477
2481
  }
2478
- return try log.fail(ident, .None);
2482
+ return log.fail(.{ ident, .None });
2479
2483
  }
2480
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2484
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2481
2485
  const stat = unmodifiedStats(battle, side).atk;
2482
2486
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2483
- try log.boost(ident, reason, n);
2487
+ try log.boost(.{ ident, reason, n });
2484
2488
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2485
2489
  if (showdown and move.effect == .Rage) return;
2486
2490
  },
2487
2491
  .DefenseUp1, .DefenseUp2 => {
2488
2492
  assert(boosts.def >= -6 and boosts.def <= 6);
2489
- if (boosts.def == 6) return try log.fail(ident, .None);
2493
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2490
2494
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2491
2495
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2492
2496
  if (stats.def == MAX_STAT_VALUE) {
2493
2497
  boosts.def -= 1;
2494
2498
  if (showdown) {
2495
- try log.boost(ident, .Defense, n);
2496
- try log.boost(ident, .Defense, -1);
2499
+ try log.boost(.{ ident, .Defense, n });
2500
+ try log.boost(.{ ident, .Defense, -1 });
2497
2501
  }
2498
- return try log.fail(ident, .None);
2502
+ return log.fail(.{ ident, .None });
2499
2503
  }
2500
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2504
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2501
2505
  const stat = unmodifiedStats(battle, side).def;
2502
2506
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2503
- try log.boost(ident, .Defense, n);
2507
+ try log.boost(.{ ident, .Defense, n });
2504
2508
  },
2505
2509
  .SpeedUp2 => {
2506
2510
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2507
- if (boosts.spe == 6) return try log.fail(ident, .None);
2511
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2508
2512
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2509
2513
  if (stats.spe == MAX_STAT_VALUE) {
2510
2514
  boosts.spe -= 1;
2511
2515
  if (showdown) {
2512
- try log.boost(ident, .Speed, 2);
2513
- try log.boost(ident, .Speed, -1);
2516
+ try log.boost(.{ ident, .Speed, 2 });
2517
+ try log.boost(.{ ident, .Speed, -1 });
2514
2518
  }
2515
- return try log.fail(ident, .None);
2519
+ return log.fail(.{ ident, .None });
2516
2520
  }
2517
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2521
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2518
2522
  const stat = unmodifiedStats(battle, side).spe;
2519
2523
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2520
- try log.boost(ident, .Speed, 2);
2524
+ try log.boost(.{ ident, .Speed, 2 });
2521
2525
  },
2522
2526
  .SpecialUp1, .SpecialUp2 => {
2523
2527
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2524
- if (boosts.spc == 6) return try log.fail(ident, .None);
2528
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2525
2529
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2526
2530
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2527
2531
  if (stats.spc == MAX_STAT_VALUE) {
2528
2532
  boosts.spc -= 1;
2529
2533
  if (showdown) {
2530
- try log.boost(ident, .SpecialAttack, n);
2531
- try log.boost(ident, .SpecialAttack, -1);
2532
- try log.boost(ident, .SpecialDefense, n);
2533
- try log.boost(ident, .SpecialDefense, -1);
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 });
2534
2538
  }
2535
- return try log.fail(ident, .None);
2539
+ return log.fail(.{ ident, .None });
2536
2540
  }
2537
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2541
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2538
2542
  const stat = unmodifiedStats(battle, side).spc;
2539
2543
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2540
- try log.boost(ident, .SpecialAttack, n);
2541
- try log.boost(ident, .SpecialDefense, n);
2544
+ try log.boost(.{ ident, .SpecialAttack, n });
2545
+ try log.boost(.{ ident, .SpecialDefense, n });
2542
2546
  },
2543
2547
  .EvasionUp1 => {
2544
2548
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2545
- if (boosts.evasion == 6) return try log.fail(ident, .None);
2549
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2546
2550
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2547
- try log.boost(ident, .Evasion, 1);
2551
+ try log.boost(.{ ident, .Evasion, 1 });
2548
2552
  },
2549
2553
  else => unreachable,
2550
2554
  }
@@ -2559,7 +2563,7 @@ pub const Effects = struct {
2559
2563
  const foe_ident = battle.active(player.foe());
2560
2564
  const secondary = move.effect.isStatDownChance();
2561
2565
 
2562
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2566
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2563
2567
 
2564
2568
  if (secondary) {
2565
2569
  const proc = try Rolls.unboost(battle, player, options);
@@ -2567,7 +2571,6 @@ pub const Effects = struct {
2567
2571
  } else {
2568
2572
  // checkHit already checks for Invulnerable
2569
2573
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2570
- try options.chance.commit(player, .hit);
2571
2574
  }
2572
2575
 
2573
2576
  var stats = &foe.active.stats;
@@ -2577,82 +2580,82 @@ pub const Effects = struct {
2577
2580
  switch (move.effect) {
2578
2581
  .AttackDown1, .AttackDownChance => {
2579
2582
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2580
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2583
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2581
2584
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2582
2585
  if (stats.atk == 1) {
2583
2586
  boosts.atk += 1;
2584
2587
  if (showdown) {
2585
- try log.boost(foe_ident, .Attack, -1);
2586
- try log.boost(foe_ident, .Attack, 1);
2588
+ try log.boost(.{ foe_ident, .Attack, -1 });
2589
+ try log.boost(.{ foe_ident, .Attack, 1 });
2587
2590
  }
2588
- return try log.fail(foe_ident, .None);
2591
+ return log.fail(.{ foe_ident, .None });
2589
2592
  }
2590
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2593
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2591
2594
  const stat = unmodifiedStats(battle, foe).atk;
2592
2595
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2593
- try log.boost(foe_ident, .Attack, -1);
2596
+ try log.boost(.{ foe_ident, .Attack, -1 });
2594
2597
  },
2595
2598
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2596
2599
  assert(boosts.def >= -6 and boosts.def <= 6);
2597
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2600
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2598
2601
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2599
2602
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2600
2603
  if (stats.def == 1) {
2601
2604
  boosts.def += 1;
2602
2605
  if (showdown) {
2603
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2604
- try log.boost(foe_ident, .Defense, 1);
2606
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2607
+ try log.boost(.{ foe_ident, .Defense, 1 });
2605
2608
  }
2606
- return try log.fail(foe_ident, .None);
2609
+ return log.fail(.{ foe_ident, .None });
2607
2610
  }
2608
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2611
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2609
2612
  const stat = unmodifiedStats(battle, foe).def;
2610
2613
  stats.def = @max(1, stat * mod[0] / mod[1]);
2611
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2614
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2612
2615
  },
2613
2616
  .SpeedDown1, .SpeedDownChance => {
2614
2617
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2615
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2618
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2616
2619
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2617
2620
  if (stats.spe == 1) {
2618
2621
  boosts.spe += 1;
2619
2622
  if (showdown) {
2620
- try log.boost(foe_ident, .Speed, -1);
2621
- try log.boost(foe_ident, .Speed, 1);
2623
+ try log.boost(.{ foe_ident, .Speed, -1 });
2624
+ try log.boost(.{ foe_ident, .Speed, 1 });
2622
2625
  }
2623
- return try log.fail(foe_ident, .None);
2626
+ return log.fail(.{ foe_ident, .None });
2624
2627
  }
2625
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2628
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2626
2629
  const stat = unmodifiedStats(battle, foe).spe;
2627
2630
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2628
- try log.boost(foe_ident, .Speed, -1);
2631
+ try log.boost(.{ foe_ident, .Speed, -1 });
2629
2632
  assert(boosts.spe >= -6);
2630
2633
  },
2631
2634
  .SpecialDownChance => {
2632
2635
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2633
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2636
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2634
2637
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2635
2638
  if (stats.spc == 1) {
2636
2639
  boosts.spc += 1;
2637
2640
  if (showdown) {
2638
- try log.boost(foe_ident, .SpecialAttack, -1);
2639
- try log.boost(foe_ident, .SpecialAttack, 1);
2640
- try log.boost(foe_ident, .SpecialDefense, -1);
2641
- try log.boost(foe_ident, .SpecialDefense, 1);
2641
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2642
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2643
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2644
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2642
2645
  }
2643
- return try log.fail(foe_ident, .None);
2646
+ return log.fail(.{ foe_ident, .None });
2644
2647
  }
2645
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2648
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2646
2649
  const stat = unmodifiedStats(battle, foe).spc;
2647
2650
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2648
- try log.boost(foe_ident, .SpecialAttack, -1);
2649
- try log.boost(foe_ident, .SpecialDefense, -1);
2651
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2652
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2650
2653
  },
2651
2654
  .AccuracyDown1 => {
2652
2655
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2653
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2656
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2654
2657
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2655
- try log.boost(foe_ident, .Accuracy, -1);
2658
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2656
2659
  },
2657
2660
  else => unreachable,
2658
2661
  }
@@ -2676,40 +2679,40 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2676
2679
  }
2677
2680
  }
2678
2681
 
2679
- inline fn isForced(active: anytype) bool {
2682
+ fn isForced(active: anytype) bool {
2680
2683
  return active.volatiles.Recharging or active.volatiles.Rage or
2681
2684
  active.volatiles.Thrashing or active.volatiles.Charging;
2682
2685
  }
2683
2686
 
2684
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2687
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2685
2688
  var log = options.log;
2686
2689
  var side = battle.side(who);
2687
2690
  var volatiles = &side.active.volatiles;
2688
2691
  const ident = battle.active(who);
2689
2692
 
2690
- options.chance.clearDurations(who, clear);
2691
-
2692
- if (volatiles.disabled_move != 0) {
2693
- volatiles.disabled_move = 0;
2694
- volatiles.disabled_duration = 0;
2695
- try log.end(ident, .DisableSilent);
2693
+ if (volatiles.disable_move != 0) {
2694
+ volatiles.disable_move = 0;
2695
+ volatiles.disable_duration = 0;
2696
+ options.chance.observe(.disable, who, .None);
2697
+ try log.end(.{ ident, .DisableSilent });
2696
2698
  }
2697
2699
  if (volatiles.Confusion) {
2698
2700
  // volatiles.confusion is left unchanged
2699
2701
  volatiles.Confusion = false;
2700
- try log.end(ident, .ConfusionSilent);
2702
+ options.chance.observe(.confusion, who, .None);
2703
+ try log.end(.{ ident, .ConfusionSilent });
2701
2704
  }
2702
2705
  if (volatiles.Mist) {
2703
2706
  volatiles.Mist = false;
2704
- try log.end(ident, .MistSilent);
2707
+ try log.end(.{ ident, .MistSilent });
2705
2708
  }
2706
2709
  if (volatiles.FocusEnergy) {
2707
2710
  volatiles.FocusEnergy = false;
2708
- try log.end(ident, .FocusEnergySilent);
2711
+ try log.end(.{ ident, .FocusEnergySilent });
2709
2712
  }
2710
2713
  if (volatiles.LeechSeed) {
2711
2714
  volatiles.LeechSeed = false;
2712
- try log.end(ident, .LeechSeedSilent);
2715
+ try log.end(.{ ident, .LeechSeedSilent });
2713
2716
  }
2714
2717
  if (!showdown and volatiles.Toxic) {
2715
2718
  volatiles.Toxic = false;
@@ -2717,24 +2720,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2717
2720
  }
2718
2721
  if (volatiles.LightScreen) {
2719
2722
  volatiles.LightScreen = false;
2720
- try log.end(ident, .LightScreenSilent);
2723
+ try log.end(.{ ident, .LightScreenSilent });
2721
2724
  }
2722
2725
  if (volatiles.Reflect) {
2723
2726
  volatiles.Reflect = false;
2724
- try log.end(ident, .ReflectSilent);
2727
+ try log.end(.{ ident, .ReflectSilent });
2725
2728
  }
2726
2729
  if (showdown and volatiles.Toxic) {
2727
2730
  volatiles.Toxic = false;
2728
2731
  // Pokémon Showdown erroneously clears the toxic counter
2729
2732
  volatiles.toxic = 0;
2730
- try log.end(ident, .ToxicSilent);
2733
+ try log.end(.{ ident, .ToxicSilent });
2731
2734
  }
2732
2735
  }
2733
2736
 
2737
+ fn decrement(
2738
+ comptime field: chance.Action.Field,
2739
+ player: Player,
2740
+ options: anytype,
2741
+ n: anytype,
2742
+ ) @TypeOf(n) {
2743
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2744
+ .started, .ended => 0,
2745
+ .continuing => if (n > 1) n - 1 else n,
2746
+ else => unreachable,
2747
+ } else n - 1;
2748
+ }
2749
+
2734
2750
  pub const Rolls = struct {
2735
- inline fn speedTie(battle: anytype, options: anytype) !bool {
2751
+ fn speedTie(battle: anytype, options: anytype) !bool {
2736
2752
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2737
2753
  player == .P1
2754
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2755
+ battle.rng.speedTie()
2738
2756
  else if (showdown)
2739
2757
  battle.rng.range(u8, 0, 2) == 0
2740
2758
  else
@@ -2744,21 +2762,31 @@ pub const Rolls = struct {
2744
2762
  return p1;
2745
2763
  }
2746
2764
 
2747
- inline fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2765
+ fn criticalHit(
2766
+ battle: anytype,
2767
+ player: Player,
2768
+ rate: u8,
2769
+ spurious: bool,
2770
+ options: anytype,
2771
+ ) !bool {
2748
2772
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2749
2773
  val == .true
2774
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2775
+ battle.rng.criticalHit(player, rate)
2750
2776
  else if (showdown)
2751
2777
  battle.rng.chance(u8, rate, 256)
2752
2778
  else
2753
2779
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2754
2780
 
2755
- try options.chance.criticalHit(player, crit, rate);
2781
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2756
2782
  return crit;
2757
2783
  }
2758
2784
 
2759
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2785
+ fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2760
2786
  const roll = if (options.calc.overridden(player, .damage)) |val|
2761
2787
  val
2788
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2789
+ battle.rng.damage(player)
2762
2790
  else roll: {
2763
2791
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2764
2792
  while (true) {
@@ -2772,21 +2800,27 @@ pub const Rolls = struct {
2772
2800
  return roll;
2773
2801
  }
2774
2802
 
2775
- inline fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2803
+ const uN = if (pkmn.options.miss) u8 else u9;
2804
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2805
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2776
2806
  const ok = if (options.calc.overridden(player, .hit)) |val|
2777
2807
  val == .true
2808
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2809
+ battle.rng.hit(player, acc)
2778
2810
  else if (showdown)
2779
- battle.rng.chance(u8, accuracy, 256)
2811
+ battle.rng.chance(uN, acc, 256)
2780
2812
  else
2781
- battle.rng.next() < accuracy;
2813
+ battle.rng.next() < acc;
2782
2814
 
2783
- options.chance.hit(ok, accuracy);
2815
+ try options.chance.hit(player, ok, acc);
2784
2816
  return ok;
2785
2817
  }
2786
2818
 
2787
- inline fn confused(battle: anytype, player: Player, options: anytype) !bool {
2819
+ fn confused(battle: anytype, player: Player, options: anytype) !bool {
2788
2820
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2789
2821
  val == .true
2822
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2823
+ battle.rng.confused(player)
2790
2824
  else if (showdown)
2791
2825
  !battle.rng.chance(u8, 128, 256)
2792
2826
  else
@@ -2796,9 +2830,11 @@ pub const Rolls = struct {
2796
2830
  return cfz;
2797
2831
  }
2798
2832
 
2799
- inline fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2833
+ fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2800
2834
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2801
2835
  val == .true
2836
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2837
+ battle.rng.paralyzed(player)
2802
2838
  else if (showdown)
2803
2839
  battle.rng.chance(u8, 63, 256)
2804
2840
  else
@@ -2808,9 +2844,11 @@ pub const Rolls = struct {
2808
2844
  return par;
2809
2845
  }
2810
2846
 
2811
- inline fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2847
+ fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2812
2848
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2813
2849
  val == .true
2850
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2851
+ battle.rng.secondaryChance(player, 25)
2814
2852
  else if (showdown)
2815
2853
  battle.rng.chance(u8, 25, 256)
2816
2854
  else
@@ -2820,11 +2858,13 @@ pub const Rolls = struct {
2820
2858
  return proc;
2821
2859
  }
2822
2860
 
2823
- inline fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2861
+ fn secondaryChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2824
2862
  const rate: u8 = if (low) 26 else 77;
2825
2863
 
2826
2864
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2827
2865
  val == .true
2866
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2867
+ battle.rng.secondaryChance(player, rate)
2828
2868
  else if (showdown)
2829
2869
  battle.rng.chance(u8, rate, 256)
2830
2870
  else
@@ -2834,11 +2874,13 @@ pub const Rolls = struct {
2834
2874
  return proc;
2835
2875
  }
2836
2876
 
2837
- inline fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2877
+ fn poisonChance(battle: anytype, player: Player, low: bool, options: anytype) !bool {
2838
2878
  const rate: u8 = if (low) 52 else 103;
2839
2879
 
2840
2880
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2841
2881
  val == .true
2882
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2883
+ battle.rng.secondaryChance(player, rate)
2842
2884
  else if (showdown)
2843
2885
  battle.rng.chance(u8, rate, 256)
2844
2886
  else
@@ -2848,9 +2890,11 @@ pub const Rolls = struct {
2848
2890
  return proc;
2849
2891
  }
2850
2892
 
2851
- inline fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2893
+ fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2852
2894
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2853
2895
  val == .true
2896
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2897
+ battle.rng.secondaryChance(player, 85)
2854
2898
  else if (showdown)
2855
2899
  battle.rng.chance(u8, 85, 256)
2856
2900
  else
@@ -2860,14 +2904,14 @@ pub const Rolls = struct {
2860
2904
  return proc;
2861
2905
  }
2862
2906
 
2863
- inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2907
+ fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2864
2908
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2865
2909
  val
2866
- else if (showdown) move: {
2867
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2868
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2869
- break :move @enumFromInt(r + mod);
2870
- } else move: {
2910
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2911
+ battle.rng.metronome(player)
2912
+ else if (showdown)
2913
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2914
+ else move: {
2871
2915
  while (true) {
2872
2916
  const r = battle.rng.next();
2873
2917
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2880,9 +2924,11 @@ pub const Rolls = struct {
2880
2924
  return move;
2881
2925
  }
2882
2926
 
2883
- inline fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2927
+ fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2884
2928
  const power = if (options.calc.overridden(player, .psywave)) |val|
2885
2929
  val - 1
2930
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2931
+ battle.rng.psywave(player)
2886
2932
  else if (showdown)
2887
2933
  battle.rng.range(u8, 0, max)
2888
2934
  else power: {
@@ -2897,11 +2943,13 @@ pub const Rolls = struct {
2897
2943
  return power;
2898
2944
  }
2899
2945
 
2900
- inline fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2946
+ fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2901
2947
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2902
2948
  @intCast(val)
2949
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2950
+ battle.rng.sleepDuration(player)
2903
2951
  else if (showdown)
2904
- @intCast(battle.rng.range(u8, 1, 8))
2952
+ battle.rng.range(u3, 1, 8)
2905
2953
  else duration: {
2906
2954
  while (true) {
2907
2955
  const r = battle.rng.next() & 7;
@@ -2910,50 +2958,58 @@ pub const Rolls = struct {
2910
2958
  };
2911
2959
 
2912
2960
  assert(duration >= 1 and duration <= 7);
2913
- options.chance.duration(.sleep, player, player.foe(), duration);
2961
+ options.chance.duration(player, duration);
2914
2962
  return duration;
2915
2963
  }
2916
2964
 
2917
- inline fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2965
+ fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2918
2966
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2919
2967
  val
2968
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2969
+ battle.rng.disableDuration(player)
2920
2970
  else if (showdown)
2921
- @intCast(battle.rng.range(u8, 1, 9))
2971
+ battle.rng.range(u4, 1, 9)
2922
2972
  else
2923
2973
  @intCast((battle.rng.next() & 7) + 1);
2924
2974
 
2925
2975
  assert(duration >= 1 and duration <= 8);
2926
- options.chance.duration(.disable, player, player.foe(), duration);
2976
+ options.chance.duration(player, duration);
2927
2977
  return duration;
2928
2978
  }
2929
2979
 
2930
- inline fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2980
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2931
2981
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2982
  @intCast(val)
2983
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2984
+ battle.rng.confusionDuration(player)
2933
2985
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2986
+ battle.rng.range(u3, 2, 6)
2935
2987
  else
2936
2988
  @intCast((battle.rng.next() & 3) + 2);
2937
2989
 
2938
2990
  assert(duration >= 2 and duration <= 5);
2939
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2991
+ options.chance.duration(player, duration);
2940
2992
  return duration;
2941
2993
  }
2942
2994
 
2943
- inline fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2995
+ fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2944
2996
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2945
2997
  @intCast(val)
2998
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2999
+ battle.rng.attackingDuration(player)
2946
3000
  else if (showdown)
2947
- @intCast(battle.rng.range(u4, 2, 4))
3001
+ battle.rng.range(u3, 2, 4)
2948
3002
  else
2949
3003
  @intCast((battle.rng.next() & 1) + 2);
2950
3004
 
2951
3005
  assert(duration >= 2 and duration <= 3);
2952
- options.chance.duration(.attacking, player, player, duration);
3006
+ options.chance.duration(player, duration);
2953
3007
  return duration;
2954
3008
  }
2955
3009
 
2956
- inline fn distribution(
3010
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
3011
+
3012
+ fn distribution(
2957
3013
  battle: anytype,
2958
3014
  comptime effect: Move.Effect,
2959
3015
  player: Player,
@@ -2962,8 +3018,10 @@ pub const Rolls = struct {
2962
3018
  const roll = if (effect == .Binding) .duration else .multi_hit;
2963
3019
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2964
3020
  @intCast(val)
3021
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3022
+ battle.rng.distribution(player)
2965
3023
  else if (showdown)
2966
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
3024
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2967
3025
  else n: {
2968
3026
  const r = (battle.rng.next() & 3);
2969
3027
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -2971,7 +3029,7 @@ pub const Rolls = struct {
2971
3029
 
2972
3030
  assert(n >= 2 and n <= 5);
2973
3031
  if (effect == .Binding) {
2974
- options.chance.duration(.binding, player, player, n);
3032
+ options.chance.duration(player, n);
2975
3033
  } else {
2976
3034
  try options.chance.multiHit(player, n);
2977
3035
  }
@@ -2994,7 +3052,9 @@ pub const Rolls = struct {
2994
3052
  else
2995
3053
  null;
2996
3054
 
2997
- const slot: u4 = overridden orelse slot: {
3055
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3056
+ battle.rng.moveSlot(player, moves, check_pp)
3057
+ else slot: {
2998
3058
  if (showdown) {
2999
3059
  if (check_pp == 0) {
3000
3060
  var i: usize = moves.len;
@@ -3010,10 +3070,9 @@ pub const Rolls = struct {
3010
3070
  while (i < moves.len and r > 0) : (i += 1) {
3011
3071
  if (moves[i].pp > 0) {
3012
3072
  r -= 1;
3013
- if (r == 0) break;
3073
+ if (r == 0) break :slot @intCast(i + 1);
3014
3074
  }
3015
3075
  }
3016
- break :slot @intCast(i + 1);
3017
3076
  }
3018
3077
  }
3019
3078
 
@@ -3075,11 +3134,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3075
3134
  const side = battle.side(player);
3076
3135
  const foe = battle.foe(player);
3077
3136
 
3078
- var active = &side.active;
3137
+ const active = &side.active;
3079
3138
  const stored = side.stored();
3080
3139
 
3081
3140
  // While players are not given any input options on the cartridge in these cases,
3082
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3141
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3083
3142
  //
3084
3143
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3085
3144
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3087,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3087
3146
  // passing due to being forced into using a move. Instead of introducing another option
3088
3147
  // we simply repurpose Move with no move slot, even though pedantically this is not
3089
3148
  // strictly correct as the player would not have been presented the option to move or
3090
- // switch at all.
3149
+ // switch at all
3091
3150
  if (isForced(active)) {
3092
3151
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3093
3152
  n += 1;
@@ -3106,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3106
3165
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3107
3166
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3108
3167
  // either disables all other moves in the case of limited or requires you to select a
3109
- // move normally if sleeping/frozen/bound.
3168
+ // move normally if sleeping/frozen/bound
3110
3169
  if (!showdown and (limited or foe.active.volatiles.Binding or
3111
3170
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3112
3171
  {
@@ -3119,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3119
3178
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3120
3179
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3121
3180
  // Move will not result in forcing the subsequent use unless the user also had the
3122
- // proc-ed move in their moveset) and disabling all other moves.
3181
+ // proc-ed move in their moveset) and disabling all other moves
3123
3182
  if (limited) {
3124
3183
  assert(showdown);
3125
3184
  assert(side.last_selected_move != .None);
@@ -3129,7 +3188,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3129
3188
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3130
3189
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3131
3190
  const struggle =
3132
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3191
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3133
3192
  const s = if (struggle) 0 else slot;
3134
3193
  out[n] = .{ .type = .Move, .data = s };
3135
3194
  n += 1;
@@ -3144,7 +3203,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3144
3203
  const m = active.moves[slot - 1];
3145
3204
  if (m.id == .None) break;
3146
3205
  if (m.pp == 0) continue;
3147
- if (active.volatiles.disabled_move == slot) continue;
3206
+ if (active.volatiles.disable_move == slot) continue;
3148
3207
  out[n] = .{ .type = .Move, .data = slot };
3149
3208
  n += 1;
3150
3209
  }