@pkmn/engine 0.1.0-dev.8b192003 → 0.1.0-dev.8cf7e679

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 (76) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +44 -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 +97 -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 +44 -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 +13 -17
  19. package/build/pkg/gen1.js +6 -10
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +38 -90
  22. package/build/pkg/index.js +6 -63
  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 +35 -69
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +141 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +141 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +349 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +18 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +254 -77
  38. package/package.json +50 -134
  39. package/src/bin/install-pkmn-engine +9 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/common/array.zig +77 -0
  42. package/src/lib/common/data.zig +8 -8
  43. package/src/lib/common/debug.zig +39 -8
  44. package/src/lib/common/js.zig +4 -2
  45. package/src/lib/common/optional.zig +13 -9
  46. package/src/lib/common/options.zig +4 -0
  47. package/src/lib/common/protocol.zig +45 -44
  48. package/src/lib/common/rational.zig +53 -51
  49. package/src/lib/common/rng.zig +5 -7
  50. package/src/lib/common/util.zig +50 -1
  51. package/src/lib/gen1/calc.zig +295 -154
  52. package/src/lib/gen1/chance.zig +477 -119
  53. package/src/lib/gen1/data/moves.zig +2 -4
  54. package/src/lib/gen1/data/species.zig +1 -3
  55. package/src/lib/gen1/data/types.zig +1 -2
  56. package/src/lib/gen1/data.zig +35 -38
  57. package/src/lib/gen1/helpers.zig +17 -27
  58. package/src/lib/gen1/mechanics.zig +248 -148
  59. package/src/lib/gen2/calc.zig +37 -27
  60. package/src/lib/gen2/chance.zig +87 -67
  61. package/src/lib/gen2/data/items.zig +1 -3
  62. package/src/lib/gen2/data/moves.zig +2 -4
  63. package/src/lib/gen2/data/species.zig +1 -3
  64. package/src/lib/gen2/data/types.zig +1 -2
  65. package/src/lib/gen2/data.zig +26 -29
  66. package/src/lib/gen2/generated.zig +18 -23
  67. package/src/lib/gen2/helpers.zig +9 -18
  68. package/src/lib/gen2/mechanics.zig +81 -81
  69. package/src/lib/node.zig +2 -4
  70. package/src/lib/pkmn.zig +11 -5
  71. package/src/lib/wasm.zig +36 -1
  72. package/src/tools/display/dom.ts +96 -0
  73. package/src/tools/display/ui.css +354 -0
  74. package/build/test/display.js +0 -400
  75. package/build/test/display.js.map +0 -1
  76. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,39 +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;
7
- const optional = @import("../common/optional.zig");
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
8
8
  const protocol = @import("../common/protocol.zig");
9
9
  const rng = @import("../common/rng.zig");
10
-
11
- const data = @import("data.zig");
12
- const chance = @import("chance.zig");
10
+ const std = @import("std");
13
11
 
14
12
  const assert = std.debug.assert;
15
-
16
- const expectEqual = std.testing.expectEqual;
17
-
18
- const showdown = pkmn.options.showdown;
19
-
20
- const Choice = common.Choice;
21
- const ID = common.ID;
22
- const Player = common.Player;
23
- const Result = common.Result;
24
-
25
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
26
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
27
18
  const Fail = protocol.Fail;
28
- const Heal = protocol.Heal;
29
-
30
19
  const Gen12 = rng.Gen12;
31
-
32
- const ActivePokemon = data.ActivePokemon;
33
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
34
22
  const Move = data.Move;
35
23
  const MoveSlot = data.MoveSlot;
36
- const Pokemon = data.Pokemon;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
37
27
  const Side = data.Side;
38
28
  const Species = data.Species;
39
29
  const Stats = data.Stats;
@@ -176,7 +166,10 @@ fn selectMove(
176
166
  if (foe_choice.type == .Switch) {
177
167
  const last = side.active.move(battle.lastMove(player).index);
178
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
179
- 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
+ }
180
173
  }
181
174
  }
182
175
  return null;
@@ -255,6 +248,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
255
248
  active.moves = incoming.moves;
256
249
 
257
250
  statusModify(incoming.status, &active.stats);
251
+ options.chance.switched(player, slot);
258
252
 
259
253
  foe.active.volatiles.Binding = false;
