@pkmn/engine 0.1.0-dev.d56bdadd → 0.1.0-dev.d73cd76c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +66 -32
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +26 -4
  13. package/build/pkg/addon.js +39 -71
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/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 +29 -1
  26. package/build/pkg/protocol.js +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +84 -73
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +66 -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 +360 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +157 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +357 -135
  39. package/package.json +61 -132
  40. package/src/bin/install-pkmn-engine +47 -21
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +201 -0
  43. package/src/lib/{binding → bindings}/node.zig +15 -32
  44. package/src/lib/bindings/wasm.zig +51 -0
  45. package/src/lib/common/array.zig +70 -0
  46. package/src/lib/common/data.zig +4 -4
  47. package/src/lib/common/debug.zig +45 -29
  48. package/src/lib/common/js.zig +3 -7
  49. package/src/lib/common/optional.zig +27 -23
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +565 -552
  52. package/src/lib/common/rational.zig +340 -253
  53. package/src/lib/common/rng.zig +7 -9
  54. package/src/lib/common/util.zig +53 -1
  55. package/src/lib/gen1/calc.zig +724 -175
  56. package/src/lib/gen1/chance.zig +486 -300
  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 +3 -4
  60. package/src/lib/gen1/data.zig +29 -32
  61. package/src/lib/gen1/helpers.zig +113 -561
  62. package/src/lib/gen1/mechanics.zig +611 -488
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +111 -511
  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 +7 -5
  69. package/src/lib/gen2/data.zig +27 -29
  70. package/src/lib/gen2/generated.zig +22 -29
  71. package/src/lib/gen2/helpers.zig +68 -39
  72. package/src/lib/gen2/mechanics.zig +504 -430
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +14 -0
  75. package/src/lib/pkmn.zig +125 -29
  76. package/src/lib/wasm.zig +13 -0
  77. package/src/tools/display/dom.ts +108 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -400
  80. package/build/test/display.js.map +0 -1
  81. package/src/lib/binding/wasm.zig +0 -12
  82. package/src/lib/gen1/README.md +0 -894
  83. package/src/lib/gen2/README.md +0 -329
  84. 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,13 +82,13 @@ 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
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
87
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
95
88
 
96
- var p2_slot = findFirstAlive(p2);
89
+ const p2_slot = findFirstAlive(p2);
97
90
  assert(!showdown or p2_slot == 1);
98
- if (p2_slot == 0) return Result.Win;
91
+ if (p2_slot == 0) return .Win;
99
92
 
100
93
  try switchIn(battle, .P1, p1_slot, true, options);
101
94
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -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 .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,19 +420,19 @@ 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
437
433
  // every Pokémon must have at least one move
438
434
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
439
- return Result.Error;
435
+ return .Error;
440
436
  } else {
441
437
  auto = true;
442
438
  }
@@ -454,7 +450,26 @@ fn executeMove(
454
450
  .skip_can => skip_can = true,
455
451
  .skip_pp => skip_pp = true,
456
452
  .ok => {},
457
- .err => return @as(?Result, Result.Error),
453
+ .err => return .Error,
454
+ }
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;
458
473
  }
459
474
 
460
475
  const can = skip_can or
@@ -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
@@ -851,20 +878,20 @@ fn doMove(
851
878
  var miss = showdown and miss: {
852
879
  immune = move.target != .Self and !status and !counter and
853
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
854
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
855
882
  if (immune and move.effect != .Binding) break :miss true;
856
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
857
884
  battle.last_damage = 0;
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,9 +913,10 @@ 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)) {
891
- return @as(?Result, Result.Error);
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);
919
+ return .Error;
892
920
  }
893
921
  if (battle.last_damage == 0) {
894
922
  immune = true;
@@ -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;
@@ -971,18 +1019,18 @@ fn doMove(
971
1019
  const sub = showdown and foe.active.volatiles.Substitute;
972
1020
 
973
1021
  var nullified = false;
974
- var hit: u4 = 0;
1022
+ var hit: usize = 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()), @as(u4, @intCast(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 .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 .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,40 +1267,43 @@ 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
 
1216
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1217
1275
  const foe_last_selected_move =
1218
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1219
- .Splash
1220
- else
1221
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1222
1280
 
1223
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1224
1282
  const selected = foe_last_selected_move.bp > 0 and
1225
1283
  foe.last_selected_move != .Counter and
1226
1284
  (foe_last_selected_move.type == .Normal or
1227
- foe_last_selected_move.type == .Fighting);
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 .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.cpu.arch.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
 
@@ -1382,7 +1446,16 @@ fn moveHit(
1382
1446
  break :miss true;
1383
1447
  }
1384
1448
  if (!showdown) {
1385
- if (move.effect == .Swift) return true;
1449
+ if (move.effect == .Swift) {
1450
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1451
+ // Showdown, hence why the block above doesn't include this code), we need the
1452
+ // damage roll to be committed. However, the commit code will not save this roll if
1453
+ // the glitch occurs and the move was not marked as having hit. To work around this
1454
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1455
+ // the hit as zero to ensure it doesn't actually get saved.
1456
+ try options.chance.hit(player, true, 0);
1457
+ return true;
1458
+ }
1386
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1387
1460
  }
1388
1461
 
@@ -1401,7 +1474,7 @@ fn moveHit(
1401
1474
  const overwritten = overwrite and state > 0;
1402
1475
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1403
1476
 
1404
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1477
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1405
1478
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1406
1479
  accuracy = accuracy * boost[0] / boost[1];
1407
1480
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1413,7 +1486,7 @@ fn moveHit(
1413
1486
  if (overwrite) side.active.volatiles.state = accuracy;
1414
1487
 
1415
1488
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1416
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1489
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1417
1490
  };
1418
1491
 
1419
1492
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1424,13 +1497,17 @@ fn moveHit(
1424
1497
 
1425
1498
  if (!miss) return true;
1426
1499
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1500
+
1427
1501
  side.active.volatiles.Binding = false;
1502
+ options.chance.observe(.binding, player, .None);
1503
+
1428
1504
  return false;
1429
1505
  }
1430
1506
 
1431
1507
  fn checkFaint(
1432
1508
  battle: anytype,
1433
1509
  player: Player,
1510
+ cap: bool,
1434
1511
  options: anytype,
1435
1512
  ) @TypeOf(options.log).Error!?Result {
1436
1513
  const side = battle.side(player);
@@ -1443,21 +1520,21 @@ fn checkFaint(
1443
1520
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
1521
  const more = player_out or foe_out;
1445
1522
 
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;
1523
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1524
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1448
1525
 
1449
1526
  assert(!side.active.volatiles.MultiHit);
1450
1527
  assert(!foe.active.volatiles.MultiHit);
1451
1528
 
1452
1529
  if (player_out and foe_out) {
1453
- try options.log.tie();
1454
- return Result.Tie;
1530
+ try options.log.tie(.{});
1531
+ return .Tie;
1455
1532
  } else if (player_out) {
1456
- try options.log.win(player.foe());
1457
- return if (player == .P1) Result.Lose else Result.Win;
1533
+ try options.log.win(.{player.foe()});
1534
+ return if (player == .P1) .Lose else .Win;
1458
1535
  } else if (foe_out) {
1459
- try options.log.win(player);
1460
- return if (player == .P1) Result.Win else Result.Lose;
1536
+ try options.log.win(.{player});
1537
+ return if (player == .P1) .Win else .Lose;
1461
1538
  }
1462
1539
 
1463
1540
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1465,7 +1542,7 @@ fn checkFaint(
1465
1542
  return .{ .p1 = foe_choice, .p2 = .Switch };
1466
1543
  }
1467
1544
 
1468
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1545
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1469
1546
  var side = battle.side(player);
1470
1547
  var foe = battle.foe(player);
1471
1548
  assert(side.stored().hp == 0);
@@ -1474,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1474
1551
  assert(!foe_volatiles.MultiHit);
1475
1552
  if (foe_volatiles.Bide) {
1476
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1477
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1478
- if (foe_volatiles.state != 0) return Result.Error;
1554
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1555
+ if (foe_volatiles.state != 0) return .Error;
1479
1556
  }
1480
1557
 
1481
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1498,8 +1575,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1498
1575
  side.stored().stats.spe;
1499
1576
  }
1500
1577
 
1501
- try options.log.faint(battle.active(player), done);
1502
- options.calc.capped(player);
1578
+ try options.log.faint(.{ battle.active(player), done });
1579
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1580
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1581
+ if (cap) options.calc.capped(player);
1503
1582
  return null;
1504
1583
  }
1505
1584
 
@@ -1520,8 +1599,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1520
1599
 
1521
1600
  if (!showdown or damage > 0) {
1522
1601
  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);
1602
+ // Pokémon Showdown uses damage [of] here but its not relevant in Generation I
1603
+ try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
1525
1604
  }
1526
1605
  }
1527
1606
 
@@ -1546,14 +1625,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1546
1625
  const amount = @min(damage, stored.hp);
1547
1626
  stored.hp -= amount;
1548
1627
 
1549
- // As above, Pokémon Showdown uses damageOf but its not relevant
1550
- if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
1628
+ // As above, Pokémon Showdown uses damage [of] but its not relevant
1629
+ if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
1551
1630
 
1552
1631
  const before = foe_stored.hp;
1553
1632
  // Uncapped damage is added back to the foe
1554
1633
  foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
1555
1634
  // 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);
1635
+ if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
1557
1636
  }
1558
1637
  }
