@pkmn/engine 0.1.0-dev.dfd46bbb → 0.1.0-dev.e0a786f8

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 (83) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +51 -23
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +27 -5
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +42 -91
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +53 -76
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +330 -93
  39. package/package.json +59 -132
  40. package/src/bin/install-pkmn-engine +31 -20
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +255 -0
  43. package/src/lib/{binding → bindings}/node.zig +8 -21
  44. package/src/lib/bindings/wasm.zig +59 -0
  45. package/src/lib/common/array.zig +77 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +39 -8
  48. package/src/lib/common/js.zig +4 -2
  49. package/src/lib/common/optional.zig +14 -10
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +396 -399
  52. package/src/lib/common/rational.zig +327 -158
  53. package/src/lib/common/rng.zig +7 -9
  54. package/src/lib/common/util.zig +60 -1
  55. package/src/lib/gen1/calc.zig +696 -160
  56. package/src/lib/gen1/chance.zig +405 -280
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +2 -4
  59. package/src/lib/gen1/data/types.zig +1 -2
  60. package/src/lib/gen1/data.zig +24 -27
  61. package/src/lib/gen1/helpers.zig +92 -548
  62. package/src/lib/gen1/mechanics.zig +575 -461
  63. package/src/lib/gen2/calc.zig +37 -27
  64. package/src/lib/gen2/chance.zig +84 -496
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +1 -2
  69. package/src/lib/gen2/data.zig +20 -23
  70. package/src/lib/gen2/generated.zig +24 -37
  71. package/src/lib/gen2/helpers.zig +46 -25
  72. package/src/lib/gen2/mechanics.zig +721 -453
  73. package/src/lib/node.zig +18 -0
  74. package/src/lib/pkmn.zig +37 -15
  75. package/src/lib/wasm.zig +17 -0
  76. package/src/tools/display/dom.ts +93 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -400
  79. package/build/test/display.js.map +0 -1
  80. package/src/lib/binding/wasm.zig +0 -12
  81. package/src/lib/gen1/README.md +0 -894
  82. package/src/lib/gen2/README.md +0 -330
  83. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,36 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
-
10
- const data = @import("data.zig");
10
+ const std = @import("std");
11
11
 
12
12
  const assert = std.debug.assert;
13
-
14
- const expectEqual = std.testing.expectEqual;
15
-
16
- const showdown = pkmn.options.showdown;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
24
15
  const Damage = protocol.Damage;
25
- const Heal = protocol.Heal;
26
-
27
- const Gen12 = rng.Gen12;
28
-
29
- const ActivePokemon = data.ActivePokemon;
30
16
  const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
18
+ const Fail = protocol.Fail;
19
+ const Gen12 = rng.Gen12;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
31
22
  const Move = data.Move;
32
23
  const MoveSlot = data.MoveSlot;
33
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
34
27
  const Side = data.Side;
35
28
  const Species = data.Species;
36
29
  const Stats = data.Stats;
@@ -70,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
70
63
  var p1 = battle.side(.P1);
71
64
  var p2 = battle.side(.P2);
72
65
 
73
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
74
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
66
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
67
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
75
68
 
76
69
  if (try turnOrder(battle, c1, c2, options) == .P1) {
77
70
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -89,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
89
82
  const p1 = battle.side(.P1);
90
83
  const p2 = battle.side(.P2);
91
84
 
92
- var p1_slot = findFirstAlive(p1);
85
+ const p1_slot = findFirstAlive(p1);
93
86
  assert(!showdown or p1_slot == 1);
94
87
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
95
88
 
96
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
97
90
  assert(!showdown or p2_slot == 1);
98
91
  if (p2_slot == 0) return Result.Win;
99
92
 
@@ -165,7 +158,7 @@ fn selectMove(
165
158
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
166
159
  // set the selected move will not actually be used, it will just be reported as having
167
160
  // been used (this differs from how Pokémon Showdown works, but its impossible to
168
- // replicate the incorrect behavior with the correct mechanisms).
161
+ // replicate the incorrect behavior with the correct mechanisms)
169
162
  saveMove(battle, player, choice);
170
163
  } else {
171
164
  assert(choice.data == 0);
@@ -173,7 +166,10 @@ fn selectMove(
173
166
  if (foe_choice.type == .Switch) {
174
167
  const last = side.active.move(battle.lastMove(player).index);
175
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
176
- if (last.id == .MirrorMove) return Result.Error;
169
+ if (last.id == .MirrorMove) {
170
+ if (!pkmn.options.mod) return Result.Error;
171
+ side.last_selected_move = last.id;
172
+ }
177
173
  }
178
174
  }
179
175
  return null;
@@ -252,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
252
248
  active.moves = incoming.moves;
253
249
 
254
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
255
252
 
256
253
  foe.active.volatiles.Binding = false;
257
254
 
258
- try options.log.switched(battle.active(player), incoming);
259
- options.chance.switched(player, side.order[0], out);
255
+ try options.log.switched(.{ battle.active(player), incoming });
260
256
 
261
257
  if (showdown and incoming.status == Status.TOX) {
262
258
  incoming.status = Status.init(.PSN);
263
259
  // Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
264
260
  // rather not clutter up turnOrder just for this (incorrect) log message
265
- try options.log.status(battle.active(player), incoming.status, .Silent);
261
+ try options.log.status(.{ battle.active(player), incoming.status, .Silent });
266
262
  }
267
263
  }
268
264
 
@@ -346,13 +342,13 @@ fn doTurn(
346
342
  )) |r| return r;
347
343
  if (!replace) {
348
344
  if (player_choice.type != .Switch) {
349
- if (try checkFaint(battle, foe_player, options)) |r| return r;
345
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
350
346
  }
351
347
  if (residual) try handleResidual(battle, player, options);
352
- if (try checkFaint(battle, player, options)) |r| return r;
348
+ if (try checkFaint(battle, player, false, options)) |r| return r;
353
349
  } else if (foe_choice.type == .Pass) return null;
354
350
 
355
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
356
352
 
357
353
  residual = true;
358
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -368,10 +364,10 @@ fn doTurn(
368
364
  )) |r| return r;
369
365
  if (!replace) {
370
366
  if (!calc and foe_choice.type != .Switch) {
371
- if (try checkFaint(battle, player, options)) |r| return r;
367
+ if (try checkFaint(battle, player, true, options)) |r| return r;
372
368
  }
373
369
  if (residual) try handleResidual(battle, foe_player, options);
374
- if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
375
371
  }
376
372
 