260
254
 
@@ -354,7 +348,7 @@ fn doTurn(
354
348
  if (try checkFaint(battle, player, false, options)) |r| return r;
355
349
  } else if (foe_choice.type == .Pass) return null;
356
350
 
357
- options.chance.clearPending();
351
+ if (!showdown) options.chance.clearPending();
358
352
 
359
353
  residual = true;
360
354
  replace = battle.side(foe_player).stored().hp == 0;
@@ -426,7 +420,7 @@ fn executeMove(
426
420
  mslot = @intCast(battle.lastMove(player).index);
427
421
  const stored = side.stored();
428
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
429
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
430
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
431
425
  side.last_selected_move == .HyperBeam or
432
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -459,6 +453,25 @@ fn executeMove(
459
453
  .err => return @as(?Result, Result.Error),
460
454
  }
461
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(battle.lastMove(player).index).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
+
462
475
  const can = skip_can or
463
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
464
477
  if (!can) return null;
@@ -485,11 +498,22 @@ fn beforeMove(
485
498
 
486
499
  if (Status.is(stored.status, .SLP)) {
487
500
  const before = stored.status;
501
+ const slf = Status.is(stored.status, .EXT);
488
502
  // Even if the EXT bit is set this will still correctly modify the sleep duration
489
- stored.status -= 1;
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,
509
+ } else {
510
+ stored.status -= 1;
511
+ }
490
512
 
491
513
  const duration = Status.duration(stored.status);
492
- options.chance.durations(.sleep, player, observation(duration));
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
493
517
 
494
518
  if (duration == 0) {
495
519
  try log.curestatus(.{ ident, before, .Message });
@@ -527,8 +551,12 @@ fn beforeMove(
527
551
  }
528
552
 
529
553
  if (volatiles.disable_duration > 0) {
530
- volatiles.disable_duration -= 1;
531
- options.chance.durations(.disable, player, observation(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
+ );
532
560
 
533
561
  if (volatiles.disable_duration == 0) {
534
562
  volatiles.disable_move = 0;
@@ -541,14 +569,24 @@ fn beforeMove(
541
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
542
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
543
571
  if (side.last_selected_move == .None) {
544
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
545
573
  return .err;
546
574
  }
547
575
 
548
576
  if (volatiles.Confusion) {
549
577
  assert(volatiles.confusion > 0);
550
- volatiles.confusion -= 1;
551
- options.chance.durations(.confusion, player, observation(volatiles.confusion));
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,
585
+ } else {
586
+ volatiles.confusion -= 1;
587
+ }
588
+
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
552
590
 
553
591
  if (volatiles.confusion == 0) {
554
592
  volatiles.Confusion = false;
@@ -567,9 +605,8 @@ fn beforeMove(
567
605
  volatiles.Binding = false;
568
606
  volatiles.Invulnerable = false;
569
607
 
570
- options.chance.durations(.bide, player, .None);
571
- options.chance.durations(.thrashing, player, .None);
572
- options.chance.durations(.binding, player, .None);
608
+ options.chance.observe(.attacking, player, .None);
609
+ options.chance.observe(.binding, player, .None);
573
610
  {
574
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
575
612
  // the opponent's defense with the user's defense and resets it after. As a
@@ -577,7 +614,9 @@ fn beforeMove(
577
614
  const def = foe.active.stats.def;
578
615
  foe.active.stats.def = active.stats.def;
579
616
  defer foe.active.stats.def = def;
580
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
581
620
  }
582
621
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
583
622
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -607,9 +646,8 @@ fn beforeMove(
607
646
  volatiles.Charging = false;
608
647
  volatiles.Binding = false;
609
648
 
610
- options.chance.durations(.bide, player, .None);
611
- options.chance.durations(.thrashing, player, .None);
612
- options.chance.durations(.binding, player, .None);
649
+ options.chance.observe(.attacking, player, .None);
650
+ options.chance.observe(.binding, player, .None);
613
651
 
614
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
615
653
  try log.cant(.{ ident, .Paralysis });
@@ -628,8 +666,8 @@ fn beforeMove(
628
666
  volatiles.state +%= battle.last_damage;
629
667
  }
630
668
 
631
- volatiles.attacks -= 1;
632
- options.chance.durations(.bide, player, observation(volatiles.attacks));
669
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
670
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
633
671
 
634
672
  if (volatiles.attacks != 0) {
635
673
  try log.activate(.{ ident, .Bide });
@@ -665,14 +703,12 @@ fn beforeMove(
665
703
  if (volatiles.Thrashing) {
666
704
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
667
705
 
668
- volatiles.attacks -= 1;
669
- options.chance.durations(
670
- .thrashing,
671
- player,
672
- if (volatiles.attacks <= 1) .ended else .continuing,
673
- );
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
674
708
 
675
709
  if (volatiles.attacks == 0) {
710
+ const overwritten = volatiles.Confusion;
711
+
676
712
  volatiles.Thrashing = false;
677
713
  volatiles.Confusion = true;
678
714
  volatiles.confusion = Rolls.confusionDuration(battle, player, options);
@@ -682,6 +718,11 @@ fn beforeMove(
682
718
  // can be expected to recognize this pattern and pass the information only to the
683
719
  // correct player)
684
720
  try log.start(.{ battle.active(player), .ConfusionSilent });
721
+ options.chance.observe(.confusion, player, if (overwritten)
722
+ if (pkmn.options.overwrite) .overwritten else .continuing
723
+ else
724
+ .started);
725
+ if (!overwritten) options.calc.confusion(player);
685
726
  }
686
727
 
687
728
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -692,8 +733,8 @@ fn beforeMove(
692
733
  }
693
734
 
694
735
  if (volatiles.Binding) {
695
- volatiles.attacks -= 1;
696
- options.chance.durations(.binding, player, observation(volatiles.attacks));
736
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
737
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
697
738
 
698
739
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
699
740
  if (showdown or battle.last_damage != 0) {
@@ -747,9 +788,14 @@ fn canMove(
747
788
 
748
789
  if (move.effect == .Thrashing) {
749
790
  Effects.thrashing(battle, player, options);
750
- } else if (!showdown and move.effect == .Binding) {
751
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
752
- try Effects.binding(battle, player, false, options);
791
+ } else if (move.effect == .Binding) {
792
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
793
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
794
+ if (showdown) {
795
+ battle.foe(player).active.volatiles.Recharging = false;
796
+ } else {
797
+ try Effects.binding(battle, player, false, options);
798
+ }
753
799
  }
754
800
 
755
801
  return true;
@@ -786,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
786
832
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
787
833
  }
788
834
 
789
- fn incrementPP(side: *Side, mslot: u4) void {
835
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
790
836
  var active = &side.active;
791
837
  const volatiles = &active.volatiles;
792
838
 
@@ -794,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
794
840
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
795
841
  if (showdown and volatiles.Transform) return;
796
842
 
843
+ const n = options.calc.overridden(player, .pp) orelse 1;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
845
+
797
846
  assert(mslot > 0 and mslot <= 4);
798
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
847
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
799
848
  }
800
849
 
801
850
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -849,7 +898,7 @@ fn doMove(
849
898
 
850
899
  var late = showdown and move.effect != .Explode;
851
900
  const skip = status or immune;
852
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
853
902
  if (showdown and move.effect.isMulti()) {
854
903
  try Effects.multiHit(battle, player, move, options);
855
904
  hits = side.active.volatiles.attacks;
@@ -871,8 +920,9 @@ fn doMove(
871
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
872
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
873
922
  battle.last_damage = if (ohko) 65535 else 0;
874
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
875
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
923
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
924
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
925
+ if (!showdown) try options.chance.commit(player, .err);
876
926
  return @as(?Result, Result.Error);
877
927
  }
878
928
  if (battle.last_damage == 0) {
@@ -886,11 +936,12 @@ fn doMove(
886
936
  }
887
937
  }
888
938
 
939
+ var missed: bool = undefined;
889
940
  const zero = battle.last_damage == 0;
890
- miss = if (showdown or skip)
891
- miss
892
- else
893
- (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
941
+ if (!showdown and !skip) {
942
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
943
+ miss = missed or zero;
944
+ }
894
945
 
895
946
  assert(showdown or miss or battle.last_damage > 0 or skip);
896
947
  assert((!showdown and miss) or !(ohko and immune));
@@ -917,11 +968,32 @@ fn doMove(
917
968
  try log.immune(.{ foe_ident, .OHKO });
918
969
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
919
970
  try log.immune(.{ foe_ident, .None });
971
+ if (!showdown) {
972
+ if (move.effect == .Binding) {
973
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
974
+ // then simply clears it later on if the move happens to miss in moveHit.
975
+ // However, moveHit can return true and we'll still end up in the outer miss
976
+ // handling block because attacking into an immune Pokemon does zero damage
977
+ // (considered missing)
978
+ assert(zero);
979
+ // Thus we still need to actually save the hit result (but not the crit roll).
980
+ // Using .miss for this commit would accomplish what we want, but is somewhat
981
+ // misleading as its entirely possible the move actually did hit (and the target
982
+ // is now trapped) so we use the .binding value instead which is effectively
983
+ // identical, just doesn't claim to know about the actual results of the hit
984
+ // roll)
985
+ try options.chance.commit(player, .binding);
986
+ } else {
987
+ // The .glitch option allows for conditionally committing if we are in a
988
+ // division-by-zero freeze alternative timeline scenario
989
+ try options.chance.commit(player, .glitch);
990
+ }
991
+ }
920
992
  } else if (mist) {
921
993
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
922
994
  try log.fail(.{ foe_ident, .None });
923
995
  } else {
924
- if (!showdown and !zero) try options.chance.commit(player, .miss);
996
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
925
997
  try log.lastmiss(.{});
926
998
  try log.miss(.{battle.active(player)});
927
999
  }
@@ -983,22 +1055,20 @@ fn doMove(
983
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
984
1056
  }
985
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
986
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
987
1059
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
988
- if (move.effect == .Binding) {
989
- try Effects.binding(battle, player, rewrap, options);
990
- if (immune) {
991
- battle.last_damage = 0;
992
- assert(foe.stored().hp > 0);
993
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
994
- if (sub) {
995
- try log.activate(.{ battle.active(player.foe()), .Substitute });
996
- } else {
997
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
998
- try buildRage(battle, player.foe(), options);
999
- }
1000
- return null;
1060
+ try Effects.binding(battle, player, rewrap, options);
1061
+ if (immune) {
1062
+ battle.last_damage = 0;
1063
+ assert(foe.stored().hp > 0);
1064
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1065
+ if (sub) {
1066
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1067
+ } else {
1068
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1069
+ try buildRage(battle, player.foe(), options);
1001
1070
  }
1071
+ return null;
1002
1072
  }
1003
1073
  }
1004
1074
 
@@ -1058,6 +1128,7 @@ fn calcDamage(
1058
1128
  target_player: Player,
1059
1129
  m: ?Move.Data,
1060
1130
  crit: bool,
1131
+ options: anytype,
1061
1132
  ) bool {
1062
1133
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1063
1134
  const cfz = m == null;
@@ -1069,28 +1140,8 @@ fn calcDamage(
1069
1140
 
1070
1141
  const special = move.type.special();
1071
1142
 
1072
- // zig fmt: off
1073
- var atk: u32 =
1074
- if (crit)
1075
- if (special) side.stored().stats.spc
1076
- else side.stored().stats.atk
1077
- else
1078
- if (special) side.active.stats.spc
1079
- else side.active.stats.atk;
1080
- var def: u32 =
1081
- if (crit)
1082
- if (special) target.stored().stats.spc
1083
- else target.stored().stats.def
1084
- else
1085
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1086
- if (special)
1087
- target.active.stats.spc *
1088
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1089
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1090
- else
1091
- target.active.stats.def *
1092
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1093
- // zig fmt: on
1143
+ var atk = attack(side, crit, special);
1144
+ var def = defense(target, crit, special, cfz);
1094
1145
 
1095
1146
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1096
1147
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1098,7 +1149,7 @@ fn calcDamage(
1098
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1099
1150
  atk = @max((atk / 4) & 255, 1);
1100
1151
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1101
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1102
1153
  }
1103
1154
 
1104
1155
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1115,11 +1166,41 @@ fn calcDamage(
1115
1166
  d = @min(997, d);
1116
1167
  d += 2;
1117
1168
 
1169
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1170
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1171
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1172
+ // that results in counter intuitive probabilities in the common case
1173
+ if (pkmn.options.chance and !pkmn.options.mod) {
1174
+ atk = attack(side, !crit, special);
1175
+ def = defense(target, !crit, special, false);
1176
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1177
+ }
1178
+
1118
1179
  battle.last_damage = @intCast(d);
1119
1180
 
1120
1181
  return true;
1121
1182
  }
1122
1183
 
1184
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1185
+ return if (crit)
1186
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1187
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1188
+ }
1189
+
1190
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1191
+ return if (crit)
1192
+ if (special) target.stored().stats.spc else target.stored().stats.def
1193
+ else
1194
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1195
+ if (special)
1196
+ target.active.stats.spc *
1197
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1198
+ else
1199
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1200
+ target.active.stats.def *
1201
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1202
+ }
1203
+
1123
1204
  fn adjustDamage(battle: anytype, player: Player) u16 {
1124
1205
  const side = battle.side(player);
1125
1206
  const foe = battle.foe(player);
@@ -1174,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1174
1255
  .Psywave => power: {
1175
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1176
1257
  // GLITCH: Psywave infinite glitch loop
1177
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1178
1259
  break :power try Rolls.psywave(battle, player, max, options);
1179
1260
  },
1180
1261
  else => unreachable,
1181
1262
  };
1182
1263
 
1183
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1264
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1184
1265
 
1185
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1186
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1217,12 +1298,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1217
1298
 
1218
1299
  if (!used or !selected) {
1219
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1220
- if (!showdown) return Result.Error;
1301
+ if (!pkmn.options.mod) return Result.Error;
1221
1302
  try options.log.fail(.{ battle.active(player), .None });
1222
1303
  return null;
1223
1304
  }
1224
1305
 
1225
- battle.last_damage *|= 2;
1306
+ // NOTE: llvm/llvm-project#58557
1307
+ battle.last_damage = if (comptime builtin.target.isWasm())
1308
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1309
+ else
1310
+ battle.last_damage *| 2;
1226
1311
 
1227
1312
  // Pokémon Showdown calls moveHit before Counter
1228
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1254,7 +1339,7 @@ fn applyDamage(
1254
1339
  subbed.active.volatiles.Substitute = false;
1255
1340
  // battle.last_damage is not updated with the amount of HP the Substitute had
1256
1341
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1257
- options.calc.capped(sub_player);
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1258
1343
  return true;
1259
1344
  } else {
1260
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1288,7 +1373,7 @@ fn mirrorMove(
1288
1373
  try options.log.fail(.{ battle.active(player), .None });
1289
1374
  return null;
1290
1375
  } else if (!showdown or foe.last_used_move != .Struggle) {
1291
- incrementPP(side, mslot);
1376
+ incrementPP(side, player, mslot, options);
1292
1377
  }
1293
1378
 
1294
1379
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1309,7 +1394,7 @@ fn metronome(
1309
1394
  var side = battle.side(player);
1310
1395
 
1311
1396
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1312
- incrementPP(side, mslot);
1397
+ incrementPP(side, player, mslot, options);
1313
1398
 
1314
1399
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1315
1400
  return null;
@@ -1410,7 +1495,10 @@ fn moveHit(
1410
1495
 
1411
1496
  if (!miss) return true;
1412
1497
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1498
+
1413
1499
  side.active.volatiles.Binding = false;
1500
+ options.chance.observe(.binding, player, .None);
1501
+
1414
1502
  return false;
1415
1503
  }
1416
1504
 
@@ -1461,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1461
1549
  assert(!foe_volatiles.MultiHit);
1462
1550
  if (foe_volatiles.Bide) {
1463
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1464
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1552
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1465
1553
  if (foe_volatiles.state != 0) return Result.Error;
1466
1554
  }
1467
1555
 
@@ -1552,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1552
1640
 
1553
1641
  battle.turn += 1;
1554
1642
 
1555
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1556
1644
  try options.log.tie(.{});
1557
1645
  return Result.Tie;
1558
1646
  }
1559
1647
 
1560
- if (showdown and battle.turn >= 1000) {
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1561
1649
  try options.log.tie(.{});
1562
1650
  return Result.Tie;
1563
1651
  } else if (battle.turn >= 65535) {
@@ -1737,7 +1825,7 @@ pub const Effects = struct {
1737
1825
 
1738
1826
  side.active.volatiles.state = 0;
1739
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1740
- options.chance.durations(.bide, player, .started);
1828
+ options.chance.observe(.attacking, player, .started);
1741
1829
 
1742
1830
  try options.log.start(.{ battle.active(player), .Bide });
1743
1831
  }
@@ -1805,7 +1893,8 @@ pub const Effects = struct {
1805
1893
  foe.active.volatiles.Confusion = true;
1806
1894
 
1807
1895
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1808
- options.chance.durations(.confusion, player.foe(), .started);
1896
+ options.chance.observe(.confusion, player.foe(), .started);
1897
+ options.calc.confusion(player.foe());
1809
1898
 
1810
1899
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1811
1900
  }
@@ -1851,19 +1940,18 @@ pub const Effects = struct {
1851
1940
 
1852
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1853
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1854
- if (n == 0) {
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1855
1944
  try options.log.fail(.{ foe_ident, .None });
1856
1945
  return null;
1857
1946
  } else if (err) {
1858
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1859
- assert(!showdown);
1860
1948
  return Result.Error;
1861
1949
  }
1862
1950
 
1863
1951
  volatiles.disable_move =
1864
1952
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1865
1953
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1866
- options.chance.durations(.disable, player.foe(), .started);
1954
+ options.chance.observe(.disable, player.foe(), .started);
1867
1955
 
1868
1956
  const id = foe.active.move(volatiles.disable_move).id;
1869
1957
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1934,7 +2022,7 @@ pub const Effects = struct {
1934
2022
  if (foe.active.types.includes(move.type)) return;
1935
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1936
2024
  // Freeze Clause Mod
1937
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2025
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1938
2026
 
1939
2027
  foe_stored.status = Status.init(.FRZ);
1940
2028
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -1970,7 +2058,7 @@ pub const Effects = struct {
1970
2058
  if (p != player and Status.any(s.stored().status)) {
1971
2059
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1972
2060
  s.stored().status = 0;
1973
- options.chance.durations(.sleep, p, .None);
2061
+ options.chance.observe(.sleep, p, .None);
1974
2062
  } else if (showdown and s.stored().status == Status.TOX) {
1975
2063
  s.stored().status = Status.init(.PSN);
1976
2064
  try log.status(.{ battle.active(p), s.stored().status, .None });
@@ -1981,7 +2069,7 @@ pub const Effects = struct {
1981
2069
  if (Status.any(foe_stored.status)) {
1982
2070
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
1983
2071
  foe.last_selected_move = .SKIP_TURN;
1984
- options.chance.durations(.sleep, player.foe(), .None);
2072
+ options.chance.observe(.sleep, player.foe(), .None);
1985
2073
  }
1986
2074
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1987
2075
  foe_stored.status = 0;
@@ -2275,7 +2363,7 @@ pub const Effects = struct {
2275
2363
  }
2276
2364
 
2277
2365
  // Sleep Clause Mod
2278
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2279
2367
  for (foe.pokemon) |p| {
2280
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2281
2369
  }
@@ -2283,7 +2371,7 @@ pub const Effects = struct {
2283
2371
  foe.active.volatiles.Recharging = false;
2284
2372
 
2285
2373
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2286
- options.chance.durations(.sleep, player.foe(), .started);
2374
+ options.chance.observe(.sleep, player.foe(), .started);
2287
2375
 
2288
2376
  const last = battle.side(player).last_selected_move;
2289
2377
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
@@ -2342,7 +2430,7 @@ pub const Effects = struct {
2342
2430
  volatiles.Thrashing = true;
2343
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2344
2432
 
2345
- options.chance.durations(.thrashing, player, .started);
2433
+ options.chance.observe(.attacking, player, .started);
2346
2434
  }
2347
2435
 
2348
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2381,14 +2469,14 @@ pub const Effects = struct {
2381
2469
  if (side.active.volatiles.Binding) return;
2382
2470
  side.active.volatiles.Binding = true;
2383
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2384
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2385
2473
  if (!showdown) {
2386
2474
  foe.active.volatiles.Recharging = false;
2387
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2388
2476
 
2389
2477
  side.active.volatiles.attacks =
2390
2478
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2391
- options.chance.durations(.binding, player, .started);
2479
+ options.chance.observe(.binding, player, .started);
2392
2480
  }
2393
2481
 
2394
2482
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2627,13 +2715,13 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2627
2715
  if (volatiles.disable_move != 0) {
2628
2716
  volatiles.disable_move = 0;
2629
2717
  volatiles.disable_duration = 0;
2630
- options.chance.durations(.disable, who, .None);
2718
+ options.chance.observe(.disable, who, .None);
2631
2719
  try log.end(.{ ident, .DisableSilent });
2632
2720
  }
2633
2721
  if (volatiles.Confusion) {
2634
2722
  // volatiles.confusion is left unchanged
2635
2723
  volatiles.Confusion = false;
2636
- options.chance.durations(.confusion, who, .None);
2724
+ options.chance.observe(.confusion, who, .None);
2637
2725
  try log.end(.{ ident, .ConfusionSilent });
2638
2726
  }
2639
2727
  if (volatiles.Mist) {
@@ -2668,8 +2756,17 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2668
2756
  }
2669
2757
  }
2670
2758
 
2671
- fn observation(n: anytype) optional.Optional(chance.Observation) {
2672
- return if (n == 0) .ended else .continuing;
2759
+ fn decrement(
2760
+ comptime field: chance.Action.Field,
2761
+ player: Player,
2762
+ options: anytype,
2763
+ n: anytype,
2764
+ ) @TypeOf(n) {
2765
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2766
+ .started, .ended => 0,
2767
+ .continuing => if (n > 1) n - 1 else n,
2768
+ else => unreachable,
2769
+ } else n - 1;
2673
2770
  }
2674
2771
 
2675
2772
  pub const Rolls = struct {
@@ -2869,9 +2966,10 @@ pub const Rolls = struct {
2869
2966
  }
2870
2967
 
2871
2968
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2872
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2873
- @intCast(val)
2874
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2969
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2970
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2971
+ else
2972
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2875
2973
  battle.rng.sleepDuration(player)
2876
2974
  else if (showdown)
2877
2975
  battle.rng.range(u3, 1, 8)
@@ -2888,9 +2986,10 @@ pub const Rolls = struct {
2888
2986
  }
2889
2987
 
2890
2988
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2891
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2892
- val
2893
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2989
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2990
+ if (val >= 1 and val <= 8) val else null
2991
+ else
2992
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2894
2993
  battle.rng.disableDuration(player)
2895
2994
  else if (showdown)
2896
2995
  battle.rng.range(u4, 1, 9)
@@ -2903,9 +3002,10 @@ pub const Rolls = struct {
2903
3002
  }
2904
3003
 
2905
3004
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
- @intCast(val)
2908
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3005
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3006
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3007
+ else
3008
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2909
3009
  battle.rng.confusionDuration(player)
2910
3010
  else if (showdown)
2911
3011
  battle.rng.range(u3, 2, 6)
@@ -2918,9 +3018,10 @@ pub const Rolls = struct {
2918
3018
  }
2919
3019
 
2920
3020
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2921
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2922
- @intCast(val)
2923
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3021
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3022
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3023
+ else
3024
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2924
3025
  battle.rng.attackingDuration(player)
2925
3026
  else if (showdown)
2926
3027
  battle.rng.range(u3, 2, 4)
@@ -2941,9 +3042,10 @@ pub const Rolls = struct {
2941
3042
  options: anytype,
2942
3043
  ) !u3 {
2943
3044
  const roll = if (effect == .Binding) .duration else .multi_hit;
2944
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2945
- @intCast(val)
2946
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3045
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3046
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3047
+ else
3048
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2947
3049
  battle.rng.distribution(player)
2948
3050
  else if (showdown)
2949
3051
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -2969,15 +3071,13 @@ pub const Rolls = struct {
2969
3071
  options: anytype,
2970
3072
  ) !u4 {
2971
3073
  // TODO: consider throwing error instead of rerolling?
2972
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
2973
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
2974
3076
  val
2975
3077
  else
2976
3078
  null
2977
3079
  else
2978
- null;
2979
-
2980
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3080
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2981
3081
  battle.rng.moveSlot(player, moves, check_pp)
2982
3082
  else slot: {
2983
3083
  if (showdown) {
@@ -3135,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3135
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3136
3236
  if (n == before) {
3137
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3138
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3139
3239
  while (slot <= 4) : (slot += 1) {
3140
3240
  if (active.moves[slot - 1].pp != 0) return n;
3141
3241
  }