1559
1638
 
@@ -1563,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1563
1642
 
1564
1643
  battle.turn += 1;
1565
1644
 
1566
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1567
- try options.log.tie();
1568
- return Result.Tie;
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1646
+ try options.log.tie(.{});
1647
+ return .Tie;
1569
1648
  }
1570
1649
 
1571
- if (showdown and battle.turn >= 1000) {
1572
- try options.log.tie();
1573
- return Result.Tie;
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1651
+ try options.log.tie(.{});
1652
+ return .Tie;
1574
1653
  } else if (battle.turn >= 65535) {
1575
- return Result.Error;
1654
+ return .Error;
1576
1655
  }
1577
1656
 
1578
- try options.log.turn(battle.turn);
1657
+ try options.log.turn(.{battle.turn});
1579
1658
 
1580
- return Result.Default;
1659
+ return .Default;
1581
1660
  }
1582
1661
 
1583
1662
  fn checkEBC(battle: anytype) bool {
@@ -1645,7 +1724,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1645
1724
  side.last_selected_move;
1646
1725
  if (m != .None and m == last) {
1647
1726
  side.active.volatiles.Charging = false;
1648
- try options.log.disabled(ident, last);
1727
+ try options.log.cant(.{ ident, .Disable, last });
1649
1728
  return true;
1650
1729
  }
1651
1730
  }
@@ -1659,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1659
1738
  }
1660
1739
  }