377
373
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -406,7 +402,7 @@ fn executeMove(
406
402
  if (side.last_selected_move == .SKIP_TURN) {
407
403
  assert(!showdown);
408
404
  if (battle.foe(player).active.volatiles.Binding) {
409
- try options.log.cant(battle.active(player), .Bound);
405
+ try options.log.cant(.{ battle.active(player), .Bound });
410
406
  }
411
407
  return null;
412
408
  }
@@ -424,13 +420,13 @@ fn executeMove(
424
420
  mslot = @intCast(battle.lastMove(player).index);
425
421
  const stored = side.stored();
426
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
427
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
428
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
429
425
  side.last_selected_move == .HyperBeam or
430
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
431
427
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
432
428
  // thawed, in which case we will desync unless the last_selected_move happened to be at
433
- // index 1 and the current Pokémon has the same move in its first slot.
429
+ // index 1 and the current Pokémon has the same move in its first slot
434
430
  if (!auto) {
435
431
  // side.active.moves(slot) is safe to check even though the slot in question might not
436
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -457,6 +453,25 @@ fn executeMove(
457
453
  .err => return @as(?Result, Result.Error),
458
454
  }
459
455
 
456
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
457
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
458
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
459
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
460
+ side.last_selected_move != side.active.move(mslot).id)
461
+ {
462
+ assert(mslot == 1);
463
+ const has_beam = has_beam: {
464
+ for (side.active.moves, 0..) |m, i| {
465
+ if (m.id == .HyperBeam) {
466
+ mslot = @intCast(i + 1);
467
+ break :has_beam true;
468
+ }
469
+ }
470
+ break :has_beam false;
471
+ };
472
+ if (!has_beam) skip_pp = true;
473
+ }
474
+
460
475
  const can = skip_can or
461
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
477
  if (!can) return null;
@@ -483,36 +498,41 @@ fn beforeMove(
483
498
 
484
499
  if (Status.is(stored.status, .SLP)) {
485
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
486
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
487
- if (options.calc.modify(player, .sleep)) |extend| {
488
- if (!extend) stored.status = 0;
503
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
504
+ .started, .ended => stored.status = 0,
505
+ .continuing => if (Status.duration(stored.status) > 1) {
506
+ stored.status -= 1;
507
+ },
508
+ else => unreachable,
489
509
  } else {
490
510
  stored.status -= 1;
491
511
  }
492
512
 
493
513
  const duration = Status.duration(stored.status);
494
- if (!Status.is(stored.status, .EXT)) {
495
- try options.chance.sleep(player, duration);
496
- }
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
497
517
 
498
518
  if (duration == 0) {
499
- try log.curestatus(ident, before, .Message);
519
+ try log.curestatus(.{ ident, before, .Message });
500
520
  stored.status = 0; // clears EXT if present
501
521
  } else {
502
- try log.cant(ident, .Sleep);
522
+ try log.cant(.{ ident, .Sleep });
503
523
  }
504
524
  side.last_used_move = .None;
505
525
  return .done;
506
526
  }
507
527
 
508
528
  if (Status.is(stored.status, .FRZ)) {
509
- try log.cant(ident, .Freeze);
529
+ try log.cant(.{ ident, .Freeze });
510
530
  side.last_used_move = .None;
511
531
  return .done;
512
532
  }
513
533
 
514
534
  if (skip or foe.active.volatiles.Binding) {
515
- try log.cant(ident, .Bound);
535
+ try log.cant(.{ ident, .Bound });
516
536
  return .done;
517
537
  }
518
538
 
@@ -520,27 +540,27 @@ fn beforeMove(
520
540
  // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
521
541
  // Pokémon can sometimes flinch multiple times from the same original hit
522
542
  if (!showdown) volatiles.Flinch = false;
523
- try log.cant(ident, .Flinch);
543
+ try log.cant(.{ ident, .Flinch });
524
544
  return .done;
525
545
  }
526
546
 
527
547
  if (volatiles.Recharging) {
528
548
  volatiles.Recharging = false;
529
- try log.cant(ident, .Recharge);
549
+ try log.cant(.{ ident, .Recharge });
530
550
  return .done;
531
551
  }
532
552
 
533
553
  if (volatiles.disable_duration > 0) {
534
- if (options.calc.modify(player, .disable)) |extend| {
535
- if (!extend) volatiles.disable_duration = 0;
536
- } else {
537
- volatiles.disable_duration -= 1;
538
- }
539
- try options.chance.disable(player, volatiles.disable_duration);
554
+ volatiles.disable_duration =
555
+ decrement(.disable, player, options, volatiles.disable_duration);
556
+ try options.chance.disable(
557
+ player,
558
+ if (volatiles.disable_duration == 0) .ended else .continuing,
559
+ );
540
560
 
541
561
  if (volatiles.disable_duration == 0) {
542
562
  volatiles.disable_move = 0;
543
- try log.end(ident, .Disable);
563
+ try log.end(.{ ident, .Disable });
544
564
  }
545
565
  }
546
566
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
@@ -549,25 +569,30 @@ fn beforeMove(
549
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
550
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
551
571
  if (side.last_selected_move == .None) {
552
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
553
573
  return .err;
554
574
  }
555
575
 
556
576
  if (volatiles.Confusion) {
557
577
  assert(volatiles.confusion > 0);
558
-
559
- if (options.calc.modify(player, .confusion)) |extend| {
560
- if (!extend) volatiles.confusion = 0;
578
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
579
+ .started, .ended => volatiles.confusion = 0,
580
+ .continuing => if (volatiles.confusion > 1) {
581
+ volatiles.confusion -= 1;
582
+ },
583
+ .overwritten => {},
584
+ else => unreachable,
561
585
  } else {
562
586
  volatiles.confusion -= 1;
563
587
  }
564
- try options.chance.confusion(player, volatiles.confusion);
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
565
590
 
566
591
  if (volatiles.confusion == 0) {
567
592
  volatiles.Confusion = false;
568
- try log.end(ident, .Confusion);
593
+ try log.end(.{ ident, .Confusion });
569
594
  } else {
570
- try log.activate(ident, .Confusion);
595
+ try log.activate(.{ ident, .Confusion });
571
596
 
572
597
  if (try Rolls.confused(battle, player, options)) {
573
598
  assert(!volatiles.MultiHit);
@@ -579,7 +604,9 @@ fn beforeMove(
579
604
  volatiles.Charging = false;
580
605
  volatiles.Binding = false;
581
606
  volatiles.Invulnerable = false;
582
- options.chance.clearDurations(player, null);
607
+
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
583
610
  {
584
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
585
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -587,19 +614,14 @@ fn beforeMove(
587
614
  const def = foe.active.stats.def;
588
615
  foe.active.stats.def = active.stats.def;
589
616
  defer foe.active.stats.def = def;
590
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
591
620
  }
592
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
593
- // on the targeting behavior of the confused Pokémon's selected move which results
594
- // in the wrong behavior with respect to the Substitute + Confusion glitch
595
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
596
- player
597
- else
598
- player.foe();
599
621
 
600
622
  const uncapped = battle.last_damage;
601
623
  // Skipping adjustDamage / randomizeDamage / checkHit
602
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
603
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
604
626
  if (showdown) battle.last_damage = uncapped;
605
627
 
@@ -616,9 +638,12 @@ fn beforeMove(
616
638
  volatiles.Thrashing = false;
617
639
  volatiles.Charging = false;
618
640
  volatiles.Binding = false;
619
- options.chance.clearDurations(player, null);
641
+
642
+ options.chance.observe(.attacking, player, .None);
643
+ options.chance.observe(.binding, player, .None);
644
+
620
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
621
- try log.cant(ident, .Paralysis);
646
+ try log.cant(.{ ident, .Paralysis });
622
647
  return .done;
623
648
  }
624
649
 
@@ -634,28 +659,22 @@ fn beforeMove(
634
659
  volatiles.state +%= battle.last_damage;
635
660
  }
636
661
 
637
- assert(volatiles.attacks > 0);
638
-
639
- if (options.calc.modify(player, .attacking)) |extend| {
640
- if (!extend) volatiles.attacks = 0;
641
- } else {
642
- volatiles.attacks -= 1;
643
- }
644
- try options.chance.attacking(player, volatiles.attacks);
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
645
664
 
646
665
  if (volatiles.attacks != 0) {
647
- try log.activate(ident, .Bide);
666
+ try log.activate(.{ ident, .Bide });
648
667
  return .done;
649
668
  }
650
669
 
651
670
  volatiles.Bide = false;
652
- try log.end(ident, .Bide);
671
+ try log.end(.{ ident, .Bide });
653
672
 
654
673
  battle.last_damage = volatiles.state *% 2;
655
674
  volatiles.state = 0;
656
675
 
657
676
  if (battle.last_damage == 0) {
658
- try log.fail(ident, .None);
677
+ try log.fail(.{ ident, .None });
659
678
  return .done;
660
679
  }
661
680
 
@@ -675,41 +694,42 @@ fn beforeMove(
675
694
  }
676
695
 
677
696
  if (volatiles.Thrashing) {
678
- assert(volatiles.attacks > 0);
697
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
679
698
 
680
- if (options.calc.modify(player, .attacking)) |extend| {
681
- if (!extend) volatiles.attacks = 0;
682
- } else {
683
- volatiles.attacks -= 1;
684
- }
685
- try options.chance.attacking(player, volatiles.attacks);
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
686
701
 
687
- if (!showdown and handleThrashing(battle, active, player, options)) {
688
- try log.start(battle.active(player), .ConfusionSilent);
689
- }
690
- try log.move(ident, side.last_selected_move, battle.active(player.foe()));
691
- if (showdown) {
692
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
693
- // used in useMove and doesn't have the notion of skipping canMove semantics
694
- side.last_used_move = side.last_selected_move;
695
- if (handleThrashing(battle, active, player, options)) {
696
- try log.start(battle.active(player), .ConfusionSilent);
697
- }
702
+ if (volatiles.attacks == 0) {
703
+ const overwritten = volatiles.Confusion;
704
+
705
+ volatiles.Thrashing = false;
706
+ volatiles.Confusion = true;
707
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
708
+
709
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
710
+ // ended (the engine's protocol is always from the omniscient perspective and clients
711
+ // can be expected to recognize this pattern and pass the information only to the
712
+ // correct player)
713
+ try log.start(.{ battle.active(player), .ConfusionSilent });
714
+ options.chance.observe(.confusion, player, if (overwritten)
715
+ if (pkmn.options.overwrite) .overwritten else .continuing
716
+ else
717
+ .started);
718
+ if (!overwritten) options.calc.confusion(player);
698
719
  }
720
+
721
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
722
+ // used in useMove and doesn't have the notion of skipping canMove semantics
723
+ if (showdown) side.last_used_move = side.last_selected_move;
724
+
699
725
  return .skip_can;
700
726
  }
701
727
 
702
728
  if (volatiles.Binding) {
703
- assert(volatiles.attacks > 0);
729
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
704
731
 
705
- if (options.calc.modify(player, .binding)) |extend| {
706
- if (!extend) volatiles.attacks = 0;
707
- } else {
708
- volatiles.attacks -= 1;
709
- }
710
- try options.chance.binding(player, volatiles.attacks);
711
-
712
- try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
732
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
713
733
  if (showdown or battle.last_damage != 0) {
714
734
  const sub = showdown and foe.active.volatiles.Substitute;
715
735
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -737,22 +757,21 @@ fn canMove(
737
757
  const player_ident = battle.active(player);
738
758
  const move = Move.get(side.last_selected_move);
739
759
 
740
- var skip = skip_pp;
741
760
  if (side.active.volatiles.Charging) {
742
761
  side.active.volatiles.Charging = false;
743
762
  side.active.volatiles.Invulnerable = false;
744
763
  } else if (move.effect == .Charge) {
745
- try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
764
+ try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
746
765
  setCounterable(battle, player, side, move);
747
766
  try Effects.charge(battle, player, options);
748
767
  return false;
749
768
  }
750
769
 
751
770
  side.last_used_move = side.last_selected_move;
752
- if (!skip) decrementPP(side, mslot, auto);
771
+ if (!skip_pp) decrementPP(side, mslot, auto);
753
772
 
754
773
  const target = if (move.target == .Self) player else player.foe();
755
- try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
774
+ try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
756
775
  setCounterable(battle, player, side, move);
757
776
 
758
777
  if (move.effect.onBegin()) {
@@ -762,9 +781,14 @@ fn canMove(
762
781
 
763
782
  if (move.effect == .Thrashing) {
764
783
  Effects.thrashing(battle, player, options);
765
- } else if (!showdown and move.effect == .Binding) {
766
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
767
- try Effects.binding(battle, player, false, options);
784
+ } else if (move.effect == .Binding) {
785
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
786
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
787
+ if (showdown) {
788
+ battle.foe(player).active.volatiles.Recharging = false;
789
+ } else {
790
+ try Effects.binding(battle, player, false, options);
791
+ }
768
792
  }
769
793
 
770
794
  return true;
@@ -801,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
801
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
802
826
  }
803
827
 
804
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
805
829
  var active = &side.active;
806
830
  const volatiles = &active.volatiles;
807
831
 
@@ -809,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
809
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
810
834
  if (showdown and volatiles.Transform) return;
811
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
812
839
  assert(mslot > 0 and mslot <= 4);
813
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
840
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
814
841
  }
815
842
 
816
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -858,13 +885,13 @@ fn doMove(
858
885
  break :miss true;
859
886
  }
860
887
  break :miss (move.target != .Self and
861
- !moveHit(battle, player, move, &immune, &mist, options));
888
+ !try moveHit(battle, player, move, &immune, &mist, options));
862
889
  };
863
890
  assert(!immune or miss or (showdown and move.effect == .Binding));
864
891
 
865
892
  var late = showdown and move.effect != .Explode;
866
893
  const skip = status or immune;
867
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
868
895
  if (showdown and move.effect.isMulti()) {
869
896
  try Effects.multiHit(battle, player, move, options);
870
897
  hits = side.active.volatiles.attacks;
@@ -886,8 +913,9 @@ fn doMove(
886
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
887
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
888
915
  battle.last_damage = if (ohko) 65535 else 0;
889
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
890
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
916
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
917
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
918
+ if (!showdown) try options.chance.commit(player, .err);
891
919
  return @as(?Result, Result.Error);
892
920
  }
893
921
  if (battle.last_damage == 0) {
@@ -901,11 +929,12 @@ fn doMove(
901
929
  }
902
930
  }
903
931
 
904
- var zero = battle.last_damage == 0;
905
- miss = if (showdown or skip)
906
- miss
907
- else
908
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
932
+ var missed: bool = undefined;
933
+ const zero = battle.last_damage == 0;
934
+ if (!showdown and !skip) {
935
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
936
+ miss = missed or zero;
937
+ }
909
938
 
910
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
911
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -929,16 +958,37 @@ fn doMove(
929
958
  ohko = (!showdown or (!immune and !invulnerable)) and
930
959
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
960
  if (ohko) {
932
- try log.immune(foe_ident, .OHKO);
961
+ try log.immune(.{ foe_ident, .OHKO });
933
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
934
- try log.immune(foe_ident, .None);
963
+ try log.immune(.{ foe_ident, .None });
964
+ if (!showdown) {
965
+ if (move.effect == .Binding) {
966
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
967
+ // then simply clears it later on if the move happens to miss in moveHit.
968
+ // However, moveHit can return true and we'll still end up in the outer miss
969
+ // handling block because attacking into an immune Pokemon does zero damage
970
+ // (considered missing)
971
+ assert(zero);
972
+ // Thus we still need to actually save the hit result (but not the crit roll).
973
+ // Using .miss for this commit would accomplish what we want, but is somewhat
974
+ // misleading as its entirely possible the move actually did hit (and the target
975
+ // is now trapped) so we use the .binding value instead which is effectively
976
+ // identical, just doesn't claim to know about the actual results of the hit
977
+ // roll)
978
+ try options.chance.commit(player, .binding);
979
+ } else {
980
+ // The .glitch option allows for conditionally committing if we are in a
981
+ // division-by-zero freeze alternative timeline scenario
982
+ try options.chance.commit(player, .glitch);
983
+ }
984
+ }
935
985
  } else if (mist) {
936
- if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
937
- try log.fail(foe_ident, .None);
986
+ if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
987
+ try log.fail(.{ foe_ident, .None });
938
988
  } else {
939
- if (showdown or !zero) try options.chance.commit(player, .miss);
940
- try log.lastmiss();
941
- try log.miss(battle.active(player));
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
990
+ try log.lastmiss(.{});
991
+ try log.miss(.{battle.active(player)});
942
992
  }
943
993
  if (move.effect == .JumpKick) {
944
994
  // Recoil is supposed to be damage/8 but damage will always be 0 here
@@ -951,17 +1001,15 @@ fn doMove(
951
1001
  try buildRage(battle, player.foe(), options);
952
1002
  } else if (showdown and move.effect == .Disable) {
953
1003
  try buildRage(battle, player.foe(), options);
954
- } else if (!showdown and immune and move.effect == .Binding) {
955
- try options.chance.commit(player, .binding);
956
1004
  }
957
1005
  return null;
958
- } else {
1006
+ } else if (!showdown) {
959
1007
  try options.chance.commit(player, .hit);
960
1008
  }
961
1009
 
962
1010
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
963
1011
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
964
- // it up above before damage calculation).
1012
+ // it up above before damage calculation)
965
1013
  if (!showdown and move.effect.isMulti()) {
966
1014
  try Effects.multiHit(battle, player, move, options);
967
1015
  hits = side.active.volatiles.attacks;
@@ -974,15 +1022,15 @@ fn doMove(
974
1022
  var hit: u4 = 0;
975
1023
  while (hit < hits) {
976
1024
  if (hit == 0) {
977
- if (crit) try log.crit(battle.active(player.foe()));
1025
+ if (crit) try log.crit(.{battle.active(player.foe())});
978
1026
  if (effectiveness > Effectiveness.neutral) {
979
- try log.supereffective(battle.active(player.foe()));
1027
+ try log.supereffective(.{battle.active(player.foe())});
980
1028
  } else if (effectiveness < Effectiveness.neutral) {
981
- try log.resisted(battle.active(player.foe()));
1029
+ try log.resisted(.{battle.active(player.foe())});
982
1030
  }
983
1031
  }
984
1032
  if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
985
- if (hit == 0 and ohko) try log.ohko();
1033
+ if (hit == 0 and ohko) try log.ohko(.{});
986
1034
  hit += 1;
987
1035
  if (foe.stored().hp == 0) break;
988
1036
  if (!late and (!sub or move.effect == .Explode)) {
@@ -999,33 +1047,31 @@ fn doMove(
999
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1000
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1001
1049
  }
1002
- try log.hitcount(battle.active(player.foe()), hit);
1003
- } else if (showdown) {
1050
+ try log.hitcount(.{ battle.active(player.foe()), hit });
1051
+ } else if (showdown and move.effect == .Binding) {
1004
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1005
- if (move.effect == .Binding) {
1006
- try Effects.binding(battle, player, rewrap, options);
1007
- if (immune) {
1008
- battle.last_damage = 0;
1009
- assert(foe.stored().hp > 0);
1010
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1011
- if (sub) {
1012
- try log.activate(battle.active(player.foe()), .Substitute);
1013
- } else {
1014
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1015
- try buildRage(battle, player.foe(), options);
1016
- }
1017
- return null;
1053
+ try Effects.binding(battle, player, rewrap, options);
1054
+ if (immune) {
1055
+ battle.last_damage = 0;
1056
+ assert(foe.stored().hp > 0);
1057
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1058
+ if (sub) {
1059
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1060
+ } else {
1061
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1062
+ try buildRage(battle, player.foe(), options);
1018
1063
  }
1064
+ return null;
1019
1065
  }
1020
1066
  }
1021
1067
 
1022
1068
  // Substitute being broken nullifies the move's effect completely so even
1023
- // if an effect was intended to "always happen" it will still get skipped.
1069
+ // if an effect was intended to "always happen" it will still get skipped
1024
1070
  if (nullified) return null;
1025
1071
 
1026
1072
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1027
1073
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1028
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1074
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1029
1075
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1030
1076
 
1031
1077
  if (foe.stored().hp == 0) return null;
@@ -1040,7 +1086,7 @@ fn doMove(
1040
1086
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1041
1087
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1042
1088
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1043
- // puts the |-status| message before |-hitcount| instead of after.
1089
+ // puts the |-status| message before |-hitcount| instead of after
1044
1090
  if (move.effect == .Twineedle) {
1045
1091
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1046
1092
  } else if (move.effect == .Disable) {
@@ -1059,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1059
1105
  const side = battle.side(player);
1060
1106
 
1061
1107
  // Base speed is used for the critical hit calculation, even when Transform-ed
1062
- var rate = @as(u16, Species.chance(side.stored().species));
1108
+ var rate: u16 = Species.chance(side.stored().species);
1063
1109
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1064
1110
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1065
1111
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1066
1112
 
1067
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1113
+ const spurious =
1114
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1115
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1068
1116
  }
1069
1117
 
1070
1118
  fn calcDamage(
@@ -1073,6 +1121,7 @@ fn calcDamage(
1073
1121
  target_player: Player,
1074
1122
  m: ?Move.Data,
1075
1123
  crit: bool,
1124
+ options: anytype,
1076
1125
  ) bool {
1077
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1078
1127
  const cfz = m == null;
@@ -1084,28 +1133,8 @@ fn calcDamage(
1084
1133
 
1085
1134
  const special = move.type.special();
1086
1135
 
1087
- // zig fmt: off
1088
- var atk: u32 =
1089
- if (crit)
1090
- if (special) side.stored().stats.spc
1091
- else side.stored().stats.atk
1092
- else
1093
- if (special) side.active.stats.spc
1094
- else side.active.stats.atk;
1095
- var def: u32 =
1096
- if (crit)
1097
- if (special) target.stored().stats.spc
1098
- else target.stored().stats.def
1099
- else
1100
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1101
- if (special)
1102
- target.active.stats.spc *
1103
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1104
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1105
- else
1106
- target.active.stats.def *
1107
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1108
- // zig fmt: on
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1109
1138
 
1110
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1111
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1113,12 +1142,12 @@ fn calcDamage(
1113
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1114
1143
  atk = @max((atk / 4) & 255, 1);
1115
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1116
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1117
1146
  }
1118
1147
 
1119
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1148
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1120
1149
 
1121
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1150
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1122
1151
 
1123
1152
  if (def == 0) return false;
1124
1153
 
@@ -1130,11 +1159,41 @@ fn calcDamage(
1130
1159
  d = @min(997, d);
1131
1160
  d += 2;
1132
1161
 
1162
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1163
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1164
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1165
+ // that results in counter intuitive probabilities in the common case
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1167
+ atk = attack(side, !crit, special);
1168
+ def = defense(target, !crit, special, false);
1169
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1170
+ }
1171
+
1133
1172
  battle.last_damage = @intCast(d);
1134
1173
 
1135
1174
  return true;
1136
1175
  }
1137
1176
 
1177
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1180
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1181
+ }
1182
+
1183
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) target.stored().stats.spc else target.stored().stats.def
1186
+ else
1187
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1188
+ if (special)
1189
+ target.active.stats.spc *
1190
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1191
+ else
1192
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1193
+ target.active.stats.def *
1194
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1195
+ }
1196
+
1138
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1139
1198
  const side = battle.side(player);
1140
1199
  const foe = battle.foe(player);
@@ -1179,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1179
1238
  const foe = battle.foe(player);
1180
1239
 
1181
1240
  if (!try checkHit(battle, player, move, options)) return null;
1182
- try options.chance.commit(player, .hit);
1183
1241
 
1184
1242
  battle.last_damage = switch (side.last_selected_move) {
1185
1243
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1190,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1190
1248
  .Psywave => power: {
1191
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1192
1250
  // GLITCH: Psywave infinite glitch loop
1193
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1194
1252
  break :power try Rolls.psywave(battle, player, max, options);
1195
1253
  },
1196
1254
  else => unreachable,
1197
1255
  };
1198
1256
 
1199
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1200
1258
 
1201
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1202
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1209,7 +1267,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1209
1267
  const foe = battle.foe(player);
1210
1268
 
1211
1269
  if (battle.last_damage == 0) {
1212
- try options.log.fail(battle.active(player), .None);
1270
+ try options.log.fail(.{ battle.active(player), .None });
1213
1271
  return null;
1214
1272
  }
1215
1273
 
@@ -1227,22 +1285,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1285
  foe_last_selected_move.type == .Fighting);
1228
1286
 
1229
1287
  if (!used and !selected) {
1230
- try options.log.fail(battle.active(player), .None);
1288
+ try options.log.fail(.{ battle.active(player), .None });
1231
1289
  return null;
1232
1290
  }
1233
1291
 
1234
1292
  if (!used or !selected) {
1235
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1236
- if (!showdown) return Result.Error;
1237
- try options.log.fail(battle.active(player), .None);
1294
+ if (!pkmn.options.mod) return Result.Error;
1295
+ try options.log.fail(.{ battle.active(player), .None });
1238
1296
  return null;
1239
1297
  }
1240
1298
 
1241
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1242
1304
 
1243
- // Pokémon Showdown calls checkHit before Counter
1305
+ // Pokémon Showdown calls moveHit before Counter
1244
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1245
- try options.chance.commit(player, .hit);
1246
1307
 
1247
1308
  const sub = showdown and foe.active.volatiles.Substitute;
1248
1309
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1270,20 +1331,20 @@ fn applyDamage(
1270
1331
  subbed.active.volatiles.substitute = 0;
1271
1332
  subbed.active.volatiles.Substitute = false;
1272
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1273
- try options.log.end(battle.active(sub_player), .Substitute);
1274
- options.calc.capped(target_player);
1334
+ try options.log.end(.{ battle.active(sub_player), .Substitute });
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1275
1336
  return true;
1276
1337
  } else {
1277
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
1278
1339
  subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
1279
- try options.log.activate(battle.active(sub_player), .Substitute);
1340
+ try options.log.activate(.{ battle.active(sub_player), .Substitute });
1280
1341
  return false;
1281
1342
  }
1282
1343
  }
1283
1344
 
1284
1345
  if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
1285
1346
  target.stored().hp -= battle.last_damage;
1286
- try options.log.damage(battle.active(target_player), target.stored(), reason);
1347
+ try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
1287
1348
  return false;
1288
1349
  }
1289
1350
 
@@ -1302,10 +1363,10 @@ fn mirrorMove(
1302
1363
  side.last_selected_move = foe.last_used_move;
1303
1364
 
1304
1365
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1305
- try options.log.fail(battle.active(player), .None);
1366
+ try options.log.fail(.{ battle.active(player), .None });
1306
1367
  return null;
1307
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1308
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1309
1370
  }
1310
1371
 
1311
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1326,7 +1387,7 @@ fn metronome(
1326
1387
  var side = battle.side(player);
1327
1388
 
1328
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1329
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1330
1391
 
1331
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1332
1393
  return null;
@@ -1338,20 +1399,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1338
1399
  var immune = false;
1339
1400
  var mist = false;
1340
1401
 
1341
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1402
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1403
+ if (!showdown) try options.chance.commit(player, .hit);
1404
+ return true;
1405
+ }
1342
1406
 
1343
1407
  assert(!immune);
1344
1408
  if (mist) {
1345
1409
  assert(!showdown);
1346
1410
  const foe_ident = battle.active(player.foe());
1347
- try options.log.activate(foe_ident, .Mist);
1348
- try options.log.fail(foe_ident, .None);
1411
+ try options.log.activate(.{ foe_ident, .Mist });
1412
+ try options.log.fail(.{ foe_ident, .None });
1349
1413
  } else {
1350
- try options.chance.commit(player, .miss);
1351
- try options.log.lastmiss();
1352
- try options.log.miss(battle.active(player));
1414
+ try options.log.lastmiss(.{});
1415
+ try options.log.miss(.{battle.active(player)});
1353
1416
  }
1354
1417
 
1418
+ if (!showdown) try options.chance.commit(player, .miss);
1355
1419
  return false;
1356
1420
  }
1357
1421
 
@@ -1362,7 +1426,7 @@ fn moveHit(
1362
1426
  immune: *bool,
1363
1427
  mist: *bool,
1364
1428
  options: anytype,
1365
- ) bool {
1429
+ ) !bool {
1366
1430
  var side = battle.side(player);
1367
1431
  const foe = battle.foe(player);
1368
1432
 
@@ -1401,7 +1465,7 @@ fn moveHit(
1401
1465
  const overwritten = overwrite and state > 0;
1402
1466
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1467
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1468
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1469
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1470
  accuracy = accuracy * boost[0] / boost[1];
1407
1471
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1413,7 +1477,7 @@ fn moveHit(
1413
1477
  if (overwrite) side.active.volatiles.state = accuracy;
1414
1478
 
1415
1479
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1416
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1480
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1417
1481
  };
1418
1482
 
1419
1483
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1424,13 +1488,17 @@ fn moveHit(
1424
1488
 
1425
1489
  if (!miss) return true;
1426
1490
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1491
+
1427
1492
  side.active.volatiles.Binding = false;
1493
+ options.chance.observe(.binding, player, .None);
1494
+
1428
1495
  return false;
1429
1496
  }
1430
1497
 
1431
1498
  fn checkFaint(
1432
1499
  battle: anytype,
1433
1500
  player: Player,
1501
+ cap: bool,
1434
1502
  options: anytype,
1435
1503
  ) @TypeOf(options.log).Error!?Result {
1436
1504
  const side = battle.side(player);
@@ -1443,20 +1511,20 @@ fn checkFaint(
1443
1511
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
1512
  const more = player_out or foe_out;
1445
1513
 
1446
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1447
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1514
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1515
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1448
1516
 
1449
1517
  assert(!side.active.volatiles.MultiHit);
1450
1518
  assert(!foe.active.volatiles.MultiHit);
1451
1519
 
1452
1520
  if (player_out and foe_out) {
1453
- try options.log.tie();
1521
+ try options.log.tie(.{});
1454
1522
  return Result.Tie;
1455
1523
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1524
+ try options.log.win(.{player.foe()});
1457
1525
  return if (player == .P1) Result.Lose else Result.Win;
1458
1526
  } else if (foe_out) {
1459
- try options.log.win(player);
1527
+ try options.log.win(.{player});
1460
1528
  return if (player == .P1) Result.Win else Result.Lose;
1461
1529
  }
1462
1530
 
@@ -1465,7 +1533,7 @@ fn checkFaint(
1465
1533
  return .{ .p1 = foe_choice, .p2 = .Switch };
1466
1534
  }
1467
1535
 
1468
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1536
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1469
1537
  var side = battle.side(player);
1470
1538
  var foe = battle.foe(player);
1471
1539
  assert(side.stored().hp == 0);
@@ -1474,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1474
1542
  assert(!foe_volatiles.MultiHit);
1475
1543
  if (foe_volatiles.Bide) {
1476
1544
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1477
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1545
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1478
1546
  if (foe_volatiles.state != 0) return Result.Error;
1479
1547
  }
1480
1548
 
@@ -1498,8 +1566,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1566
  side.stored().stats.spe;
1499
1567
  }
1500
1568
 
1501
- try options.log.faint(battle.active(player), done);
1502
- options.calc.capped(player);
1569
+ try options.log.faint(.{ battle.active(player), done });
1570
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1571
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1572
+ if (cap) options.calc.capped(player);
1503
1573
  return null;
1504
1574
  }
1505
1575
 
@@ -1520,8 +1590,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1590
 
1521
1591
  if (!showdown or damage > 0) {
1522
1592
  stored.hp -= damage;
1523
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1524
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1593
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1594
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1525
1595
  }
1526
1596
  }
1527
1597
 
@@ -1546,14 +1616,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1616
  const amount = @min(damage, stored.hp);
1547
1617
  stored.hp -= amount;
1548
1618
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1619
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1620
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1551
1621
 
1552
1622
  const before = foe_stored.hp;
1553
1623
  // Uncapped damage is added back to the foe
1554
1624
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1625
  // Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
1556
- if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
1626
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1627
  }
1558
1628
  }
1559
1629
 
@@ -1563,19 +1633,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1563
1633
 
1564
1634
  battle.turn += 1;
1565
1635
 
1566
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1636
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1637
+ try options.log.tie(.{});
1568
1638
  return Result.Tie;
1569
1639
  }
1570
1640
 
1571
- if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1641
+ if (pkmn.options.mod and battle.turn >= 1000) {
1642
+ try options.log.tie(.{});
1573
1643
  return Result.Tie;
1574
1644
  } else if (battle.turn >= 65535) {
1575
1645
  return Result.Error;
1576
1646
  }
1577
1647
 
1578
- try options.log.turn(battle.turn);
1648
+ try options.log.turn(.{battle.turn});
1579
1649
 
1580
1650
  return Result.Default;
1581
1651
  }
@@ -1645,7 +1715,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1715
  side.last_selected_move;
1646
1716
  if (m != .None and m == last) {
1647
1717
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1718
+ try options.log.cant(.{ ident, .Disable, last });
1649
1719
  return true;
1650
1720
  }
1651
1721
  }
@@ -1659,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1729
  }
1660
1730
  }
1661
1731
 
1662
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1663
- var volatiles = &active.volatiles;
1664
- assert(volatiles.Thrashing);
1665
- if (volatiles.attacks > 0) return false;
1666
-
1667
- volatiles.Thrashing = false;
1668
- volatiles.Confusion = true;
1669
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1670
- return true;
1671
- }
1672
-
1673
1732
  fn onBegin(
1674
1733
  battle: anytype,
1675
1734
  player: Player,
@@ -1678,7 +1737,6 @@ fn onBegin(
1678
1737
  residual: *bool,
1679
1738
  options: anytype,
1680
1739
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1740
  return switch (move.effect) {
1683
1741
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1742
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,7 +1759,6 @@ fn onBegin(
1701
1759
  }
1702
1760
 
1703
1761
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1705
1762
  return switch (move.effect) {
1706
1763
  .Bide => Effects.bide(battle, player, options),
1707
1764
  // zig fmt: off
@@ -1723,7 +1780,6 @@ fn alwaysHappens(
1723
1780
  residual: *bool,
1724
1781
  options: anytype,
1725
1782
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1783
  return switch (move.effect) {
1728
1784
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1785
  .Explode => Effects.explode(battle, player),
@@ -1759,10 +1815,12 @@ pub const Effects = struct {
1759
1815
 
1760
1816
  side.active.volatiles.Bide = true;
1761
1817
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1818
+
1762
1819
  side.active.volatiles.state = 0;
1763
1820
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1821
+ options.chance.observe(.attacking, player, .started);
1764
1822
 
1765
- try options.log.start(battle.active(player), .Bide);
1823
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1824
  }
1767
1825
 
1768
1826
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1834,11 @@ pub const Effects = struct {
1776
1834
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1835
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1836
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1837
+ try options.log.curestatus(.{
1780
1838
  battle.active(player.foe()),
1781
1839
  foe_stored.status,
1782
1840
  .Message,
1783
- );
1841
+ });
1784
1842
  foe_stored.status = 0;
1785
1843
  }
1786
1844
  return;
@@ -1794,7 +1852,7 @@ pub const Effects = struct {
1794
1852
  foe_stored.status = Status.init(.BRN);
1795
1853
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1854
 
1797
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1855
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1798
1856
  }
1799
1857
 
1800
1858
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1862,8 @@ pub const Effects = struct {
1804
1862
  volatiles.Charging = true;
1805
1863
  const move = side.last_selected_move;
1806
1864
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1865
+ try options.log.laststill(.{});
1866
+ try options.log.prepare(.{ battle.active(player), move });
1809
1867
  }
1810
1868
 
1811
1869
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,42 +1874,39 @@ pub const Effects = struct {
1816
1874
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1875
  } else {
1818
1876
  if (showdown) {
1819
- if (!try checkHit(battle, player, move, options)) {
1820
- return;
1821
- } else if (sub) {
1822
- return options.log.fail(battle.active(player.foe()), .None);
1823
- }
1877
+ if (!try checkHit(battle, player, move, options)) return;
1878
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1879
  } else {
1825
- if (sub) {
1826
- return options.log.fail(battle.active(player.foe()), .None);
1827
- } else if (!try checkHit(battle, player, move, options)) {
1828
- return;
1829
- }
1880
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1881
+ if (!try checkHit(battle, player, move, options)) return;
1830
1882
  }
1831
- try options.chance.commit(player, .hit);
1832
1883
  }
1833
1884
 
1834
1885
  if (foe.active.volatiles.Confusion) return;
1835
1886
  foe.active.volatiles.Confusion = true;
1836
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1887
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1888
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1889
+ options.chance.observe(.confusion, player.foe(), .started);
1890
+ options.calc.confusion(player.foe());
1891
+
1892
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1893
  }
1840
1894
 
1841
1895
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1896
  const foe = battle.foe(player);
1843
1897
 
1844
1898
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1899
+ try options.log.lastmiss(.{});
1900
+ return options.log.miss(.{battle.active(player)});
1847
1901
  }
1848
1902
 
1849
1903
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1904
+ return options.log.start(.{
1851
1905
  battle.active(player),
1906
+ .TypeChange,
1852
1907
  foe.active.types,
1853
1908
  battle.active(player.foe()),
1854
- );
1909
+ });
1855
1910
  }
1856
1911
 
1857
1912
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1860,13 +1915,10 @@ pub const Effects = struct {
1860
1915
  const foe_ident = battle.active(player.foe());
1861
1916
 
1862
1917
  // Pokémon Showdown handles hit/miss earlier in doMove
1863
- if (!showdown) {
1864
- if (!try checkHit(battle, player, move, options)) return null;
1865
- try options.chance.commit(player, .hit);
1866
- }
1918
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1867
1919
 
1868
1920
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1921
+ try options.log.fail(.{ foe_ident, .None });
1870
1922
  return null;
1871
1923
  }
1872
1924
 
@@ -1881,26 +1933,27 @@ pub const Effects = struct {
1881
1933
 
1882
1934
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1935
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
- if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1936
+ if (n == 0 or (pkmn.options.mod and err)) {
1937
+ try options.log.fail(.{ foe_ident, .None });
1886
1938
  return null;
1887
1939
  } else if (err) {
1888
1940
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1889
- assert(!showdown);
1890
1941
  return Result.Error;
1891
1942
  }
1892
1943
 
1893
1944
  volatiles.disable_move =
1894
1945
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1895
1946
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1947
+ options.chance.observe(.disable, player.foe(), .started);
1896
1948
 
1897
1949
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1950
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1951
  return null;
1900
1952
  }
1901
1953
 
1902
1954
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1955
  var stored = battle.side(player).stored();
1956
+ const ident = battle.active(player);
1904
1957
 
1905
1958
  if (battle.last_damage == 0) {
1906
1959
  assert(showdown);
@@ -1913,7 +1966,7 @@ pub const Effects = struct {
1913
1966
  if (stored.hp == stored.stats.hp) return;
1914
1967
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1968
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1969
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1970
  }
1918
1971
 
1919
1972
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +1998,7 @@ pub const Effects = struct {
1945
1998
  if (side.active.volatiles.FocusEnergy) return;
1946
1999
  side.active.volatiles.FocusEnergy = true;
1947
2000
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
2001
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
2002
  }
1950
2003
 
1951
2004
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1962,12 +2015,12 @@ pub const Effects = struct {
1962
2015
  if (foe.active.types.includes(move.type)) return;
1963
2016
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1964
2017
  // Freeze Clause Mod
1965
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2018
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1966
2019
 
1967
2020
  foe_stored.status = Status.init(.FRZ);
1968
2021
  // GLITCH: Hyper Beam recharging status is not cleared
1969
2022
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
2023
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
2024
  }
1972
2025
 
1973
2026
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1975,7 +2028,7 @@ pub const Effects = struct {
1975
2028
  var side = battle.side(player);
1976
2029
  var foe = battle.foe(player);
1977
2030
 
1978
- var side_stored = side.stored();
2031
+ const side_stored = side.stored();
1979
2032
  var foe_stored = foe.stored();
1980
2033
 
1981
2034
  const player_ident = battle.active(player);
@@ -1987,8 +2040,8 @@ pub const Effects = struct {
1987
2040
  side.active.stats = side_stored.stats;
1988
2041
  foe.active.stats = foe_stored.stats;
1989
2042
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
2043
+ try log.activate(.{ player_ident, .Haze });
2044
+ try log.clearallboost(.{});
1992
2045
 
1993
2046
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
2047
  if (showdown) {
@@ -1996,24 +2049,26 @@ pub const Effects = struct {
1996
2049
  const p: Player = @enumFromInt(i);
1997
2050
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
2051
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2052
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2053
  s.stored().status = 0;
2054
+ options.chance.observe(.sleep, p, .None);
2001
2055
  } else if (showdown and s.stored().status == Status.TOX) {
2002
2056
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
2057
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
2058
  }
2005
- try clearVolatiles(battle, p, p != player, options);
2059
+ try clearVolatiles(battle, p, options);
2006
2060
  }
2007
2061
  } else {
2008
2062
  if (Status.any(foe_stored.status)) {
2009
2063
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
2064
  foe.last_selected_move = .SKIP_TURN;
2065
+ options.chance.observe(.sleep, player.foe(), .None);
2011
2066
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2067
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
2068
  foe_stored.status = 0;
2014
2069
  }
2015
- try clearVolatiles(battle, player, false, options);
2016
- try clearVolatiles(battle, player.foe(), true, options);
2070
+ try clearVolatiles(battle, player, options);
2071
+ try clearVolatiles(battle, player.foe(), options);
2017
2072
  }
2018
2073
  }
2019
2074
 
@@ -2025,7 +2080,7 @@ pub const Effects = struct {
2025
2080
  // GLITCH: HP recovery move failure glitches
2026
2081
  const delta = stored.stats.hp - stored.hp;
2027
2082
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return options.log.fail(ident, .None);
2083
+ return options.log.fail(.{ ident, .None });
2029
2084
  }
2030
2085
 
2031
2086
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2088,17 @@ pub const Effects = struct {
2033
2088
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2089
  if (showdown) battle.rng.advance(1);
2035
2090
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2091
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2092
  stored.hp = stored.stats.hp;
2038
2093
  } else {
2039
2094
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2095
  }
2041
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2096
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2042
2097
  }
2043
2098
 
2044
2099
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2100
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2101
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2102
  }
2048
2103
 
2049
2104
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2107,31 @@ pub const Effects = struct {
2052
2107
  if (showdown) {
2053
2108
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2109
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2110
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2111
  }
2057
2112
  if (!try checkHit(battle, player, move, options)) return;
2058
2113
  if (foe.active.volatiles.LeechSeed) return;
2059
2114
  } else {
2060
2115
  if (!try checkHit(battle, player, move, options)) return;
2061
2116
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2117
+ try options.log.lastmiss(.{});
2118
+ return options.log.miss(.{battle.active(player)});
2064
2119
  }
2065
2120
  }
2066
2121
 
2067
- try options.chance.commit(player, .hit);
2068
2122
  foe.active.volatiles.LeechSeed = true;
2069
2123
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2124
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2125
  }
2072
2126
 
2073
2127
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2128
  var side = battle.side(player);
2129
+ const ident = battle.active(player);
2075
2130
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2131
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2132
  side.active.volatiles.LightScreen = true;
2080
2133
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2134
+ try options.log.start(.{ ident, .LightScreen });
2082
2135
  }
2083
2136
 
2084
2137
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2106,17 +2159,15 @@ pub const Effects = struct {
2106
2159
  if (!has_mimic) {
2107
2160
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2161
  if (!try checkHit(battle, player, move, options)) return;
2109
- try options.chance.commit(player, .hit);
2110
- return options.log.fail(battle.active(player.foe()), .None);
2162
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2111
2163
  }
2112
2164
  }
2113
2165
  if (!try checkHit(battle, player, move, options)) return;
2114
- try options.chance.commit(player, .hit);
2115
2166
 
2116
2167
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2168
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2169
 
2119
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2170
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2120
2171
  }
2121
2172
 
2122
2173
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2176,7 @@ pub const Effects = struct {
2125
2176
  if (side.active.volatiles.Mist) return;
2126
2177
  side.active.volatiles.Mist = true;
2127
2178
 
2128
- try options.log.start(battle.active(player), .Mist);
2179
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2180
  }
2130
2181
 
2131
2182
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,25 +2202,25 @@ pub const Effects = struct {
2151
2202
 
2152
2203
  if (showdown) {
2153
2204
  // Invulnerability trumps type immunity on Pokémon Showdown
2154
- if (immune and !foe.active.volatiles.Invulnerable) return log.immune(foe_ident, .None);
2155
- if (!try checkHit(battle, player, move, options)) return;
2205
+ if (immune and !foe.active.volatiles.Invulnerable) {
2206
+ return log.immune(.{ foe_ident, .None });
2207
+ } else if (!try checkHit(battle, player, move, options)) return;
2156
2208
  }
2157
2209
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2210
+ return log.fail(.{
2159
2211
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2212
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2213
+ });
2162
2214
  }
2163
2215
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2216
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2217
  if (!try checkHit(battle, player, move, options)) return;
2166
2218
  }
2167
- try options.chance.commit(player, .hit);
2168
2219
 
2169
2220
  foe_stored.status = Status.init(.PAR);
2170
2221
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2222
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2223
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2224
  }
2174
2225
 
2175
2226
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2242,12 @@ pub const Effects = struct {
2191
2242
  foe_stored.status = Status.init(.PAR);
2192
2243
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2244
 
2194
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2245
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2195
2246
  }
2196
2247
 
2197
2248
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2249
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2250
+ try options.log.fieldactivate(.{});
2200
2251
  }
