@pkmn/engine 0.1.0-dev.abd68573 → 0.1.0-dev.ac652f4e

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 (79) hide show
  1. package/README.md +17 -12
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +47 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +110 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +53 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -3
  12. package/build/pkg/addon.js +34 -65
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/data.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +15 -18
  19. package/build/pkg/gen1.js +28 -19
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +40 -90
  22. package/build/pkg/index.js +23 -70
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +52 -76
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +152 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +152 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +359 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +33 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +307 -78
  38. package/package.json +54 -135
  39. package/src/bin/install-pkmn-engine +10 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/c.zig +255 -0
  42. package/src/lib/bindings/node.zig +121 -0
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +8 -8
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +13 -9
  49. package/src/lib/common/options.zig +6 -0
  50. package/src/lib/common/protocol.zig +45 -44
  51. package/src/lib/common/rational.zig +323 -158
  52. package/src/lib/common/rng.zig +5 -7
  53. package/src/lib/common/util.zig +60 -1
  54. package/src/lib/gen1/calc.zig +693 -157
  55. package/src/lib/gen1/chance.zig +415 -291
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +38 -41
  60. package/src/lib/gen1/helpers.zig +64 -540
  61. package/src/lib/gen1/mechanics.zig +332 -244
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +90 -503
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +26 -29
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +46 -25
  71. package/src/lib/gen2/mechanics.zig +88 -115
  72. package/src/lib/node.zig +3 -116
  73. package/src/lib/pkmn.zig +14 -7
  74. package/src/lib/wasm.zig +13 -9
  75. package/src/tools/display/dom.ts +93 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -400
  78. package/build/test/display.js.map +0 -1
  79. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,37 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
-
10
- const data = @import("data.zig");
10
+ const std = @import("std");
11
11
 
12
12
  const assert = std.debug.assert;
13
-
14
- const expectEqual = std.testing.expectEqual;
15
-
16
- const showdown = pkmn.options.showdown;
17
-
18
- const Choice = common.Choice;
19
- const ID = common.ID;
20
- const Player = common.Player;
21
- const Result = common.Result;
22
-
23
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
24
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
25
18
  const Fail = protocol.Fail;
26
- const Heal = protocol.Heal;
27
-
28
19
  const Gen12 = rng.Gen12;
29
-
30
- const ActivePokemon = data.ActivePokemon;
31
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
32
22
  const Move = data.Move;
33
23
  const MoveSlot = data.MoveSlot;
34
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
35
27
  const Side = data.Side;
36
28
  const Species = data.Species;
37
29
  const Stats = data.Stats;
@@ -166,7 +158,7 @@ fn selectMove(
166
158
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
159
  // set the selected move will not actually be used, it will just be reported as having
168
160
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
161
+ // replicate the incorrect behavior with the correct mechanisms)
170
162
  saveMove(battle, player, choice);
171
163
  } else {
172
164
  assert(choice.data == 0);
@@ -174,7 +166,10 @@ fn selectMove(
174
166
  if (foe_choice.type == .Switch) {
175
167
  const last = side.active.move(battle.lastMove(player).index);
176
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
177
- if (last.id == .MirrorMove) return Result.Error;
169
+ if (last.id == .MirrorMove) {
170
+ if (!pkmn.options.mod) return Result.Error;
171
+ side.last_selected_move = last.id;
172
+ }
178
173
  }
179
174
  }
180
175
  return null;
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
248
  active.moves = incoming.moves;
254
249
 
255
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
256
252
 
257
253
  foe.active.volatiles.Binding = false;
258
254
 
259
255
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
256
 
262
257
  if (showdown and incoming.status == Status.TOX) {
263
258
  incoming.status = Status.init(.PSN);
@@ -347,13 +342,13 @@ fn doTurn(
347
342
  )) |r| return r;
348
343
  if (!replace) {
349
344
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
345
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
346
  }
352
347
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
348
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
349
  } else if (foe_choice.type == .Pass) return null;
355
350
 
356
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
357
352
 
358
353
  residual = true;
359
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +364,10 @@ fn doTurn(
369
364
  )) |r| return r;
370
365
  if (!replace) {
371
366
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
367
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
368
  }
374
369
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
370
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
371
  }
377
372
 