1661
1740
 
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
1741
  fn onBegin(
1674
1742
  battle: anytype,
1675
1743
  player: Player,
@@ -1678,7 +1746,6 @@ fn onBegin(
1678
1746
  residual: *bool,
1679
1747
  options: anytype,
1680
1748
  ) !void {
1681
- assert(move.effect.onBegin());
1682
1749
  return switch (move.effect) {
1683
1750
  .Confusion => Effects.confusion(battle, player, move, options),
1684
1751
  .Conversion => Effects.conversion(battle, player, options),
@@ -1701,7 +1768,6 @@ fn onBegin(
1701
1768
  }
1702
1769
 
1703
1770
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1704
- assert(move.effect.onEnd());
1705
1771
  return switch (move.effect) {
1706
1772
  .Bide => Effects.bide(battle, player, options),
1707
1773
  // zig fmt: off
@@ -1723,7 +1789,6 @@ fn alwaysHappens(
1723
1789
  residual: *bool,
1724
1790
  options: anytype,
1725
1791
  ) !void {
1726
- assert(move.effect.alwaysHappens());
1727
1792
  return switch (move.effect) {
1728
1793
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1729
1794
  .Explode => Effects.explode(battle, player),
@@ -1759,10 +1824,12 @@ pub const Effects = struct {
1759
1824
 
1760
1825
  side.active.volatiles.Bide = true;
1761
1826
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1827
+
1762
1828
  side.active.volatiles.state = 0;
1763
1829
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1830
+ options.chance.observe(.attacking, player, .started);
1764
1831
 
1765
- try options.log.start(battle.active(player), .Bide);
1832
+ try options.log.start(.{ battle.active(player), .Bide });
1766
1833
  }
1767
1834
 
1768
1835
  fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1776,11 +1843,11 @@ pub const Effects = struct {
1776
1843
  // GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
1777
1844
  if (Status.is(foe_stored.status, .FRZ)) {
1778
1845
  assert(move.type == .Fire);
1779
- try options.log.curestatus(
1846
+ try options.log.curestatus(.{
1780
1847
  battle.active(player.foe()),
1781
1848
  foe_stored.status,
1782
1849
  .Message,
1783
- );
1850
+ });
1784
1851
  foe_stored.status = 0;
1785
1852
  }
1786
1853
  return;
@@ -1794,7 +1861,7 @@ pub const Effects = struct {
1794
1861
  foe_stored.status = Status.init(.BRN);
1795
1862
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
1796
1863
 
1797
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
1864
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
1798
1865
  }
1799
1866
 
1800
1867
  fn charge(battle: anytype, player: Player, options: anytype) !void {
@@ -1804,8 +1871,8 @@ pub const Effects = struct {
1804
1871
  volatiles.Charging = true;
1805
1872
  const move = side.last_selected_move;
1806
1873
  if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
1807
- try options.log.laststill();
1808
- try options.log.prepare(battle.active(player), move);
1874
+ try options.log.laststill(.{});
1875
+ try options.log.prepare(.{ battle.active(player), move });
1809
1876
  }
1810
1877
 
1811
1878
  fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1816,42 +1883,39 @@ pub const Effects = struct {
1816
1883
  if (!try Rolls.confusionChance(battle, player, options)) return;
1817
1884
  } else {
1818
1885
  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
- }
1886
+ if (!try checkHit(battle, player, move, options)) return;
1887
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1888
  } 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
- }
1889
+ if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1890
+ if (!try checkHit(battle, player, move, options)) return;
1830
1891
  }
1831
- try options.chance.commit(player, .hit);
1832
1892
  }
1833
1893
 
1834
1894
  if (foe.active.volatiles.Confusion) return;
1835
1895
  foe.active.volatiles.Confusion = true;
1836
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1837
1896
 
1838
- try options.log.start(battle.active(player.foe()), .Confusion);
1897
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1898
+ options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1900
+
1901
+ try options.log.start(.{ battle.active(player.foe()), .Confusion });
1839
1902
  }
1840
1903
 
1841
1904
  fn conversion(battle: anytype, player: Player, options: anytype) !void {
1842
1905
  const foe = battle.foe(player);
1843
1906
 
1844
1907
  if (foe.active.volatiles.Invulnerable) {
1845
- try options.log.lastmiss();
1846
- return options.log.miss(battle.active(player));
1908
+ try options.log.lastmiss(.{});
1909
+ return options.log.miss(.{battle.active(player)});
1847
1910
  }
1848
1911
 
1849
1912
  battle.side(player).active.types = foe.active.types;
1850
- return options.log.typechange(
1913
+ return options.log.start(.{
1851
1914
  battle.active(player),
1915
+ .TypeChange,
1852
1916
  foe.active.types,
1853
1917
  battle.active(player.foe()),
1854
- );
1918
+ });
1855
1919
  }
1856
1920
 
1857
1921
  fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
@@ -1860,13 +1924,10 @@ pub const Effects = struct {
1860
1924
  const foe_ident = battle.active(player.foe());
1861
1925
 
1862
1926
  // 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
- }
1927
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1867
1928
 
1868
1929
  if (volatiles.disable_move != 0) {
1869
- try options.log.fail(foe_ident, .None);
1930
+ try options.log.fail(.{ foe_ident, .None });
1870
1931
  return null;
1871
1932
  }
1872
1933
 
@@ -1881,26 +1942,27 @@ pub const Effects = struct {
1881
1942
 
1882
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1883
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1884
- if (n == 0) {
1885
- try options.log.fail(foe_ident, .None);
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1946
+ try options.log.fail(.{ foe_ident, .None });
1886
1947
  return null;
1887
1948
  } else if (err) {
1888
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1889
- assert(!showdown);
1890
- return Result.Error;
1950
+ return .Error;
1891
1951
  }
1892
1952
 
1893
1953
  volatiles.disable_move =
1894
1954
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1895
1955
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1956
+ options.chance.observe(.disable, player.foe(), .started);
1896
1957
 
1897
1958
  const id = foe.active.move(volatiles.disable_move).id;
1898
- try options.log.startEffect(foe_ident, .Disable, id);
1959
+ try options.log.start(.{ foe_ident, .Disable, id });
1899
1960
  return null;
1900
1961
  }
1901
1962
 
1902
1963
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1903
1964
  var stored = battle.side(player).stored();
1965
+ const ident = battle.active(player);
1904
1966
 
1905
1967
  if (battle.last_damage == 0) {
1906
1968
  assert(showdown);
@@ -1913,7 +1975,7 @@ pub const Effects = struct {
1913
1975
  if (stored.hp == stored.stats.hp) return;
1914
1976
  stored.hp = @min(stored.stats.hp, stored.hp + drain);
1915
1977
 
1916
- try options.log.drain(battle.active(player), stored, battle.active(player.foe()));
1978
+ try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
1917
1979
  }
1918
1980
 
1919
1981
  fn explode(battle: anytype, player: Player) !void {
@@ -1945,7 +2007,7 @@ pub const Effects = struct {
1945
2007
  if (side.active.volatiles.FocusEnergy) return;
1946
2008
  side.active.volatiles.FocusEnergy = true;
1947
2009
 
1948
- try options.log.start(battle.active(player), .FocusEnergy);
2010
+ try options.log.start(.{ battle.active(player), .FocusEnergy });
1949
2011
  }
1950
2012
 
1951
2013
  fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -1962,12 +2024,12 @@ pub const Effects = struct {
1962
2024
  if (foe.active.types.includes(move.type)) return;
1963
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1964
2026
  // Freeze Clause Mod
1965
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2027
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1966
2028
 
1967
2029
  foe_stored.status = Status.init(.FRZ);
1968
2030
  // GLITCH: Hyper Beam recharging status is not cleared
1969
2031
 
1970
- try options.log.status(foe_ident, foe_stored.status, .None);
2032
+ try options.log.status(.{ foe_ident, foe_stored.status, .None });
1971
2033
  }
1972
2034
 
1973
2035
  fn haze(battle: anytype, player: Player, options: anytype) !void {
@@ -1975,7 +2037,7 @@ pub const Effects = struct {
1975
2037
  var side = battle.side(player);
1976
2038
  var foe = battle.foe(player);
1977
2039
 
1978
- var side_stored = side.stored();
2040
+ const side_stored = side.stored();
1979
2041
  var foe_stored = foe.stored();
1980
2042
 
1981
2043
  const player_ident = battle.active(player);
@@ -1987,8 +2049,8 @@ pub const Effects = struct {
1987
2049
  side.active.stats = side_stored.stats;
1988
2050
  foe.active.stats = foe_stored.stats;
1989
2051
 
1990
- try log.activate(player_ident, .Haze);
1991
- try log.clearallboost();
2052
+ try log.activate(.{ player_ident, .Haze });
2053
+ try log.clearallboost(.{});
1992
2054
 
1993
2055
  // Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
1994
2056
  if (showdown) {
@@ -1996,24 +2058,26 @@ pub const Effects = struct {
1996
2058
  const p: Player = @enumFromInt(i);
1997
2059
  // Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
1998
2060
  if (p != player and Status.any(s.stored().status)) {
1999
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2061
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2062
  s.stored().status = 0;
2063
+ options.chance.observe(.sleep, p, .None);
2001
2064
  } else if (showdown and s.stored().status == Status.TOX) {
2002
2065
  s.stored().status = Status.init(.PSN);
2003
- try log.status(battle.active(p), s.stored().status, .None);
2066
+ try log.status(.{ battle.active(p), s.stored().status, .None });
2004
2067
  }
2005
- try clearVolatiles(battle, p, p != player, options);
2068
+ try clearVolatiles(battle, p, options);
2006
2069
  }
2007
2070
  } else {
2008
2071
  if (Status.any(foe_stored.status)) {
2009
2072
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2010
2073
  foe.last_selected_move = .SKIP_TURN;
2074
+ options.chance.observe(.sleep, player.foe(), .None);
2011
2075
  }
2012
- try log.curestatus(foe_ident, foe_stored.status, .Silent);
2076
+ try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2013
2077
  foe_stored.status = 0;
2014
2078
  }
2015
- try clearVolatiles(battle, player, false, options);
2016
- try clearVolatiles(battle, player.foe(), true, options);
2079
+ try clearVolatiles(battle, player, options);
2080
+ try clearVolatiles(battle, player.foe(), options);
2017
2081
  }
2018
2082
  }
2019
2083
 
@@ -2025,7 +2089,7 @@ pub const Effects = struct {
2025
2089
  // GLITCH: HP recovery move failure glitches
2026
2090
  const delta = stored.stats.hp - stored.hp;
2027
2091
  if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
2028
- return options.log.fail(ident, .None);
2092
+ return options.log.fail(.{ ident, .None });
2029
2093
  }
2030
2094
 
2031
2095
  const rest = side.last_selected_move == .Rest;
@@ -2033,17 +2097,17 @@ pub const Effects = struct {
2033
2097
  // Adding the sleep status runs the sleep condition handler to roll duration
2034
2098
  if (showdown) battle.rng.advance(1);
2035
2099
  stored.status = Status.slf(2);
2036
- try options.log.statusFrom(ident, stored.status, Move.Rest);
2100
+ try options.log.status(.{ ident, stored.status, .From, Move.Rest });
2037
2101
  stored.hp = stored.stats.hp;
2038
2102
  } else {
2039
2103
  stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
2040
2104
  }
2041
- try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
2105
+ try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
2042
2106
  }
2043
2107
 
2044
2108
  fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
2045
2109
  battle.side(player).active.volatiles.Recharging = true;
2046
- try options.log.mustrecharge(battle.active(player));
2110
+ try options.log.mustrecharge(.{battle.active(player)});
2047
2111
  }
2048
2112
 
2049
2113
  fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2052,33 +2116,31 @@ pub const Effects = struct {
2052
2116
  if (showdown) {
2053
2117
  // Invulnerability trumps type immunity on Pokémon Showdown
2054
2118
  if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
2055
- return options.log.immune(battle.active(player.foe()), .None);
2119
+ return options.log.immune(.{ battle.active(player.foe()), .None });
2056
2120
  }
2057
2121
  if (!try checkHit(battle, player, move, options)) return;
2058
2122
  if (foe.active.volatiles.LeechSeed) return;
2059
2123
  } else {
2060
2124
  if (!try checkHit(battle, player, move, options)) return;
2061
2125
  if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
2062
- try options.log.lastmiss();
2063
- return options.log.miss(battle.active(player));
2126
+ try options.log.lastmiss(.{});
2127
+ return options.log.miss(.{battle.active(player)});
2064
2128
  }
2065
2129
  }
2066
2130
 
2067
- try options.chance.commit(player, .hit);
2068
2131
  foe.active.volatiles.LeechSeed = true;
2069
2132
 
2070
- try options.log.start(battle.active(player.foe()), .LeechSeed);
2133
+ try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
2071
2134
  }
2072
2135
 
2073
2136
  fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
2074
2137
  var side = battle.side(player);
2138
+ const ident = battle.active(player);
2075
2139
 
2076
- if (side.active.volatiles.LightScreen) {
2077
- return options.log.fail(battle.active(player), .None);
2078
- }
2140
+ if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
2079
2141
  side.active.volatiles.LightScreen = true;
2080
2142
 
2081
- try options.log.start(battle.active(player), .LightScreen);
2143
+ try options.log.start(.{ ident, .LightScreen });
2082
2144
  }
2083
2145
 
2084
2146
  fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
@@ -2106,17 +2168,15 @@ pub const Effects = struct {
2106
2168
  if (!has_mimic) {
2107
2169
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2108
2170
  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);
2171
+ return options.log.fail(.{ battle.active(player.foe()), .None });
2111
2172
  }
2112
2173
  }