2201
2252
  }
2202
2253
 
@@ -2211,28 +2262,27 @@ pub const Effects = struct {
2211
2262
  return;
2212
2263
  } else if (foe.active.volatiles.Substitute) {
2213
2264
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2265
+ return log.fail(.{ foe_ident, .None });
2215
2266
  } else if (Status.any(foe_stored.status)) {
2216
2267
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2268
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2269
  // will not include a fail reason for Toxic vs. Poison or vice-versa...
2219
- return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2270
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2271
  if (!showdown)
2221
- .Poison
2272
+ Fail.Poison
2222
2273
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2274
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2275
  else
2225
- .None
2276
+ Fail.None
2226
2277
  else
2227
- .None);
2278
+ Fail.None });
2228
2279
  } else if (foe.active.types.includes(.Poison)) {
2229
2280
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2281
+ return log.immune(.{ foe_ident, .None });
2231
2282
  }
2232
2283
 
2233
2284
  if (move.effect == .Poison) {
2234
2285
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2235
- try options.chance.commit(player, .hit);
2236
2286
  } else {
2237
2287
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2238
2288
  return;
@@ -2246,7 +2296,7 @@ pub const Effects = struct {
2246
2296
  foe.active.volatiles.toxic = 0;
2247
2297
  }
2248
2298
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2299
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2300
  }