378
373
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -425,13 +420,13 @@ fn executeMove(
425
420
  mslot = @intCast(battle.lastMove(player).index);
426
421
  const stored = side.stored();
427
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
428
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
429
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
430
425
  side.last_selected_move == .HyperBeam or
431
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
427
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
428
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // index 1 and the current Pokémon has the same move in its first slot.
429
+ // index 1 and the current Pokémon has the same move in its first slot
435
430
  if (!auto) {
436
431
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -458,6 +453,25 @@ fn executeMove(
458
453
  .err => return @as(?Result, Result.Error),
459
454
  }
460
455
 
456
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
457
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
458
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
459
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
460
+ side.last_selected_move != side.active.move(mslot).id)
461
+ {
462
+ assert(mslot == 1);
463
+ const has_beam = has_beam: {
464
+ for (side.active.moves, 0..) |m, i| {
465
+ if (m.id == .HyperBeam) {
466
+ mslot = @intCast(i + 1);
467
+ break :has_beam true;
468
+ }
469
+ }
470
+ break :has_beam false;
471
+ };
472
+ if (!has_beam) skip_pp = true;
473
+ }
474
+
461
475
  const can = skip_can or
462
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
463
477
  if (!can) return null;
@@ -484,17 +498,22 @@ fn beforeMove(
484
498
 
485
499
  if (Status.is(stored.status, .SLP)) {
486
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
487
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
488
- if (options.calc.modify(player, .sleep)) |extend| {
489
- if (!extend) stored.status = 0;
503
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
504
+ .started, .ended => stored.status = 0,
505
+ .continuing => if (Status.duration(stored.status) > 1) {
506
+ stored.status -= 1;
507
+ },
508
+ else => unreachable,
490
509
  } else {
491
510
  stored.status -= 1;
492
511
  }
493
512
 
494
513
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
498
517
 
499
518
  if (duration == 0) {
500
519
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +551,12 @@ fn beforeMove(
532
551
  }
533
552
 
534
553
  if (volatiles.disable_duration > 0) {
535
- if (options.calc.modify(player, .disable)) |extend| {
536
- if (!extend) volatiles.disable_duration = 0;
537
- } else {
538
- volatiles.disable_duration -= 1;
539
- }
540
- try options.chance.disable(player, volatiles.disable_duration);
554
+ volatiles.disable_duration =
555
+ decrement(.disable, player, options, volatiles.disable_duration);
556
+ try options.chance.disable(
557
+ player,
558
+ if (volatiles.disable_duration == 0) .ended else .continuing,
559
+ );
541
560
 
542
561
  if (volatiles.disable_duration == 0) {
543
562
  volatiles.disable_move = 0;
@@ -550,19 +569,24 @@ fn beforeMove(
550
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
551
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
552
571
  if (side.last_selected_move == .None) {
553
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
554
573
  return .err;
555
574
  }
556
575
 
557
576
  if (volatiles.Confusion) {
558
577
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
578
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
579
+ .started, .ended => volatiles.confusion = 0,
580
+ .continuing => if (volatiles.confusion > 1) {
581
+ volatiles.confusion -= 1;
582
+ },
583
+ .overwritten => {},
584
+ else => unreachable,
562
585
  } else {
563
586
  volatiles.confusion -= 1;
564
587
  }
565
- try options.chance.confusion(player, volatiles.confusion);
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
590
 
567
591
  if (volatiles.confusion == 0) {
568
592
  volatiles.Confusion = false;
@@ -580,7 +604,9 @@ fn beforeMove(
580
604
  volatiles.Charging = false;
581
605
  volatiles.Binding = false;
582
606
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
607
+
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
584
610
  {
585
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,19 +614,14 @@ fn beforeMove(
588
614
  const def = foe.active.stats.def;
589
615
  foe.active.stats.def = active.stats.def;
590
616
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
592
620
  }
593
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
- // on the targeting behavior of the confused Pokémon's selected move which results
595
- // in the wrong behavior with respect to the Substitute + Confusion glitch
596
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
597
- player
598
- else
599
- player.foe();
600
621
 
601
622
  const uncapped = battle.last_damage;
602
623
  // Skipping adjustDamage / randomizeDamage / checkHit
603
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
604
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
605
626
  if (showdown) battle.last_damage = uncapped;
606
627
 
@@ -617,7 +638,10 @@ fn beforeMove(
617
638
  volatiles.Thrashing = false;
618
639
  volatiles.Charging = false;
619
640
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
641
+
642
+ options.chance.observe(.attacking, player, .None);
643
+ options.chance.observe(.binding, player, .None);
644
+
621
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
646
  try log.cant(.{ ident, .Paralysis });
623
647
  return .done;
@@ -635,12 +659,8 @@ fn beforeMove(
635
659
  volatiles.state +%= battle.last_damage;
636
660
  }
637
661
 
638
- if (options.calc.modify(player, .attacking)) |extend| {
639
- if (!extend) volatiles.attacks = 0;
640
- } else {
641
- volatiles.attacks -= 1;
642
- }
643
- try options.chance.attacking(player, volatiles.attacks);
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
644
664
 
645
665
  if (volatiles.attacks != 0) {
646
666
  try log.activate(.{ ident, .Bide });
@@ -674,35 +694,40 @@ fn beforeMove(
674
694
  }
675
695
 
676
696
  if (volatiles.Thrashing) {
677
- if (options.calc.modify(player, .attacking)) |extend| {
678
- if (!extend) volatiles.attacks = 0;
679
- } else {
680
- volatiles.attacks -= 1;
681
- }
682
- try options.chance.attacking(player, volatiles.attacks);
697
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
698
+
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
683
701
 
684
- if (!showdown and handleThrashing(battle, active, player, options)) {
702
+ if (volatiles.attacks == 0) {
703
+ const overwritten = volatiles.Confusion;
704
+
705
+ volatiles.Thrashing = false;
706
+ volatiles.Confusion = true;
707
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
708
+
709
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
710
+ // ended (the engine's protocol is always from the omniscient perspective and clients
711
+ // can be expected to recognize this pattern and pass the information only to the
712
+ // correct player)
685
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
714
+ options.chance.observe(.confusion, player, if (overwritten)
715
+ if (pkmn.options.overwrite) .overwritten else .continuing
716
+ else
717
+ .started);
718
+ if (!overwritten) options.calc.confusion(player);
686
719
  }
687
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active, player, options)) {
693
- try log.start(.{ battle.active(player), .ConfusionSilent });
694
- }
695
- }
720
+
721
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
722
+ // used in useMove and doesn't have the notion of skipping canMove semantics
723
+ if (showdown) side.last_used_move = side.last_selected_move;
724
+
696
725
  return .skip_can;
697
726
  }
698
727
 
699
728
  if (volatiles.Binding) {
700
- if (options.calc.modify(player, .binding)) |extend| {
701
- if (!extend) volatiles.attacks = 0;
702
- } else {
703
- volatiles.attacks -= 1;
704
- }
705
- try options.chance.binding(player, volatiles.attacks);
729
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
706
731
 
707
732
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
708
733
  if (showdown or battle.last_damage != 0) {
@@ -756,9 +781,14 @@ fn canMove(
756
781
 
757
782
  if (move.effect == .Thrashing) {
758
783
  Effects.thrashing(battle, player, options);
759
- } else if (!showdown and move.effect == .Binding) {
760
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
761
- try Effects.binding(battle, player, false, options);
784
+ } else if (move.effect == .Binding) {
785
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
786
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
787
+ if (showdown) {
788
+ battle.foe(player).active.volatiles.Recharging = false;
789
+ } else {
790
+ try Effects.binding(battle, player, false, options);
791
+ }
762
792
  }
763
793
 
764
794
  return true;
@@ -795,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
795
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
796
826
  }
797
827
 
798
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
799
829
  var active = &side.active;
800
830
  const volatiles = &active.volatiles;
801
831
 
@@ -803,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
803
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
804
834
  if (showdown and volatiles.Transform) return;
805
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
806
839
  assert(mslot > 0 and mslot <= 4);
807
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
840
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
808
841
  }
809
842
 
810
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -852,13 +885,13 @@ fn doMove(
852
885
  break :miss true;
853
886
  }
854
887
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
888
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
889
  };
857
890
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
891
 
859
892
  var late = showdown and move.effect != .Explode;
860
893
  const skip = status or immune;
861
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
862
895
  if (showdown and move.effect.isMulti()) {
863
896
  try Effects.multiHit(battle, player, move, options);
864
897
  hits = side.active.volatiles.attacks;
@@ -880,8 +913,9 @@ fn doMove(
880
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
881
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
882
915
  battle.last_damage = if (ohko) 65535 else 0;
883
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
884
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
916
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
917
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
918
+ if (!showdown) try options.chance.commit(player, .err);
885
919
  return @as(?Result, Result.Error);
886
920
  }
887
921
  if (battle.last_damage == 0) {
@@ -895,11 +929,12 @@ fn doMove(
895
929
  }
896
930
  }
897
931
 
932
+ var missed: bool = undefined;
898
933
  const zero = battle.last_damage == 0;
899
- miss = if (showdown or skip)
900
- miss
901
- else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
934
+ if (!showdown and !skip) {
935
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
936
+ miss = missed or zero;
937
+ }
903
938
 
904
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -926,11 +961,32 @@ fn doMove(
926
961
  try log.immune(.{ foe_ident, .OHKO });
927
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
928
963
  try log.immune(.{ foe_ident, .None });
964
+ if (!showdown) {
965
+ if (move.effect == .Binding) {
966
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
967
+ // then simply clears it later on if the move happens to miss in moveHit.
968
+ // However, moveHit can return true and we'll still end up in the outer miss
969
+ // handling block because attacking into an immune Pokemon does zero damage
970
+ // (considered missing)
971
+ assert(zero);
972
+ // Thus we still need to actually save the hit result (but not the crit roll).
973
+ // Using .miss for this commit would accomplish what we want, but is somewhat
974
+ // misleading as its entirely possible the move actually did hit (and the target
975
+ // is now trapped) so we use the .binding value instead which is effectively
976
+ // identical, just doesn't claim to know about the actual results of the hit
977
+ // roll)
978
+ try options.chance.commit(player, .binding);
979
+ } else {
980
+ // The .glitch option allows for conditionally committing if we are in a
981
+ // division-by-zero freeze alternative timeline scenario
982
+ try options.chance.commit(player, .glitch);
983
+ }
984
+ }
929
985
  } else if (mist) {
930
986
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
987
  try log.fail(.{ foe_ident, .None });
932
988
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
934
990
  try log.lastmiss(.{});
935
991
  try log.miss(.{battle.active(player)});
936
992
  }
@@ -945,17 +1001,15 @@ fn doMove(
945
1001
  try buildRage(battle, player.foe(), options);
946
1002
  } else if (showdown and move.effect == .Disable) {
947
1003
  try buildRage(battle, player.foe(), options);
948
- } else if (!showdown and immune and move.effect == .Binding) {
949
- try options.chance.commit(player, .binding);
950
1004
  }
951
1005
  return null;
952
- } else {
1006
+ } else if (!showdown) {
953
1007
  try options.chance.commit(player, .hit);
954
1008
  }
955
1009
 
956
1010
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
1011
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
1012
+ // it up above before damage calculation)
959
1013
  if (!showdown and move.effect.isMulti()) {
960
1014
  try Effects.multiHit(battle, player, move, options);
961
1015
  hits = side.active.volatiles.attacks;
@@ -994,32 +1048,30 @@ fn doMove(
994
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
995
1049
  }
996
1050
  try log.hitcount(.{ battle.active(player.foe()), hit });
997
- } else if (showdown) {
1051
+ } else if (showdown and move.effect == .Binding) {
998
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
999
- if (move.effect == .Binding) {
1000
- try Effects.binding(battle, player, rewrap, options);
1001
- if (immune) {
1002
- battle.last_damage = 0;
1003
- assert(foe.stored().hp > 0);
1004
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1005
- if (sub) {
1006
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1007
- } else {
1008
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1009
- try buildRage(battle, player.foe(), options);
1010
- }
1011
- return null;
1053
+ try Effects.binding(battle, player, rewrap, options);
1054
+ if (immune) {
1055
+ battle.last_damage = 0;
1056
+ assert(foe.stored().hp > 0);
1057
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1058
+ if (sub) {
1059
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1060
+ } else {
1061
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1062
+ try buildRage(battle, player.foe(), options);
1012
1063
  }
1064
+ return null;
1013
1065
  }
1014
1066
  }
1015
1067
 
1016
1068
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1069
+ // if an effect was intended to "always happen" it will still get skipped
1018
1070
  if (nullified) return null;
1019
1071
 
1020
1072
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1073
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1074
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1075
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1076
 
1025
1077
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1086,7 @@ fn doMove(
1034
1086
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1087
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1088
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1089
+ // puts the |-status| message before |-hitcount| instead of after
1038
1090
  if (move.effect == .Twineedle) {
1039
1091
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1092
  } else if (move.effect == .Disable) {
@@ -1058,7 +1110,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1110
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1111
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1112
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1113
+ const spurious =
1114
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1115
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1116
  }
1063
1117
 
1064
1118
  fn calcDamage(
@@ -1067,6 +1121,7 @@ fn calcDamage(
1067
1121
  target_player: Player,
1068
1122
  m: ?Move.Data,
1069
1123
  crit: bool,
1124
+ options: anytype,
1070
1125
  ) bool {
1071
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1072
1127
  const cfz = m == null;
@@ -1078,28 +1133,8 @@ fn calcDamage(
1078
1133
 
1079
1134
  const special = move.type.special();
1080
1135
 
1081
- // zig fmt: off
1082
- var atk: u32 =
1083
- if (crit)
1084
- if (special) side.stored().stats.spc
1085
- else side.stored().stats.atk
1086
- else
1087
- if (special) side.active.stats.spc
1088
- else side.active.stats.atk;
1089
- var def: u32 =
1090
- if (crit)
1091
- if (special) target.stored().stats.spc
1092
- else target.stored().stats.def
1093
- else
1094
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1095
- if (special)
1096
- target.active.stats.spc *
1097
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1098
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1099
- else
1100
- target.active.stats.def *
1101
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1102
- // zig fmt: on
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1103
1138
 
1104
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1105
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1107,7 +1142,7 @@ fn calcDamage(
1107
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1108
1143
  atk = @max((atk / 4) & 255, 1);
1109
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1110
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1111
1146
  }
1112
1147
 
1113
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1124,11 +1159,41 @@ fn calcDamage(
1124
1159
  d = @min(997, d);
1125
1160
  d += 2;
1126
1161
 
1162
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1163
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1164
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1165
+ // that results in counter intuitive probabilities in the common case
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1167
+ atk = attack(side, !crit, special);
1168
+ def = defense(target, !crit, special, false);
1169
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1170
+ }
1171
+
1127
1172
  battle.last_damage = @intCast(d);
1128
1173
 
1129
1174
  return true;
1130
1175
  }
1131
1176
 
1177
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1178
+ return if (crit)
1179
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1180
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1181
+ }
1182
+
1183
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1184
+ return if (crit)
1185
+ if (special) target.stored().stats.spc else target.stored().stats.def
1186
+ else
1187
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1188
+ if (special)
1189
+ target.active.stats.spc *
1190
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1191
+ else
1192
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1193
+ target.active.stats.def *
1194
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1195
+ }
1196
+
1132
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1133
1198
  const side = battle.side(player);
1134
1199
  const foe = battle.foe(player);
@@ -1173,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1238
  const foe = battle.foe(player);
1174
1239
 
1175
1240
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1241
 
1178
1242
  battle.last_damage = switch (side.last_selected_move) {
1179
1243
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1184,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1184
1248
  .Psywave => power: {
1185
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1186
1250
  // GLITCH: Psywave infinite glitch loop
1187
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1188
1252
  break :power try Rolls.psywave(battle, player, max, options);
1189
1253
  },
1190
1254
  else => unreachable,
1191
1255
  };
1192
1256
 
1193
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1194
1258
 
1195
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1196
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1227,16 +1291,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1227
1291
 
1228
1292
  if (!used or !selected) {
1229
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1230
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return Result.Error;
1231
1295
  try options.log.fail(.{ battle.active(player), .None });
1232
1296
  return null;
1233
1297
  }
1234
1298
 
1235
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1236
1304
 
1237
- // Pokémon Showdown calls checkHit before Counter
1305
+ // Pokémon Showdown calls moveHit before Counter
1238
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1307
 
1241
1308
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1309
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1332,7 @@ fn applyDamage(
1265
1332
  subbed.active.volatiles.Substitute = false;
1266
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1269
1336
  return true;
1270
1337
  } else {
1271
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1299,7 +1366,7 @@ fn mirrorMove(
1299
1366
  try options.log.fail(.{ battle.active(player), .None });
1300
1367
  return null;
1301
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1302
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1303
1370
  }
1304
1371
 
1305
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1320,7 +1387,7 @@ fn metronome(
1320
1387
  var side = battle.side(player);
1321
1388
 
1322
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1323
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1324
1391
 
1325
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1326
1393
  return null;
@@ -1332,7 +1399,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1399
  var immune = false;
1333
1400
  var mist = false;
1334
1401
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1402
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1403
+ if (!showdown) try options.chance.commit(player, .hit);
1404
+ return true;
1405
+ }
1336
1406
 
1337
1407
  assert(!immune);
1338
1408
  if (mist) {
@@ -1341,11 +1411,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1411
  try options.log.activate(.{ foe_ident, .Mist });
1342
1412
  try options.log.fail(.{ foe_ident, .None });
1343
1413
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1414
  try options.log.lastmiss(.{});
1346
1415
  try options.log.miss(.{battle.active(player)});
1347
1416
  }
1348
1417
 
1418
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1419
  return false;
1350
1420
  }
1351
1421
 
@@ -1356,7 +1426,7 @@ fn moveHit(
1356
1426
  immune: *bool,
1357
1427
  mist: *bool,
1358
1428
  options: anytype,
1359
- ) bool {
1429
+ ) !bool {
1360
1430
  var side = battle.side(player);
1361
1431
  const foe = battle.foe(player);
1362
1432
 
@@ -1407,7 +1477,7 @@ fn moveHit(
1407
1477
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1478
 
1409
1479
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1480
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1481
  };
1412
1482
 
1413
1483
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1418,13 +1488,17 @@ fn moveHit(
1418
1488
 
1419
1489
  if (!miss) return true;
1420
1490
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1491
+
1421
1492
  side.active.volatiles.Binding = false;
1493
+ options.chance.observe(.binding, player, .None);
1494
+
1422
1495
  return false;
1423
1496
  }
1424
1497
 
1425
1498
  fn checkFaint(
1426
1499
  battle: anytype,
1427
1500
  player: Player,
1501
+ cap: bool,
1428
1502
  options: anytype,
1429
1503
  ) @TypeOf(options.log).Error!?Result {
1430
1504
  const side = battle.side(player);
@@ -1437,8 +1511,8 @@ fn checkFaint(
1437
1511
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1512
  const more = player_out or foe_out;
1439
1513
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1514
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1515
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1516
 
1443
1517
  assert(!side.active.volatiles.MultiHit);
1444
1518
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1533,7 @@ fn checkFaint(
1459
1533
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1534
  }
1461
1535
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1536
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1537
  var side = battle.side(player);
1464
1538
  var foe = battle.foe(player);
1465
1539
  assert(side.stored().hp == 0);
@@ -1468,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1468
1542
  assert(!foe_volatiles.MultiHit);
1469
1543
  if (foe_volatiles.Bide) {
1470
1544
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1471
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1545
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1472
1546
  if (foe_volatiles.state != 0) return Result.Error;
1473
1547
  }
1474
1548
 
@@ -1493,7 +1567,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1567
  }
1494
1568
 
1495
1569
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1570
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1571
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1572
+ if (cap) options.calc.capped(player);
1497
1573
  return null;
1498
1574
  }
1499
1575
 
@@ -1557,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1557
1633
 
1558
1634
  battle.turn += 1;
1559
1635
 
1560
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1636
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1561
1637
  try options.log.tie(.{});
1562
1638
  return Result.Tie;
1563
1639
  }
1564
1640
 
1565
- if (showdown and battle.turn >= 1000) {
1641
+ if (pkmn.options.mod and battle.turn >= 1000) {
1566
1642
  try options.log.tie(.{});
1567
1643
  return Result.Tie;
1568
1644
  } else if (battle.turn >= 65535) {
@@ -1653,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1653
1729
  }
1654
1730
  }
1655
1731
 
1656
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1657
- var volatiles = &active.volatiles;
1658
- assert(volatiles.Thrashing);
1659
- if (volatiles.attacks > 0) return false;
1660
-
1661
- volatiles.Thrashing = false;
1662
- volatiles.Confusion = true;
1663
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1664
- return true;
1665
- }
1666
-
1667
1732
  fn onBegin(
1668
1733
  battle: anytype,
1669
1734
  player: Player,
@@ -1750,8 +1815,10 @@ pub const Effects = struct {
1750
1815
 
1751
1816
  side.active.volatiles.Bide = true;
1752
1817
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1818
+
1753
1819
  side.active.volatiles.state = 0;
1754
1820
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1821
+ options.chance.observe(.attacking, player, .started);
1755
1822
 
1756
1823
  try options.log.start(.{ battle.active(player), .Bide });
1757
1824
  }
@@ -1813,12 +1880,14 @@ pub const Effects = struct {
1813
1880
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1881
  if (!try checkHit(battle, player, move, options)) return;
1815
1882
  }
1816
- try options.chance.commit(player, .hit);
1817
1883
  }
1818
1884
 
1819
1885
  if (foe.active.volatiles.Confusion) return;
1820
1886
  foe.active.volatiles.Confusion = true;
1821
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1887
+
1888
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1889
+ options.chance.observe(.confusion, player.foe(), .started);
1890
+ options.calc.confusion(player.foe());
1822
1891
 
1823
1892
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1824
1893
  }
@@ -1846,10 +1915,7 @@ pub const Effects = struct {
1846
1915
  const foe_ident = battle.active(player.foe());
1847
1916
 
1848
1917
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1918
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1919
 
1854
1920
  if (volatiles.disable_move != 0) {
1855
1921
  try options.log.fail(.{ foe_ident, .None });
@@ -1867,18 +1933,18 @@ pub const Effects = struct {
1867
1933
 
1868
1934
  // Technically this is still considered simply a "miss" on the cartridge,
1869
1935
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1870
- if (n == 0) {
1936
+ if (n == 0 or (pkmn.options.mod and err)) {
1871
1937
  try options.log.fail(.{ foe_ident, .None });
1872
1938
  return null;
1873
1939
  } else if (err) {
1874
1940
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1875
- assert(!showdown);
1876
1941
  return Result.Error;
1877
1942
  }
1878
1943
 
1879
1944
  volatiles.disable_move =
1880
1945
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1881
1946
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1947
+ options.chance.observe(.disable, player.foe(), .started);
1882
1948
 
1883
1949
  const id = foe.active.move(volatiles.disable_move).id;
1884
1950
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1949,7 +2015,7 @@ pub const Effects = struct {
1949
2015
  if (foe.active.types.includes(move.type)) return;
1950
2016
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1951
2017
  // Freeze Clause Mod
1952
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2018
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1953
2019
 
1954
2020
  foe_stored.status = Status.init(.FRZ);
1955
2021
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1985,22 +2051,24 @@ pub const Effects = struct {
1985
2051
  if (p != player and Status.any(s.stored().status)) {
1986
2052
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2053
  s.stored().status = 0;
2054
+ options.chance.observe(.sleep, p, .None);
1988
2055
  } else if (showdown and s.stored().status == Status.TOX) {
1989
2056
  s.stored().status = Status.init(.PSN);
1990
2057
  try log.status(.{ battle.active(p), s.stored().status, .None });
1991
2058
  }
1992
- try clearVolatiles(battle, p, p != player, options);
2059
+ try clearVolatiles(battle, p, options);
1993
2060
  }
1994
2061
  } else {
1995
2062
  if (Status.any(foe_stored.status)) {
1996
2063
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1997
2064
  foe.last_selected_move = .SKIP_TURN;
2065
+ options.chance.observe(.sleep, player.foe(), .None);
1998
2066
  }
1999
2067
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2000
2068
  foe_stored.status = 0;
2001
2069
  }
2002
- try clearVolatiles(battle, player, false, options);
2003
- try clearVolatiles(battle, player.foe(), true, options);
2070
+ try clearVolatiles(battle, player, options);
2071
+ try clearVolatiles(battle, player.foe(), options);
2004
2072
  }
2005
2073
  }
2006
2074
 
@@ -2051,7 +2119,6 @@ pub const Effects = struct {
2051
2119
  }
2052
2120
  }
2053
2121
 
2054
- try options.chance.commit(player, .hit);
2055
2122
  foe.active.volatiles.LeechSeed = true;
2056
2123
 
2057
2124
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2159,10 @@ pub const Effects = struct {
2092
2159
  if (!has_mimic) {
2093
2160
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2161
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2162
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2163
  }
2098
2164
  }
2099
2165
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2166
 
2102
2167
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2168
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2216,6 @@ pub const Effects = struct {
2151
2216
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2217
  if (!try checkHit(battle, player, move, options)) return;
2153
2218
  }
2154
- try options.chance.commit(player, .hit);
2155
2219
 
2156
2220
  foe_stored.status = Status.init(.PAR);
2157
2221
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2283,6 @@ pub const Effects = struct {
2219
2283
 
2220
2284
  if (move.effect == .Poison) {
2221
2285
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2286
  } else {
2224
2287
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2288
  return;
@@ -2288,13 +2351,12 @@ pub const Effects = struct {
2288
2351
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2352
  });
2290
2353
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2354
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2355
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2356
  }
2295
2357
 
2296
2358
  // Sleep Clause Mod
2297
- if (showdown) {
2359
+ if (pkmn.options.mod) {
2298
2360
  for (foe.pokemon) |p| {
2299
2361
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2300
2362
  }
@@ -2302,6 +2364,8 @@ pub const Effects = struct {
2302
2364
  foe.active.volatiles.Recharging = false;
2303
2365
 
2304
2366
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2367
+ options.chance.observe(.sleep, player.foe(), .started);
2368
+
2305
2369
  const last = battle.side(player).last_selected_move;
2306
2370
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2307
2371
  }
@@ -2343,9 +2407,7 @@ pub const Effects = struct {
2343
2407
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2408
  if (showdown) {
2345
2409
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2410
+ _ = try checkHit(battle, player, move, options);
2349
2411
  battle.last_damage = 0;
2350
2412
  } else {
2351
2413
  try options.log.fail(.{ battle.active(player), .None });
@@ -2360,6 +2422,8 @@ pub const Effects = struct {
2360
2422
 
2361
2423
  volatiles.Thrashing = true;
2362
2424
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2425
+
2426
+ options.chance.observe(.attacking, player, .started);
2363
2427
  }
2364
2428
 
2365
2429
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2398,13 +2462,14 @@ pub const Effects = struct {
2398
2462
  if (side.active.volatiles.Binding) return;
2399
2463
  side.active.volatiles.Binding = true;
2400
2464
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2401
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2465
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2402
2466
  if (!showdown) {
2403
2467
  foe.active.volatiles.Recharging = false;
2404
2468
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2405
2469
 
2406
2470
  side.active.volatiles.attacks =
2407
2471
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2472
+ options.chance.observe(.binding, player, .started);
2408
2473
  }
2409
2474
 
2410
2475
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2521,7 +2586,6 @@ pub const Effects = struct {
2521
2586
  } else {
2522
2587
  // checkHit already checks for Invulnerable
2523
2588
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2589
  }
2526
2590
 
2527
2591
  var stats = &foe.active.stats;
@@ -2635,22 +2699,22 @@ fn isForced(active: anytype) bool {
2635
2699
  active.volatiles.Thrashing or active.volatiles.Charging;
2636
2700
  }
2637
2701
 
2638
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2702
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2639
2703
  var log = options.log;
2640
2704
  var side = battle.side(who);
2641
2705
  var volatiles = &side.active.volatiles;
2642
2706
  const ident = battle.active(who);
2643
2707
 
2644
- options.chance.clearDurations(who, clear);
2645
-
2646
2708
  if (volatiles.disable_move != 0) {
2647
2709
  volatiles.disable_move = 0;
2648
2710
  volatiles.disable_duration = 0;
2711
+ options.chance.observe(.disable, who, .None);
2649
2712
  try log.end(.{ ident, .DisableSilent });
2650
2713
  }
2651
2714
  if (volatiles.Confusion) {
2652
2715
  // volatiles.confusion is left unchanged
2653
2716
  volatiles.Confusion = false;
2717
+ options.chance.observe(.confusion, who, .None);
2654
2718
  try log.end(.{ ident, .ConfusionSilent });
2655
2719
  }
2656
2720
  if (volatiles.Mist) {
@@ -2685,6 +2749,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2685
2749
  }
2686
2750
  }
2687
2751
 
2752
+ fn decrement(
2753
+ comptime field: chance.Action.Field,
2754
+ player: Player,
2755
+ options: anytype,
2756
+ n: anytype,
2757
+ ) @TypeOf(n) {
2758
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2759
+ .started, .ended => 0,
2760
+ .continuing => if (n > 1) n - 1 else n,
2761
+ else => unreachable,
2762
+ } else n - 1;
2763
+ }
2764
+
2688
2765
  pub const Rolls = struct {
2689
2766
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2767
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2700,7 +2777,13 @@ pub const Rolls = struct {
2700
2777
  return p1;
2701
2778
  }
2702
2779
 
2703
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2780
+ fn criticalHit(
2781
+ battle: anytype,
2782
+ player: Player,
2783
+ rate: u8,
2784
+ spurious: bool,
2785
+ options: anytype,
2786
+ ) !bool {
2704
2787
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2705
2788
  val == .true
2706
2789
  else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
@@ -2710,7 +2793,7 @@ pub const Rolls = struct {
2710
2793
  else
2711
2794
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2712
2795
 
2713
- try options.chance.criticalHit(player, crit, rate);
2796
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2714
2797
  return crit;
2715
2798
  }
2716
2799
 
@@ -2732,17 +2815,19 @@ pub const Rolls = struct {
2732
2815
  return roll;
2733
2816
  }
2734
2817
 
2735
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2818
+ const uN = if (pkmn.options.miss) u8 else u9;
2819
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2820
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2736
2821
  const ok = if (options.calc.overridden(player, .hit)) |val|
2737
2822
  val == .true
2738
2823
  else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2739
- battle.rng.hit(player, accuracy)
2824
+ battle.rng.hit(player, acc)
2740
2825
  else if (showdown)
2741
- battle.rng.chance(u8, accuracy, 256)
2826
+ battle.rng.chance(uN, acc, 256)
2742
2827
  else
2743
- battle.rng.next() < accuracy;
2828
+ battle.rng.next() < acc;
2744
2829
 
2745
- options.chance.hit(ok, accuracy);
2830
+ try options.chance.hit(player, ok, acc);
2746
2831
  return ok;
2747
2832
  }
2748
2833
 
@@ -2874,9 +2959,10 @@ pub const Rolls = struct {
2874
2959
  }
2875
2960
 
2876
2961
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2877
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2878
- @intCast(val)
2879
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2962
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2963
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2964
+ else
2965
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2880
2966
  battle.rng.sleepDuration(player)
2881
2967
  else if (showdown)
2882
2968
  battle.rng.range(u3, 1, 8)
@@ -2888,14 +2974,15 @@ pub const Rolls = struct {
2888
2974
  };
2889
2975
 
2890
2976
  assert(duration >= 1 and duration <= 7);
2891
- options.chance.duration(.sleep, player, player.foe(), duration);
2977
+ options.chance.duration(player, duration);
2892
2978
  return duration;
2893
2979
  }
2894
2980
 
2895
2981
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2896
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2897
- val
2898
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2982
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2983
+ if (val >= 1 and val <= 8) val else null
2984
+ else
2985
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2899
2986
  battle.rng.disableDuration(player)
2900
2987
  else if (showdown)
2901
2988
  battle.rng.range(u4, 1, 9)
@@ -2903,29 +2990,31 @@ pub const Rolls = struct {
2903
2990
  @intCast((battle.rng.next() & 7) + 1);
2904
2991
 
2905
2992
  assert(duration >= 1 and duration <= 8);
2906
- options.chance.duration(.disable, player, player.foe(), duration);
2993
+ options.chance.duration(player, duration);
2907
2994
  return duration;
2908
2995
  }
2909
2996
 
2910
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2911
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2912
- @intCast(val)
2913
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2914
- battle.rng.confusionDuration(player, self)
2997
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2998
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2999
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3000
+ else
3001
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3002
+ battle.rng.confusionDuration(player)
2915
3003
  else if (showdown)
2916
3004
  battle.rng.range(u3, 2, 6)
2917
3005
  else
2918
3006
  @intCast((battle.rng.next() & 3) + 2);
2919
3007
 
2920
3008
  assert(duration >= 2 and duration <= 5);
2921
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
3009
+ options.chance.duration(player, duration);
2922
3010
  return duration;
2923
3011
  }
2924
3012
 
2925
3013
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2926
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2927
- @intCast(val)
2928
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3014
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3015
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3016
+ else
3017
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2929
3018
  battle.rng.attackingDuration(player)
2930
3019
  else if (showdown)
2931
3020
  battle.rng.range(u3, 2, 4)
@@ -2933,7 +3022,7 @@ pub const Rolls = struct {
2933
3022
  @intCast((battle.rng.next() & 1) + 2);
2934
3023
 
2935
3024
  assert(duration >= 2 and duration <= 3);
2936
- options.chance.duration(.attacking, player, player, duration);
3025
+ options.chance.duration(player, duration);
2937
3026
  return duration;
2938
3027
  }
2939
3028
 
@@ -2946,9 +3035,10 @@ pub const Rolls = struct {
2946
3035
  options: anytype,
2947
3036
  ) !u3 {
2948
3037
  const roll = if (effect == .Binding) .duration else .multi_hit;
2949
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2950
- @intCast(val)
2951
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3038
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3039
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3040
+ else
3041
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2952
3042
  battle.rng.distribution(player)
2953
3043
  else if (showdown)
2954
3044
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2959,7 +3049,7 @@ pub const Rolls = struct {
2959
3049
 
2960
3050
  assert(n >= 2 and n <= 5);
2961
3051
  if (effect == .Binding) {
2962
- options.chance.duration(.binding, player, player, n);
3052
+ options.chance.duration(player, n);
2963
3053
  } else {
2964
3054
  try options.chance.multiHit(player, n);
2965
3055
  }
@@ -2974,15 +3064,13 @@ pub const Rolls = struct {
2974
3064
  options: anytype,
2975
3065
  ) !u4 {
2976
3066
  // TODO: consider throwing error instead of rerolling?
2977
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3067
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2978
3068
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2979
3069
  val
2980
3070
  else
2981
3071
  null
2982
3072
  else
2983
- null;
2984
-
2985
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3073
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2986
3074
  battle.rng.moveSlot(player, moves, check_pp)
2987
3075
  else slot: {
2988
3076
  if (showdown) {
@@ -3068,7 +3156,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3068
3156
  const stored = side.stored();
3069
3157
 
3070
3158
  // While players are not given any input options on the cartridge in these cases,
3071
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3159
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3072
3160
  //
3073
3161
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3074
3162
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3076,7 +3164,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3076
3164
  // passing due to being forced into using a move. Instead of introducing another option
3077
3165
  // we simply repurpose Move with no move slot, even though pedantically this is not
3078
3166
  // strictly correct as the player would not have been presented the option to move or
3079
- // switch at all.
3167
+ // switch at all
3080
3168
  if (isForced(active)) {
3081
3169
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3082
3170
  n += 1;
@@ -3095,7 +3183,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3095
3183
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3096
3184
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3097
3185
  // either disables all other moves in the case of limited or requires you to select a
3098
- // move normally if sleeping/frozen/bound.
3186
+ // move normally if sleeping/frozen/bound
3099
3187
  if (!showdown and (limited or foe.active.volatiles.Binding or
3100
3188
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3101
3189
  {
@@ -3108,7 +3196,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3108
3196
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3109
3197
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3110
3198
  // Move will not result in forcing the subsequent use unless the user also had the
3111
- // proc-ed move in their moveset) and disabling all other moves.
3199
+ // proc-ed move in their moveset) and disabling all other moves
3112
3200
  if (limited) {
3113
3201
  assert(showdown);
3114
3202
  assert(side.last_selected_move != .None);
@@ -3140,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3140
3228
  // Struggle (Pokémon Showdown would use 'move 1' here)
3141
3229
  if (n == before) {
3142
3230
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3143
- if (!showdown) {
3231
+ if (!pkmn.options.mod) {
3144
3232
  while (slot <= 4) : (slot += 1) {
3145
3233
  if (active.moves[slot - 1].pp != 0) return n;
3146
3234
  }