2113
2174
  if (!try checkHit(battle, player, move, options)) return;
2114
- try options.chance.commit(player, .hit);
2115
2175
 
2116
2176
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2117
2177
  side.active.move(oslot).id = foe.active.move(rslot).id;
2118
2178
 
2119
- try options.log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
2179
+ try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
2120
2180
  }
2121
2181
 
2122
2182
  fn mist(battle: anytype, player: Player, options: anytype) !void {
@@ -2125,7 +2185,7 @@ pub const Effects = struct {
2125
2185
  if (side.active.volatiles.Mist) return;
2126
2186
  side.active.volatiles.Mist = true;
2127
2187
 
2128
- try options.log.start(battle.active(player), .Mist);
2188
+ try options.log.start(.{ battle.active(player), .Mist });
2129
2189
  }
2130
2190
 
2131
2191
  fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2151,25 +2211,25 @@ pub const Effects = struct {
2151
2211
 
2152
2212
  if (showdown) {
2153
2213
  // 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;
2214
+ if (immune and !foe.active.volatiles.Invulnerable) {
2215
+ return log.immune(.{ foe_ident, .None });
2216
+ } else if (!try checkHit(battle, player, move, options)) return;
2156
2217
  }
2157
2218
  if (Status.any(foe_stored.status)) {
2158
- return log.fail(
2219
+ return log.fail(.{
2159
2220
  foe_ident,
2160
- if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
2161
- );
2221
+ if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
2222
+ });
2162
2223
  }
2163
2224
  if (!showdown) {
2164
- if (immune) return log.immune(foe_ident, .None);
2225
+ if (immune) return log.immune(.{ foe_ident, .None });
2165
2226
  if (!try checkHit(battle, player, move, options)) return;
2166
2227
  }
2167
- try options.chance.commit(player, .hit);
2168
2228
 
2169
2229
  foe_stored.status = Status.init(.PAR);
2170
2230
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2171
2231
 
2172
- try log.status(foe_ident, foe_stored.status, .None);
2232
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2173
2233
  }
2174
2234
 
2175
2235
  fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2191,12 +2251,12 @@ pub const Effects = struct {
2191
2251
  foe_stored.status = Status.init(.PAR);
2192
2252
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
2193
2253
 
2194
- try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
2254
+ try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
2195
2255
  }
2196
2256
 
2197
2257
  fn payDay(battle: anytype, player: Player, options: anytype) !void {
2198
2258
  if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
2199
- try options.log.fieldactivate();
2259
+ try options.log.fieldactivate(.{});
2200
2260
  }
2201
2261
  }
2202
2262
 
@@ -2211,28 +2271,27 @@ pub const Effects = struct {
2211
2271
  return;
2212
2272
  } else if (foe.active.volatiles.Substitute) {
2213
2273
  if (move.effect != .Poison) return;
2214
- return log.fail(foe_ident, .None);
2274
+ return log.fail(.{ foe_ident, .None });
2215
2275
  } else if (Status.any(foe_stored.status)) {
2216
2276
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2217
2277
  // Pokémon Showdown considers Toxic to be a status even in Generation I and so
2218
2278
  // 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))
2279
+ return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
2220
2280
  if (!showdown)
2221
- .Poison
2281
+ Fail.Poison
2222
2282
  else if (toxic == (foe_stored.status == Status.TOX))
2223
- if (toxic) .Toxic else .Poison
2283
+ if (toxic) Fail.Toxic else Fail.Poison
2224
2284
  else
2225
- .None
2285
+ Fail.None
2226
2286
  else
2227
- .None);
2287
+ Fail.None });
2228
2288
  } else if (foe.active.types.includes(.Poison)) {
2229
2289
  if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
2230
- return log.immune(foe_ident, .None);
2290
+ return log.immune(.{ foe_ident, .None });
2231
2291
  }
2232
2292
 
2233
2293
  if (move.effect == .Poison) {
2234
2294
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2235
- try options.chance.commit(player, .hit);
2236
2295
  } else {
2237
2296
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2238
2297
  return;
@@ -2246,7 +2305,7 @@ pub const Effects = struct {
2246
2305
  foe.active.volatiles.toxic = 0;
2247
2306
  }
2248
2307
 
2249
- try log.status(foe_ident, foe_stored.status, .None);
2308
+ try log.status(.{ foe_ident, foe_stored.status, .None });
2250
2309
  }
2251
2310
 