2251
2301
 
2252
2302
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2316,23 @@ pub const Effects = struct {
2266
2316
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2317
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2318
 
2269
- try options.log.damageOf(
2319
+ try options.log.damage(.{
2270
2320
  battle.active(player),
2271
2321
  stored,
2272
2322
  .RecoilOf,
2273
2323
  battle.active(player.foe()),
2274
- );
2324
+ });
2275
2325
  if (showdown and stored.hp == 0) residual.* = false;
2276
2326
  }
2277
2327
 
2278
2328
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2329
  var side = battle.side(player);
2330
+ const ident = battle.active(player);
2280
2331
 
2281
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2332
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2282
2333
  side.active.volatiles.Reflect = true;
2283
2334
 
2284
- try options.log.start(battle.active(player), .Reflect);
2335
+ try options.log.start(.{ ident, .Reflect });
2285
2336
  }
2286
2337
 
2287
2338
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,18 +2346,17 @@ pub const Effects = struct {
2295
2346
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2347
  } else {
2297
2348
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2349
+ return options.log.fail(.{
2299
2350
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2351
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2352
+ });
2302
2353
  }
2303
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2354
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2304
2355
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2305
- try options.chance.commit(player, .hit);
2306
2356
  }
2307
2357
 
2308
2358
  // Sleep Clause Mod
