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

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