2252
2311
  fn rage(battle: anytype, player: Player) !void {
@@ -2266,22 +2325,23 @@ pub const Effects = struct {
2266
2325
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2267
2326
  stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
2268
2327
 
2269
- try options.log.damageOf(
2328
+ try options.log.damage(.{
2270
2329
  battle.active(player),
2271
2330
  stored,
2272
2331
  .RecoilOf,
2273
2332
  battle.active(player.foe()),
2274
- );
2333
+ });
2275
2334
  if (showdown and stored.hp == 0) residual.* = false;
2276
2335
  }
2277
2336
 
2278
2337
  fn reflect(battle: anytype, player: Player, options: anytype) !void {
2279
2338
  var side = battle.side(player);
2339
+ const ident = battle.active(player);
2280
2340
 
2281
- if (side.active.volatiles.Reflect) return options.log.fail(battle.active(player), .None);
2341
+ if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
2282
2342
  side.active.volatiles.Reflect = true;
2283
2343
 
2284
- try options.log.start(battle.active(player), .Reflect);
2344
+ try options.log.start(.{ ident, .Reflect });
2285
2345
  }
2286
2346
 
2287
2347
  fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2295,18 +2355,17 @@ pub const Effects = struct {
2295
2355
  // the Sleep Clause Mod might activate, causing us to not actually bypass
2296
2356
  } else {
2297
2357
  if (Status.any(foe_stored.status)) {
2298
- return options.log.fail(
2358
+ return options.log.fail(.{
2299
2359
  foe_ident,
2300
- if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
2301
- );
2360
+ if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2361
+ });
2302
2362
  }
2303
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2363
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2304
2364
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2305
- try options.chance.commit(player, .hit);
2306
2365
  }
2307
2366
 
2308
2367
  // Sleep Clause Mod
2309
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2310
2369
  for (foe.pokemon) |p| {
2311
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2312
2371
  }
@@ -2314,18 +2373,20 @@ pub const Effects = struct {
2314
2373
  foe.active.volatiles.Recharging = false;
2315
2374
 
2316
2375
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2376
+ options.chance.observe(.sleep, player.foe(), .started);
2377
+
2317
2378
  const last = battle.side(player).last_selected_move;
2318
- try options.log.statusFrom(foe_ident, foe_stored.status, last);
2379
+ try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2319
2380
  }
2320
2381
 
2321
2382
  fn splash(battle: anytype, player: Player, options: anytype) !void {
2322
- try options.log.activate(battle.active(player), .Splash);
2383
+ try options.log.activate(.{ battle.active(player), .Splash });
2323
2384
  }
2324
2385
 
2325
2386
  fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
2326
2387
  var side = battle.side(player);
2327
2388
  if (side.active.volatiles.Substitute) {
2328
- return options.log.fail(battle.active(player), .Substitute);
2389
+ return options.log.fail(.{ battle.active(player), .Substitute });
2329
2390
  }
2330
2391
 
2331
2392
  assert(side.stored().stats.hp <= 1023);
@@ -2336,15 +2397,17 @@ pub const Effects = struct {
2336
2397
  // the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
2337
2398
  // avoid having to convert to floating point)
2338
2399
  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);
2400
+ if (side.stored().hp < required_hp) {
2401
+ return options.log.fail(.{ battle.active(player), .Weak });
2402
+ }
2340
2403
 
2341
2404
  // GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
2342
2405
  side.stored().hp -= hp;
2343
2406
  side.active.volatiles.substitute = hp + 1;
2344
2407
  side.active.volatiles.Substitute = true;
2345
- try options.log.start(battle.active(player), .Substitute);
2408
+ try options.log.start(.{ battle.active(player), .Substitute });
2346
2409
  if (hp > 0) {
2347
- try options.log.damage(battle.active(player), side.stored(), .None);
2410
+ try options.log.damage(.{ battle.active(player), side.stored(), .None });
2348
2411
  if (showdown and side.stored().hp == 0) residual.* = false;
2349
2412
  }
2350
2413
  }
@@ -2353,13 +2416,11 @@ pub const Effects = struct {
2353
2416
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2417
  if (showdown) {
2355
2418
  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
- }
2419
+ _ = try checkHit(battle, player, move, options);
2359
2420
  battle.last_damage = 0;
2360
2421
  } else {
2361
- try options.log.fail(battle.active(player), .None);
2362
- try options.log.laststill();
2422
+ try options.log.fail(.{ battle.active(player), .None });
2423
+ try options.log.laststill(.{});
2363
2424
  }
2364
2425
  }
2365
2426
 
@@ -2370,6 +2431,8 @@ pub const Effects = struct {
2370
2431
 
2371
2432
  volatiles.Thrashing = true;
2372
2433
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2434
+
2435
+ options.chance.observe(.attacking, player, .started);
2373
2436
  }
2374
2437
 
2375
2438
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2398,7 +2461,7 @@ pub const Effects = struct {
2398
2461
  side.active.moves[i].pp = if (m.id != .None) 5 else 0;
2399
2462
  }
2400
2463
 
2401
- try options.log.transform(battle.active(player), foe_ident);
2464
+ try options.log.transform(.{ battle.active(player), foe_ident });
2402
2465
  }
2403
2466
 
2404
2467
  fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