2309
- if (showdown) {
2359
+ if (pkmn.options.mod) {
2310
2360
  for (foe.pokemon) |p| {
2311
2361
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2312
2362
  }
@@ -2314,18 +2364,20 @@ pub const Effects = struct {
2314
2364
  foe.active.volatiles.Recharging = false;
2315
2365
 
2316
2366
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2367
+ options.chance.observe(.sleep, player.foe(), .started);
2368
+
2317
2369
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2370
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2371
  }
2320
2372
 
2321
2373
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2374
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2375
  }
2324
2376
 
2325
2377
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2378
  var side = battle.side(player);
2327
2379
  if (side.active.volatiles.Substitute) {
2328
- return options.log.fail(battle.active(player), .Substitute);
2380
+ return options.log.fail(.{ battle.active(player), .Substitute });
2329
2381
  }
2330
2382
 
2331
2383
  assert(side.stored().stats.hp <= 1023);
@@ -2336,15 +2388,17 @@ pub const Effects = struct {
2336
2388
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2337
2389
  // avoid having to convert to floating point)
2338
2390
  const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
2339
- if (side.stored().hp < required_hp) return options.log.fail(battle.active(player), .Weak);
2391
+ if (side.stored().hp < required_hp) {
2392
+ return options.log.fail(.{ battle.active(player), .Weak });
2393
+ }
2340
2394
 
2341
2395
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2342
2396
  side.stored().hp -= hp;
2343
2397
  side.active.volatiles.substitute = hp + 1;
2344
2398
  side.active.volatiles.Substitute = true;
2345
- try options.log.start(battle.active(player), .Substitute);
2399
+ try options.log.start(.{ battle.active(player), .Substitute });
2346
2400
  if (hp > 0) {
2347
- try options.log.damage(battle.active(player), side.stored(), .None);
2401
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2348
2402
  if (showdown and side.stored().hp == 0) residual.* = false;
2349
2403
  }
2350
2404
  }
@@ -2353,13 +2407,11 @@ pub const Effects = struct {
2353
2407
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2408
  if (showdown) {
2355
2409
  if (battle.side(player).last_selected_move == .Teleport) return;
2356
- if (try checkHit(battle, player, move, options)) {
2357
- try options.chance.commit(player, .hit);
2358
- }
2410
+ _ = try checkHit(battle, player, move, options);
2359
2411
  battle.last_damage = 0;
2360
2412
  } else {
2361
- try options.log.fail(battle.active(player), .None);
2362
- try options.log.laststill();
2413
+ try options.log.fail(.{ battle.active(player), .None });
2414
+ try options.log.laststill(.{});
2363
2415
  }
2364
2416
  }
2365
2417
 
@@ -2370,6 +2422,8 @@ pub const Effects = struct {
2370
2422
 
2371
2423
  volatiles.Thrashing = true;
2372
2424
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2425
+
2426
+ options.chance.observe(.attacking, player, .started);
2373
2427
  }
2374
2428
 
2375
2429
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2398,7 +2452,7 @@ pub const Effects = struct {
2398
2452
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2399
2453
  }
2400
2454
 
2401
- try options.log.transform(battle.active(player), foe_ident);
2455
+ try options.log.transform(.{ battle.active(player), foe_ident });
2402
2456
  }
