@pkmn/engine 0.1.0-dev.1220fbc5 → 0.1.0-dev.130de754

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