@@ -2408,13 +2471,14 @@ pub const Effects = struct {
2408
2471
  if (side.active.volatiles.Binding) return;
2409
2472
  side.active.volatiles.Binding = true;
2410
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2411
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2412
2475
  if (!showdown) {
2413
2476
  foe.active.volatiles.Recharging = false;
2414
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2415
2478
 
2416
2479
  side.active.volatiles.attacks =
2417
2480
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2481
+ options.chance.observe(.binding, player, .started);
2418
2482
  }
2419
2483
 
2420
2484
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2428,87 +2492,87 @@ pub const Effects = struct {
2428
2492
  switch (move.effect) {
2429
2493
  .AttackUp1, .AttackUp2, .Rage => {
2430
2494
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2431
- if (boosts.atk == 6) return log.fail(ident, .None);
2495
+ if (boosts.atk == 6) return log.fail(.{ ident, .None });
2432
2496
  const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
2433
2497
  boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
2434
- const reason = if (move.effect == .Rage) Boost.Rage else Boost.Attack;
2498
+ const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
2435
2499
  if (stats.atk == MAX_STAT_VALUE) {
2436
2500
  boosts.atk -= 1;
2437
2501
  if (showdown) {
2438
- try log.boost(ident, reason, n);
2439
- try log.boost(ident, Boost.Attack, -1);
2502
+ try log.boost(.{ ident, reason, n });
2503
+ try log.boost(.{ ident, .Attack, -1 });
2440
2504
  if (move.effect == .Rage) return;
2441
2505
  }
2442
- return log.fail(ident, .None);
2506
+ return log.fail(.{ ident, .None });
2443
2507
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2508
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2509
  const stat = unmodifiedStats(battle, side).atk;
2446
2510
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
- try log.boost(ident, reason, n);
2511
+ try log.boost(.{ ident, reason, n });
2448
2512
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2449
2513
  if (showdown and move.effect == .Rage) return;
2450
2514
  },
2451
2515
  .DefenseUp1, .DefenseUp2 => {
2452
2516
  assert(boosts.def >= -6 and boosts.def <= 6);
2453
- if (boosts.def == 6) return log.fail(ident, .None);
2517
+ if (boosts.def == 6) return log.fail(.{ ident, .None });
2454
2518
  const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
2455
2519
  boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
2456
2520
  if (stats.def == MAX_STAT_VALUE) {
2457
2521
  boosts.def -= 1;
2458
2522
  if (showdown) {
2459
- try log.boost(ident, .Defense, n);
2460
- try log.boost(ident, .Defense, -1);
2523
+ try log.boost(.{ ident, .Defense, n });
2524
+ try log.boost(.{ ident, .Defense, -1 });
2461
2525
  }
2462
- return log.fail(ident, .None);
2526
+ return log.fail(.{ ident, .None });
2463
2527
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2528
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2529
  const stat = unmodifiedStats(battle, side).def;
2466
2530
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
- try log.boost(ident, .Defense, n);
2531
+ try log.boost(.{ ident, .Defense, n });
2468
2532
  },
2469
2533
  .SpeedUp2 => {
2470
2534
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2471
- if (boosts.spe == 6) return log.fail(ident, .None);
2535
+ if (boosts.spe == 6) return log.fail(.{ ident, .None });
2472
2536
  boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
2473
2537
  if (stats.spe == MAX_STAT_VALUE) {
2474
2538
  boosts.spe -= 1;
2475
2539
  if (showdown) {
2476
- try log.boost(ident, .Speed, 2);
2477
- try log.boost(ident, .Speed, -1);
2540
+ try log.boost(.{ ident, .Speed, 2 });
2541
+ try log.boost(.{ ident, .Speed, -1 });
2478
2542
  }
2479
- return log.fail(ident, .None);
2543
+ return log.fail(.{ ident, .None });
2480
2544
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2545
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2546
  const stat = unmodifiedStats(battle, side).spe;
2483
2547
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
- try log.boost(ident, .Speed, 2);
2548
+ try log.boost(.{ ident, .Speed, 2 });
2485
2549
  },
2486
2550
  .SpecialUp1, .SpecialUp2 => {
2487
2551
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2488
- if (boosts.spc == 6) return log.fail(ident, .None);
2552
+ if (boosts.spc == 6) return log.fail(.{ ident, .None });
2489
2553
  const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
2490
2554
  boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
2491
2555
  if (stats.spc == MAX_STAT_VALUE) {
2492
2556
  boosts.spc -= 1;
2493
2557
  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);
2558
+ try log.boost(.{ ident, .SpecialAttack, n });
2559
+ try log.boost(.{ ident, .SpecialAttack, -1 });
2560
+ try log.boost(.{ ident, .SpecialDefense, n });
2561
+ try log.boost(.{ ident, .SpecialDefense, -1 });
2498
2562
  }
2499
- return log.fail(ident, .None);
2563
+ return log.fail(.{ ident, .None });
2500
2564
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2565
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2566
  const stat = unmodifiedStats(battle, side).spc;
2503
2567
  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);
2568
+ try log.boost(.{ ident, .SpecialAttack, n });
2569
+ try log.boost(.{ ident, .SpecialDefense, n });
2506
2570
  },
2507
2571
  .EvasionUp1 => {
2508
2572
  assert(boosts.evasion >= -6 and boosts.evasion <= 6);
2509
- if (boosts.evasion == 6) return log.fail(ident, .None);
2573
+ if (boosts.evasion == 6) return log.fail(.{ ident, .None });
2510
2574
  boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
2511
- try log.boost(ident, .Evasion, 1);
2575
+ try log.boost(.{ ident, .Evasion, 1 });
2512
2576
  },
2513
2577
  else => unreachable,
2514
2578
  }
@@ -2523,7 +2587,7 @@ pub const Effects = struct {
2523
2587
  const foe_ident = battle.active(player.foe());
2524
2588
  const secondary = move.effect.isStatDownChance();
2525
2589
 
2526
- if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2590
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
2527
2591
 
2528
2592
  if (secondary) {
2529
2593
  const proc = try Rolls.unboost(battle, player, options);
@@ -2531,7 +2595,6 @@ pub const Effects = struct {
2531
2595
  } else {
2532
2596
  // checkHit already checks for Invulnerable
2533
2597
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2598
  }
2536
2599
 
2537
2600
  var stats = &foe.active.stats;
@@ -2541,82 +2604,82 @@ pub const Effects = struct {
2541
2604
  switch (move.effect) {
2542
2605
  .AttackDown1, .AttackDownChance => {
2543
2606
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2544
- if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2607
+ if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2545
2608
  boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
2546
2609
  if (stats.atk == 1) {
2547
2610
  boosts.atk += 1;
2548
2611
  if (showdown) {
2549
- try log.boost(foe_ident, .Attack, -1);
2550
- try log.boost(foe_ident, .Attack, 1);
2612
+ try log.boost(.{ foe_ident, .Attack, -1 });
2613
+ try log.boost(.{ foe_ident, .Attack, 1 });
2551
2614
  }
2552
- return log.fail(foe_ident, .None);
2615
+ return log.fail(.{ foe_ident, .None });
2553
2616
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2617
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2618
  const stat = unmodifiedStats(battle, foe).atk;
2556
2619
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
- try log.boost(foe_ident, .Attack, -1);
2620
+ try log.boost(.{ foe_ident, .Attack, -1 });
2558
2621
  },
2559
2622
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2560
2623
  assert(boosts.def >= -6 and boosts.def <= 6);
2561
- if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2624
+ if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2562
2625
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2563
2626
  boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
2564
2627
  if (stats.def == 1) {
2565
2628
  boosts.def += 1;
2566
2629
  if (showdown) {
2567
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2568
- try log.boost(foe_ident, .Defense, 1);
2630
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2631
+ try log.boost(.{ foe_ident, .Defense, 1 });
2569
2632
  }
2570
- return log.fail(foe_ident, .None);
2633
+ return log.fail(.{ foe_ident, .None });
2571
2634
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2635
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2636
  const stat = unmodifiedStats(battle, foe).def;
2574
2637
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
- try log.boost(foe_ident, .Defense, -@as(i8, n));
2638
+ try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
2576
2639
  },
2577
2640
  .SpeedDown1, .SpeedDownChance => {
2578
2641
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2579
- if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2642
+ if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2580
2643
  boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
2581
2644
  if (stats.spe == 1) {
2582
2645
  boosts.spe += 1;
2583
2646
  if (showdown) {
2584
- try log.boost(foe_ident, .Speed, -1);
2585
- try log.boost(foe_ident, .Speed, 1);
2647
+ try log.boost(.{ foe_ident, .Speed, -1 });
2648
+ try log.boost(.{ foe_ident, .Speed, 1 });
2586
2649
  }
2587
- return log.fail(foe_ident, .None);
2650
+ return log.fail(.{ foe_ident, .None });
2588
2651
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2652
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2653
  const stat = unmodifiedStats(battle, foe).spe;
2591
2654
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
- try log.boost(foe_ident, .Speed, -1);
2655
+ try log.boost(.{ foe_ident, .Speed, -1 });
2593
2656
  assert(boosts.spe >= -6);
2594
2657
  },
2595
2658
  .SpecialDownChance => {
2596
2659
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2597
- if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2660
+ if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2598
2661
  boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
2599
2662
  if (stats.spc == 1) {
2600
2663
  boosts.spc += 1;
2601
2664
  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);
2665
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2666
+ try log.boost(.{ foe_ident, .SpecialAttack, 1 });
2667
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2668
+ try log.boost(.{ foe_ident, .SpecialDefense, 1 });
2606
2669
  }
2607
- return log.fail(foe_ident, .None);
2670
+ return log.fail(.{ foe_ident, .None });
2608
2671
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2672
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2673
  const stat = unmodifiedStats(battle, foe).spc;
2611
2674
  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);
2675
+ try log.boost(.{ foe_ident, .SpecialAttack, -1 });
2676
+ try log.boost(.{ foe_ident, .SpecialDefense, -1 });
2614
2677
  },
2615
2678
  .AccuracyDown1 => {
2616
2679
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2617
- if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2680
+ if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
2618
2681
  boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
2619
- try log.boost(foe_ident, .Accuracy, -1);
2682
+ try log.boost(.{ foe_ident, .Accuracy, -1 });
2620
2683
  },
2621
2684
  else => unreachable,
2622
2685
  }
@@ -2645,35 +2708,35 @@ fn isForced(active: anytype) bool {
2645
2708
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2709
  }
2647
2710
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2711
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2712
  var log = options.log;
2650
2713
  var side = battle.side(who);
2651
2714
  var volatiles = &side.active.volatiles;
2652
2715
  const ident = battle.active(who);
2653
2716
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2717
  if (volatiles.disable_move != 0) {
2657
2718
  volatiles.disable_move = 0;
2658
2719
  volatiles.disable_duration = 0;
2659
- try log.end(ident, .DisableSilent);
2720
+ options.chance.observe(.disable, who, .None);
2721
+ try log.end(.{ ident, .DisableSilent });
2660
2722
  }
2661
2723
  if (volatiles.Confusion) {
2662
2724
  // volatiles.confusion is left unchanged
2663
2725
  volatiles.Confusion = false;
2664
- try log.end(ident, .ConfusionSilent);
2726
+ options.chance.observe(.confusion, who, .None);
2727
+ try log.end(.{ ident, .ConfusionSilent });
2665
2728
  }
2666
2729
  if (volatiles.Mist) {
2667
2730
  volatiles.Mist = false;
2668
- try log.end(ident, .MistSilent);
2731
+ try log.end(.{ ident, .MistSilent });
2669
2732
  }
2670
2733
  if (volatiles.FocusEnergy) {
2671
2734
  volatiles.FocusEnergy = false;
2672
- try log.end(ident, .FocusEnergySilent);
2735
+ try log.end(.{ ident, .FocusEnergySilent });
2673
2736
  }
2674
2737
  if (volatiles.LeechSeed) {
2675
2738
  volatiles.LeechSeed = false;
2676
- try log.end(ident, .LeechSeedSilent);
2739
+ try log.end(.{ ident, .LeechSeedSilent });
2677
2740
  }
2678
2741
  if (!showdown and volatiles.Toxic) {
2679
2742
  volatiles.Toxic = false;
@@ -2681,24 +2744,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2681
2744
  }
2682
2745
  if (volatiles.LightScreen) {
2683
2746
  volatiles.LightScreen = false;
2684
- try log.end(ident, .LightScreenSilent);
2747
+ try log.end(.{ ident, .LightScreenSilent });
2685
2748
  }
2686
2749
  if (volatiles.Reflect) {
2687
2750
  volatiles.Reflect = false;
2688
- try log.end(ident, .ReflectSilent);
2751
+ try log.end(.{ ident, .ReflectSilent });
2689
2752
  }
2690
2753
  if (showdown and volatiles.Toxic) {
2691
2754
  volatiles.Toxic = false;
2692
2755
  // Pokémon Showdown erroneously clears the toxic counter
2693
2756
  volatiles.toxic = 0;
2694
- try log.end(ident, .ToxicSilent);
2757
+ try log.end(.{ ident, .ToxicSilent });
2695
2758
  }
2696
2759
  }
2697
2760
 
2761
+ fn decrement(
2762
+ comptime field: chance.Action.Field,
2763
+ player: Player,
2764
+ options: anytype,
2765
+ n: anytype,
2766
+ ) @TypeOf(n) {
2767
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2768
+ .started, .ended => 0,
2769
+ .continuing => if (n > 1) n - 1 else n,
2770
+ else => unreachable,
2771
+ } else n - 1;
2772
+ }
2773
+
2698
2774
  pub const Rolls = struct {
2699
2775
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2776
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2777
  player == .P1
2778
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2779
+ battle.rng.speedTie()
2702
2780
  else if (showdown)
2703
2781
  battle.rng.range(u8, 0, 2) == 0
2704
2782
  else
@@ -2708,21 +2786,31 @@ pub const Rolls = struct {
2708
2786
  return p1;
2709
2787
  }
2710
2788
 
2711
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2789
+ fn criticalHit(
2790
+ battle: anytype,
2791
+ player: Player,
2792
+ rate: u8,
2793
+ spurious: bool,
2794
+ options: anytype,
2795
+ ) !bool {
2712
2796
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2797
  val == .true
2798
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2799
+ battle.rng.criticalHit(player, rate)
2714
2800
  else if (showdown)
2715
2801
  battle.rng.chance(u8, rate, 256)
2716
2802
  else
2717
2803
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2804
 
2719
- try options.chance.criticalHit(player, crit, rate);
2805
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2806
  return crit;
2721
2807
  }
2722
2808
 
2723
2809
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2810
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2811
  val
2812
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2813
+ battle.rng.damage(player)
2726
2814
  else roll: {
2727
2815
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2816
  while (true) {
@@ -2736,21 +2824,27 @@ pub const Rolls = struct {
2736
2824
  return roll;
2737
2825
  }
2738
2826
 
2739
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2827
+ const uN = if (pkmn.options.miss) u8 else u9;
2828
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2829
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2740
2830
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2831
  val == .true
2832
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2833
+ battle.rng.hit(player, acc)
2742
2834
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2835
+ battle.rng.chance(uN, acc, 256)
2744
2836
  else
2745
- battle.rng.next() < accuracy;
2837
+ battle.rng.next() < acc;
2746
2838
 
2747
- options.chance.hit(ok, accuracy);
2839
+ try options.chance.hit(player, ok, acc);
2748
2840
  return ok;
2749
2841
  }
2750
2842
 
2751
2843
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2844
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2845
  val == .true
2846
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2847
+ battle.rng.confused(player)
2754
2848
  else if (showdown)
2755
2849
  !battle.rng.chance(u8, 128, 256)
2756
2850
  else
@@ -2763,6 +2857,8 @@ pub const Rolls = struct {
2763
2857
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2858
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2859
  val == .true
2860
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2861
+ battle.rng.paralyzed(player)
2766
2862
  else if (showdown)
2767
2863
  battle.rng.chance(u8, 63, 256)
2768
2864
  else
@@ -2775,6 +2871,8 @@ pub const Rolls = struct {
2775
2871
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2872
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2873
  val == .true
2874
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2875
+ battle.rng.secondaryChance(player, 25)
2778
2876
  else if (showdown)
2779
2877
  battle.rng.chance(u8, 25, 256)
2780
2878
  else
@@ -2789,6 +2887,8 @@ pub const Rolls = struct {
2789
2887
 
2790
2888
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2889
  val == .true
2890
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2891
+ battle.rng.secondaryChance(player, rate)
2792
2892
  else if (showdown)
2793
2893
  battle.rng.chance(u8, rate, 256)
2794
2894
  else
@@ -2803,6 +2903,8 @@ pub const Rolls = struct {
2803
2903
 
2804
2904
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2905
  val == .true
2906
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2907
+ battle.rng.secondaryChance(player, rate)
2806
2908
  else if (showdown)
2807
2909
  battle.rng.chance(u8, rate, 256)
2808
2910
  else
@@ -2815,6 +2917,8 @@ pub const Rolls = struct {
2815
2917
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2918
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2919
  val == .true
2920
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2921
+ battle.rng.secondaryChance(player, 85)
2818
2922
  else if (showdown)
2819
2923
  battle.rng.chance(u8, 85, 256)
2820
2924
  else
@@ -2827,6 +2931,8 @@ pub const Rolls = struct {
2827
2931
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2932
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2933
  val
2934
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2935
+ battle.rng.metronome(player)
2830
2936
  else if (showdown)
2831
2937
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2938
  else move: {
@@ -2845,6 +2951,8 @@ pub const Rolls = struct {
2845
2951
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2952
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2953
  val - 1
2954
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2955
+ battle.rng.psywave(player)
2848
2956
  else if (showdown)
2849
2957
  battle.rng.range(u8, 0, max)
2850
2958
  else power: {
@@ -2860,8 +2968,11 @@ pub const Rolls = struct {
2860
2968
  }
2861
2969
 
2862
2970
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
- @intCast(val)
2971
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2972
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2973
+ else
2974
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2975
+ battle.rng.sleepDuration(player)
2865
2976
  else if (showdown)
2866
2977
  battle.rng.range(u3, 1, 8)
2867
2978
  else duration: {
@@ -2872,46 +2983,55 @@ pub const Rolls = struct {
2872
2983
  };
2873
2984
 
2874
2985
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2986
+ options.chance.duration(player, duration);
2876
2987
  return duration;
2877
2988
  }
2878
2989
 
2879
2990
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
- val
2991
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2992
+ if (val >= 1 and val <= 8) val else null
2993
+ else
2994
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2995
+ battle.rng.disableDuration(player)
2882
2996
  else if (showdown)
2883
2997
  battle.rng.range(u4, 1, 9)
2884
2998
  else
2885
2999
  @intCast((battle.rng.next() & 7) + 1);
2886
3000
 
2887
3001
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
3002
+ options.chance.duration(player, duration);
2889
3003
  return duration;
2890
3004
  }
2891
3005
 
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)
3006
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
3007
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3008
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3009
+ else
3010
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3011
+ battle.rng.confusionDuration(player)
2895
3012
  else if (showdown)
2896
3013
  battle.rng.range(u3, 2, 6)
2897
3014
  else
2898
3015
  @intCast((battle.rng.next() & 3) + 2);
2899
3016
 
2900
3017
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3018
+ options.chance.duration(player, duration);
2902
3019
  return duration;
2903
3020
  }
2904
3021
 
2905
3022
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
3023
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3024
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3025
+ else
3026
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3027
+ battle.rng.attackingDuration(player)
2908
3028
  else if (showdown)
2909
3029
  battle.rng.range(u3, 2, 4)
2910
3030
  else
2911
3031
  @intCast((battle.rng.next() & 1) + 2);
2912
3032
 
2913
3033
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3034
+ options.chance.duration(player, duration);
2915
3035
  return duration;
2916
3036
  }
2917
3037
 
@@ -2924,8 +3044,11 @@ pub const Rolls = struct {
2924
3044
  options: anytype,
2925
3045
  ) !u3 {
2926
3046
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
- @intCast(val)
3047
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3048
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3049
+ else
3050
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3051
+ battle.rng.distribution(player)
2929
3052
  else if (showdown)
2930
3053
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3054
  else n: {
@@ -2935,7 +3058,7 @@ pub const Rolls = struct {
2935
3058
 
2936
3059
  assert(n >= 2 and n <= 5);
2937
3060
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3061
+ options.chance.duration(player, n);
2939
3062
  } else {
2940
3063
  try options.chance.multiHit(player, n);
2941
3064
  }
@@ -2950,15 +3073,15 @@ pub const Rolls = struct {
2950
3073
  options: anytype,
2951
3074
  ) !u4 {
2952
3075
  // TODO: consider throwing error instead of rerolling?
2953
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2954
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2955
3078
  val
2956
3079
  else
2957
3080
  null
2958
3081
  else
2959
- null;
2960
-
2961
- const slot: u4 = overridden orelse slot: {
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3083
+ battle.rng.moveSlot(player, moves, check_pp)
3084
+ else slot: {
2962
3085
  if (showdown) {
2963
3086
  if (check_pp == 0) {
2964
3087
  var i: usize = moves.len;
@@ -3022,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3022
3145
  },
3023
3146
  .Switch => {
3024
3147
  const side = battle.side(player);
3025
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3026
3149
  while (slot <= 6) : (slot += 1) {
3027
3150
  const id = side.order[slot - 1];
3028
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3029
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3030
3153
  n += 1;
3031
3154
  }
3032
3155
  if (n == 0) {
@@ -3038,11 +3161,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3161
  const side = battle.side(player);
3039
3162
  const foe = battle.foe(player);
3040
3163
 
3041
- var active = &side.active;
3164
+ const active = &side.active;
3042
3165
  const stored = side.stored();
3043
3166
 
3044
3167
  // 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.
3168
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3169
  //
3047
3170
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3171
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3173
  // passing due to being forced into using a move. Instead of introducing another option
3051
3174
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3175
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3176
+ // switch at all
3054
3177
  if (isForced(active)) {
3055
3178
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3179
  n += 1;
3057
3180
  return n;
3058
3181
  }
3059
3182
 
3060
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3061
3184
  while (slot <= 6) : (slot += 1) {
3062
3185
  const id = side.order[slot - 1];
3063
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3064
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3065
3188
  n += 1;
3066
3189
  }
3067
3190
 
@@ -3069,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3192
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3193
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3194
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3195
+ // move normally if sleeping/frozen/bound
3073
3196
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3197
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3198
  {
@@ -3082,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3205
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3206
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3207
  // 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.
3208
+ // proc-ed move in their moveset) and disabling all other moves
3086
3209
  if (limited) {
3087
3210
  assert(showdown);
3088
3211
  assert(side.last_selected_move != .None);
@@ -3094,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3094
3217
  const struggle =
3095
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3096
3219
  const s = if (struggle) 0 else slot;
3097
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3098
3221
  n += 1;
3099
3222
  return n;
3100
3223
  }
@@ -3108,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3231
  if (m.id == .None) break;
3109
3232
  if (m.pp == 0) continue;
3110
3233
  if (active.volatiles.disable_move == slot) continue;
3111
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3112
3235
  n += 1;
3113
3236
  }
3114
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3115
3238
  if (n == before) {
3116
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3117
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3118
3241
  while (slot <= 4) : (slot += 1) {
3119
3242
  if (active.moves[slot - 1].pp != 0) return n;
3120
3243
  }