2403
2457
 
2404
2458
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2408,13 +2462,14 @@ pub const Effects = struct {
2408
2462
  if (side.active.volatiles.Binding) return;
2409
2463
  side.active.volatiles.Binding = true;
2410
2464
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2411
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2465
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2412
2466
  if (!showdown) {
2413
2467
  foe.active.volatiles.Recharging = false;
2414
2468
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2415
2469
 
2416
2470
  side.active.volatiles.attacks =
2417
2471
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2472
+ options.chance.observe(.binding, player, .started);
2418
2473
  }
2419
2474
 
2420
2475
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2428,87 +2483,87 @@ pub const Effects = struct {
2428
2483
  switch (move.effect) {
2429
2484
  .AttackUp1, .AttackUp2, .Rage => {
2430
2485
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2431
- if (boosts.atk == 6) return log.fail(ident, .None);
2486
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2432
2487
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2433
2488
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2434
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2489
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2435
2490
  if (stats.atk == MAX_STAT_VALUE) {
2436
2491
  boosts.atk -= 1;
2437
2492
  if (showdown) {
2438
- try log.boost(ident, reason, n);
2439
- try log.boost(ident, Boost.Attack, -1);
2493
+ try log.boost(.{ ident, reason, n });
2494
+ try log.boost(.{ ident, .Attack, -1 });
2440
2495
  if (move.effect == .Rage) return;
2441
2496
  }
2442
- return log.fail(ident, .None);
2497
+ return log.fail(.{ ident, .None });
2443
2498
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2499
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2500
  const stat = unmodifiedStats(battle, side).atk;
2446
2501
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
- try log.boost(ident, reason, n);
2502
+ try log.boost(.{ ident, reason, n });
2448
2503
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2449
2504
  if (showdown and move.effect == .Rage) return;
2450
2505
  },
2451
2506
  .DefenseUp1, .DefenseUp2 => {
2452
2507
  assert(boosts.def >= -6 and boosts.def <= 6);
2453
- if (boosts.def == 6) return log.fail(ident, .None);
2508
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2454
2509
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2455
2510
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2456
2511
  if (stats.def == MAX_STAT_VALUE) {
2457
2512
  boosts.def -= 1;
2458
2513
  if (showdown) {
2459
- try log.boost(ident, .Defense, n);
2460
- try log.boost(ident, .Defense, -1);
2514
+ try log.boost(.{ ident, .Defense, n });
2515
+ try log.boost(.{ ident, .Defense, -1 });
2461
2516
  }
2462
- return log.fail(ident, .None);
2517
+ return log.fail(.{ ident, .None });
2463
2518
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2519
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2520
  const stat = unmodifiedStats(battle, side).def;
2466
2521
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
- try log.boost(ident, .Defense, n);
2522
+ try log.boost(.{ ident, .Defense, n });
2468
2523
  },
2469
2524
  .SpeedUp2 => {
2470
2525
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2471
- if (boosts.spe == 6) return log.fail(ident, .None);
2526
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2472
2527
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2473
2528
  if (stats.spe == MAX_STAT_VALUE) {
2474
2529
  boosts.spe -= 1;
2475
2530
  if (showdown) {
2476
- try log.boost(ident, .Speed, 2);
2477
- try log.boost(ident, .Speed, -1);
2531
+ try log.boost(.{ ident, .Speed, 2 });
2532
+ try log.boost(.{ ident, .Speed, -1 });
2478
2533
  }
2479
- return log.fail(ident, .None);
2534
+ return log.fail(.{ ident, .None });
2480
2535
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2536
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2537
  const stat = unmodifiedStats(battle, side).spe;
2483
2538
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
- try log.boost(ident, .Speed, 2);
2539
+ try log.boost(.{ ident, .Speed, 2 });
2485
2540
  },
2486
2541
  .SpecialUp1, .SpecialUp2 => {
2487
2542
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2488
- if (boosts.spc == 6) return log.fail(ident, .None);
2543
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2489
2544
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2490
2545
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2491
2546
  if (stats.spc == MAX_STAT_VALUE) {
2492
2547
  boosts.spc -= 1;
2493
2548
  if (showdown) {
2494
- try log.boost(ident, .SpecialAttack, n);
2495
- try log.boost(ident, .SpecialAttack, -1);
2496
- try log.boost(ident, .SpecialDefense, n);
2497
- try log.boost(ident, .SpecialDefense, -1);
2549
+ try log.boost(.{ ident, .SpecialAttack, n });
2550
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2551
+ try log.boost(.{ ident, .SpecialDefense, n });
2552
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2498
2553
  }
2499
- return log.fail(ident, .None);
2554
+ return log.fail(.{ ident, .None });
2500
2555
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2556
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2557
  const stat = unmodifiedStats(battle, side).spc;
2503
2558
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
- try log.boost(ident, .SpecialAttack, n);
2505
- try log.boost(ident, .SpecialDefense, n);
2559
+ try log.boost(.{ ident, .SpecialAttack, n });
2560
+ try log.boost(.{ ident, .SpecialDefense, n });
2506
2561
  },
2507
2562
  .EvasionUp1 => {
2508
2563
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2509
- if (boosts.evasion == 6) return log.fail(ident, .None);
2564
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2510
2565
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2511
- try log.boost(ident, .Evasion, 1);
2566
+ try log.boost(.{ ident, .Evasion, 1 });
2512
2567
  },
2513
2568
  else => unreachable,
2514
2569
  }
@@ -2523,7 +2578,7 @@ pub const Effects = struct {
2523
2578
  const foe_ident = battle.active(player.foe());
2524
2579
  const secondary = move.effect.isStatDownChance();
2525
2580
 
2526
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2581
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2527
2582
 
2528
2583
  if (secondary) {
2529
2584
  const proc = try Rolls.unboost(battle, player, options);
@@ -2531,7 +2586,6 @@ pub const Effects = struct {
2531
2586
  } else {
2532
2587
  // checkHit already checks for Invulnerable
2533
2588
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2589
  }
2536
2590
 
2537
2591
  var stats = &foe.active.stats;
@@ -2541,82 +2595,82 @@ pub const Effects = struct {
2541
2595
  switch (move.effect) {
2542
2596
  .AttackDown1, .AttackDownChance => {
2543
2597
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2544
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2598
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2545
2599
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2546
2600
  if (stats.atk == 1) {
2547
2601
  boosts.atk += 1;
2548
2602
  if (showdown) {
2549
- try log.boost(foe_ident, .Attack, -1);
2550
- try log.boost(foe_ident, .Attack, 1);
2603
+ try log.boost(.{ foe_ident, .Attack, -1 });
2604
+ try log.boost(.{ foe_ident, .Attack, 1 });
2551
2605
  }
2552
- return log.fail(foe_ident, .None);
2606
+ return log.fail(.{ foe_ident, .None });
2553
2607
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2608
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2609
  const stat = unmodifiedStats(battle, foe).atk;
2556
2610
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
- try log.boost(foe_ident, .Attack, -1);
2611
+ try log.boost(.{ foe_ident, .Attack, -1 });
2558
2612
  },
2559
2613
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2560
2614
  assert(boosts.def >= -6 and boosts.def <= 6);
2561
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2615
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2562
2616
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2563
2617
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2564
2618
  if (stats.def == 1) {
2565
2619
  boosts.def += 1;
2566
2620
  if (showdown) {
2567
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2568
- try log.boost(foe_ident, .Defense, 1);
2621
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2622
+ try log.boost(.{ foe_ident, .Defense, 1 });
2569
2623
  }
2570
- return log.fail(foe_ident, .None);
2624
+ return log.fail(.{ foe_ident, .None });
2571
2625
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2626
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2627
  const stat = unmodifiedStats(battle, foe).def;
2574
2628
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2629
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2576
2630
  },
2577
2631
  .SpeedDown1, .SpeedDownChance => {
2578
2632
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2579
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2633
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2580
2634
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2581
2635
  if (stats.spe == 1) {
2582
2636
  boosts.spe += 1;
2583
2637
  if (showdown) {
2584
- try log.boost(foe_ident, .Speed, -1);
2585
- try log.boost(foe_ident, .Speed, 1);
2638
+ try log.boost(.{ foe_ident, .Speed, -1 });
2639
+ try log.boost(.{ foe_ident, .Speed, 1 });
2586
2640
  }
2587
- return log.fail(foe_ident, .None);
2641
+ return log.fail(.{ foe_ident, .None });
2588
2642
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2643
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2644
  const stat = unmodifiedStats(battle, foe).spe;
2591
2645
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
- try log.boost(foe_ident, .Speed, -1);
2646
+ try log.boost(.{ foe_ident, .Speed, -1 });
2593
2647
  assert(boosts.spe >= -6);
2594
2648
  },
2595
2649
  .SpecialDownChance => {
2596
2650
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2597
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2651
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2598
2652
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2599
2653
  if (stats.spc == 1) {
2600
2654
  boosts.spc += 1;
2601
2655
  if (showdown) {
2602
- try log.boost(foe_ident, .SpecialAttack, -1);
2603
- try log.boost(foe_ident, .SpecialAttack, 1);
2604
- try log.boost(foe_ident, .SpecialDefense, -1);
2605
- try log.boost(foe_ident, .SpecialDefense, 1);
2656
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2657
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2658
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2659
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2606
2660
  }
2607
- return log.fail(foe_ident, .None);
2661
+ return log.fail(.{ foe_ident, .None });
2608
2662
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2663
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2664
  const stat = unmodifiedStats(battle, foe).spc;
2611
2665
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
- try log.boost(foe_ident, .SpecialAttack, -1);
2613
- try log.boost(foe_ident, .SpecialDefense, -1);
2666
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2667
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2614
2668
  },
2615
2669
  .AccuracyDown1 => {
2616
2670
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2617
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2671
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2618
2672
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2619
- try log.boost(foe_ident, .Accuracy, -1);
2673
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2620
2674
  },
2621
2675
  else => unreachable,
2622
2676
  }
@@ -2645,35 +2699,35 @@ fn isForced(active: anytype) bool {
2645
2699
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2700
  }
2647
2701
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2702
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2703
  var log = options.log;
2650
2704
  var side = battle.side(who);
2651
2705
  var volatiles = &side.active.volatiles;
2652
2706
  const ident = battle.active(who);
2653
2707
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2708
  if (volatiles.disable_move != 0) {
2657
2709
  volatiles.disable_move = 0;
2658
2710
  volatiles.disable_duration = 0;
2659
- try log.end(ident, .DisableSilent);
2711
+ options.chance.observe(.disable, who, .None);
2712
+ try log.end(.{ ident, .DisableSilent });
2660
2713
  }
2661
2714
  if (volatiles.Confusion) {
2662
2715
  // volatiles.confusion is left unchanged
2663
2716
  volatiles.Confusion = false;
2664
- try log.end(ident, .ConfusionSilent);
2717
+ options.chance.observe(.confusion, who, .None);
2718
+ try log.end(.{ ident, .ConfusionSilent });
2665
2719
  }
2666
2720
  if (volatiles.Mist) {
2667
2721
  volatiles.Mist = false;
2668
- try log.end(ident, .MistSilent);
2722
+ try log.end(.{ ident, .MistSilent });
2669
2723
  }
2670
2724
  if (volatiles.FocusEnergy) {
2671
2725
  volatiles.FocusEnergy = false;
2672
- try log.end(ident, .FocusEnergySilent);
2726
+ try log.end(.{ ident, .FocusEnergySilent });
2673
2727
  }
2674
2728
  if (volatiles.LeechSeed) {
2675
2729
  volatiles.LeechSeed = false;
2676
- try log.end(ident, .LeechSeedSilent);
2730
+ try log.end(.{ ident, .LeechSeedSilent });
2677
2731
  }
2678
2732
  if (!showdown and volatiles.Toxic) {
2679
2733
  volatiles.Toxic = false;
@@ -2681,24 +2735,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2681
2735
  }
2682
2736
  if (volatiles.LightScreen) {
2683
2737
  volatiles.LightScreen = false;
2684
- try log.end(ident, .LightScreenSilent);
2738
+ try log.end(.{ ident, .LightScreenSilent });
2685
2739
  }
2686
2740
  if (volatiles.Reflect) {
2687
2741
  volatiles.Reflect = false;
2688
- try log.end(ident, .ReflectSilent);
2742
+ try log.end(.{ ident, .ReflectSilent });
2689
2743
  }
2690
2744
  if (showdown and volatiles.Toxic) {
2691
2745
  volatiles.Toxic = false;
2692
2746
  // Pokémon Showdown erroneously clears the toxic counter
2693
2747
  volatiles.toxic = 0;
2694
- try log.end(ident, .ToxicSilent);
2748
+ try log.end(.{ ident, .ToxicSilent });
2695
2749
  }
2696
2750
  }
2697
2751
 
2752
+ fn decrement(
2753
+ comptime field: chance.Action.Field,
2754
+ player: Player,
2755
+ options: anytype,
2756
+ n: anytype,
2757
+ ) @TypeOf(n) {
2758
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2759
+ .started, .ended => 0,
2760
+ .continuing => if (n > 1) n - 1 else n,
2761
+ else => unreachable,
2762
+ } else n - 1;
2763
+ }
2764
+
2698
2765
  pub const Rolls = struct {
2699
2766
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2767
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2768
  player == .P1
2769
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2770
+ battle.rng.speedTie()
2702
2771
  else if (showdown)
2703
2772
  battle.rng.range(u8, 0, 2) == 0
2704
2773
  else
@@ -2708,21 +2777,31 @@ pub const Rolls = struct {
2708
2777
  return p1;
2709
2778
  }
2710
2779
 
2711
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2780
+ fn criticalHit(
2781
+ battle: anytype,
2782
+ player: Player,
2783
+ rate: u8,
2784
+ spurious: bool,
2785
+ options: anytype,
2786
+ ) !bool {
2712
2787
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2788
  val == .true
2789
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2790
+ battle.rng.criticalHit(player, rate)
2714
2791
  else if (showdown)
2715
2792
  battle.rng.chance(u8, rate, 256)
2716
2793
  else
2717
2794
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2795
 
2719
- try options.chance.criticalHit(player, crit, rate);
2796
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2797
  return crit;
2721
2798
  }
2722
2799
 
2723
2800
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2801
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2802
  val
2803
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2804
+ battle.rng.damage(player)
2726
2805
  else roll: {
2727
2806
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2807
  while (true) {
@@ -2736,21 +2815,27 @@ pub const Rolls = struct {
2736
2815
  return roll;
2737
2816
  }
2738
2817
 
2739
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2818
+ const uN = if (pkmn.options.miss) u8 else u9;
2819
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2820
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2740
2821
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2822
  val == .true
2823
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2824
+ battle.rng.hit(player, acc)
2742
2825
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2826
+ battle.rng.chance(uN, acc, 256)
2744
2827
  else
2745
- battle.rng.next() < accuracy;
2828
+ battle.rng.next() < acc;
2746
2829
 
2747
- options.chance.hit(ok, accuracy);
2830
+ try options.chance.hit(player, ok, acc);
2748
2831
  return ok;
2749
2832
  }
2750
2833
 
2751
2834
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2835
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2836
  val == .true
2837
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2838
+ battle.rng.confused(player)
2754
2839
  else if (showdown)
2755
2840
  !battle.rng.chance(u8, 128, 256)
2756
2841
  else
@@ -2763,6 +2848,8 @@ pub const Rolls = struct {
2763
2848
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2849
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2850
  val == .true
2851
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2852
+ battle.rng.paralyzed(player)
2766
2853
  else if (showdown)
2767
2854
  battle.rng.chance(u8, 63, 256)
2768
2855
  else
@@ -2775,6 +2862,8 @@ pub const Rolls = struct {
2775
2862
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2863
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2864
  val == .true
2865
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2866
+ battle.rng.secondaryChance(player, 25)
2778
2867
  else if (showdown)
2779
2868
  battle.rng.chance(u8, 25, 256)
2780
2869
  else
@@ -2789,6 +2878,8 @@ pub const Rolls = struct {
2789
2878
 
2790
2879
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2880
  val == .true
2881
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2882
+ battle.rng.secondaryChance(player, rate)
2792
2883
  else if (showdown)
2793
2884
  battle.rng.chance(u8, rate, 256)
2794
2885
  else
@@ -2803,6 +2894,8 @@ pub const Rolls = struct {
2803
2894
 
2804
2895
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2896
  val == .true
2897
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2898
+ battle.rng.secondaryChance(player, rate)
2806
2899
  else if (showdown)
2807
2900
  battle.rng.chance(u8, rate, 256)
2808
2901
  else
@@ -2815,6 +2908,8 @@ pub const Rolls = struct {
2815
2908
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2909
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2910
  val == .true
2911
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2912
+ battle.rng.secondaryChance(player, 85)
2818
2913
  else if (showdown)
2819
2914
  battle.rng.chance(u8, 85, 256)
2820
2915
  else
@@ -2827,6 +2922,8 @@ pub const Rolls = struct {
2827
2922
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2923
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2924
  val
2925
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2926
+ battle.rng.metronome(player)
2830
2927
  else if (showdown)
2831
2928
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2929
  else move: {
@@ -2845,6 +2942,8 @@ pub const Rolls = struct {
2845
2942
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2943
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2944
  val - 1
2945
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2946
+ battle.rng.psywave(player)
2848
2947
  else if (showdown)
2849
2948
  battle.rng.range(u8, 0, max)
2850
2949
  else power: {
@@ -2860,8 +2959,11 @@ pub const Rolls = struct {
2860
2959
  }
2861
2960
 
2862
2961
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
- @intCast(val)
2962
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2963
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2964
+ else
2965
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2966
+ battle.rng.sleepDuration(player)
2865
2967
  else if (showdown)
2866
2968
  battle.rng.range(u3, 1, 8)
2867
2969
  else duration: {
@@ -2872,46 +2974,55 @@ pub const Rolls = struct {
2872
2974
  };
2873
2975
 
2874
2976
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2977
+ options.chance.duration(player, duration);
2876
2978
  return duration;
2877
2979
  }
2878
2980
 
2879
2981
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
- val
2982
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 1 and val <= 8) val else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2986
+ battle.rng.disableDuration(player)
2882
2987
  else if (showdown)
2883
2988
  battle.rng.range(u4, 1, 9)
2884
2989
  else
2885
2990
  @intCast((battle.rng.next() & 7) + 1);
2886
2991
 
2887
2992
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
2993
+ options.chance.duration(player, duration);
2889
2994
  return duration;
2890
2995
  }
2891
2996
 
2892
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2893
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2894
- @intCast(val)
2997
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3002
+ battle.rng.confusionDuration(player)
2895
3003
  else if (showdown)
2896
3004
  battle.rng.range(u3, 2, 6)
2897
3005
  else
2898
3006
  @intCast((battle.rng.next() & 3) + 2);
2899
3007
 
2900
3008
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3009
+ options.chance.duration(player, duration);
2902
3010
  return duration;
2903
3011
  }
2904
3012
 
2905
3013
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
3014
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3015
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3018
+ battle.rng.attackingDuration(player)
2908
3019
  else if (showdown)
2909
3020
  battle.rng.range(u3, 2, 4)
2910
3021
  else
2911
3022
  @intCast((battle.rng.next() & 1) + 2);
2912
3023
 
2913
3024
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3025
+ options.chance.duration(player, duration);
2915
3026
  return duration;
2916
3027
  }
2917
3028
 
@@ -2924,8 +3035,11 @@ pub const Rolls = struct {
2924
3035
  options: anytype,
2925
3036
  ) !u3 {
2926
3037
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
- @intCast(val)
3038
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3039
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3040
+ else
3041
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3042
+ battle.rng.distribution(player)
2929
3043
  else if (showdown)
2930
3044
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3045
  else n: {
@@ -2935,7 +3049,7 @@ pub const Rolls = struct {
2935
3049
 
2936
3050
  assert(n >= 2 and n <= 5);
2937
3051
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3052
+ options.chance.duration(player, n);
2939
3053
  } else {
2940
3054
  try options.chance.multiHit(player, n);
2941
3055
  }
@@ -2950,15 +3064,15 @@ pub const Rolls = struct {
2950
3064
  options: anytype,
2951
3065
  ) !u4 {
2952
3066
  // TODO: consider throwing error instead of rerolling?
2953
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3067
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2954
3068
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2955
3069
  val
2956
3070
  else
2957
3071
  null
2958
3072
  else
2959
- null;
2960
-
2961
- const slot: u4 = overridden orelse slot: {
3073
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3074
+ battle.rng.moveSlot(player, moves, check_pp)
3075
+ else slot: {
2962
3076
  if (showdown) {
2963
3077
  if (check_pp == 0) {
2964
3078
  var i: usize = moves.len;
@@ -3038,11 +3152,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3152
  const side = battle.side(player);
3039
3153
  const foe = battle.foe(player);
3040
3154
 
3041
- var active = &side.active;
3155
+ const active = &side.active;
3042
3156
  const stored = side.stored();
3043
3157
 
3044
3158
  // While players are not given any input options on the cartridge in these cases,
3045
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3159
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3160
  //
3047
3161
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3162
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,7 +3164,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3164
  // passing due to being forced into using a move. Instead of introducing another option
3051
3165
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3166
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3167
+ // switch at all
3054
3168
  if (isForced(active)) {
3055
3169
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3170
  n += 1;
@@ -3069,7 +3183,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3183
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3184
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3185
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3186
+ // move normally if sleeping/frozen/bound
3073
3187
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3188
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3189
  {
@@ -3082,7 +3196,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3196
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3197
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3198
  // Move will not result in forcing the subsequent use unless the user also had the
3085
- // proc-ed move in their moveset) and disabling all other moves.
3199
+ // proc-ed move in their moveset) and disabling all other moves
3086
3200
  if (limited) {
3087
3201
  assert(showdown);
3088
3202
  assert(side.last_selected_move != .None);
@@ -3114,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3114
3228
  // Struggle (Pokémon Showdown would use 'move 1' here)
3115
3229
  if (n == before) {
3116
3230
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3117
- if (!showdown) {
3231
+ if (!pkmn.options.mod) {
3118
3232
  while (slot <= 4) : (slot += 1) {
3119
3233
  if (active.moves[slot - 1].pp != 0) return n;
3120
3234
  }