@pkmn/engine 0.1.0-dev.b01cb48c → 0.1.0-dev.b3197aa3

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 (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -6
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +25 -3
  13. package/build/pkg/addon.js +34 -65
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +23 -1
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -90
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +105 -103
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +52 -76
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +331 -85
  39. package/package.json +56 -136
  40. package/src/bin/install-pkmn-engine +10 -7
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +255 -0
  43. package/src/lib/bindings/node.zig +117 -0
  44. package/src/lib/bindings/wasm.zig +59 -0
  45. package/src/lib/common/array.zig +77 -0
  46. package/src/lib/common/data.zig +8 -8
  47. package/src/lib/common/debug.zig +39 -8
  48. package/src/lib/common/js.zig +4 -6
  49. package/src/lib/common/napi.zig +4 -0
  50. package/src/lib/common/optional.zig +13 -9
  51. package/src/lib/common/options.zig +4 -0
  52. package/src/lib/common/protocol.zig +44 -43
  53. package/src/lib/common/rational.zig +317 -304
  54. package/src/lib/common/rng.zig +5 -7
  55. package/src/lib/common/util.zig +65 -1
  56. package/src/lib/gen1/calc.zig +266 -135
  57. package/src/lib/gen1/chance.zig +469 -110
  58. package/src/lib/gen1/data/moves.zig +2 -4
  59. package/src/lib/gen1/data/species.zig +1 -3
  60. package/src/lib/gen1/data/types.zig +2 -3
  61. package/src/lib/gen1/data.zig +35 -38
  62. package/src/lib/gen1/helpers.zig +17 -27
  63. package/src/lib/gen1/mechanics.zig +213 -160
  64. package/src/lib/gen2/calc.zig +37 -27
  65. package/src/lib/gen2/chance.zig +87 -67
  66. package/src/lib/gen2/data/items.zig +1 -3
  67. package/src/lib/gen2/data/moves.zig +2 -4
  68. package/src/lib/gen2/data/species.zig +1 -3
  69. package/src/lib/gen2/data/types.zig +7 -5
  70. package/src/lib/gen2/data.zig +28 -30
  71. package/src/lib/gen2/generated.zig +18 -23
  72. package/src/lib/gen2/helpers.zig +9 -18
  73. package/src/lib/gen2/mechanics.zig +82 -82
  74. package/src/lib/node.zig +4 -121
  75. package/src/lib/pkmn.zig +14 -7
  76. package/src/lib/wasm.zig +13 -9
  77. package/src/tools/display/dom.ts +93 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -405
  80. package/build/test/display.js.map +0 -1
  81. 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(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
+
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,6 +498,7 @@ 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
503
  if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
490
504
  .started, .ended => stored.status = 0,
@@ -497,7 +511,9 @@ fn beforeMove(
497
511
  }
498
512
 
499
513
  const duration = Status.duration(stored.status);
500
- options.chance.observe(.sleep, player, observation(duration));
514
+ try options.chance.sleep(player, if (slf)
515
+ .None
516
+ else if (duration == 0) .ended else .continuing);
501
517
 
502
518
  if (duration == 0) {
503
519
  try log.curestatus(.{ ident, before, .Message });
@@ -537,7 +553,10 @@ fn beforeMove(
537
553
  if (volatiles.disable_duration > 0) {
538
554
  volatiles.disable_duration =
539
555
  decrement(.disable, player, options, volatiles.disable_duration);
540
- options.chance.observe(.disable, player, observation(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,25 +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
578
  if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
560
- .started => volatiles.confusion -= 1,
579
+ .started, .ended => volatiles.confusion = 0,
561
580
  .continuing => if (volatiles.confusion > 1) {
562
581
  volatiles.confusion -= 1;
563
582
  },
564
- .ended => volatiles.confusion = 0,
565
583
  .overwritten => {},
566
584
  else => unreachable,
567
585
  } else {
568
586
  volatiles.confusion -= 1;
569
587
  }
570
588
 
571
- options.chance.observe(.confusion, player, observation(volatiles.confusion));
589
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
572
590
 
573
591
  if (volatiles.confusion == 0) {
574
592
  volatiles.Confusion = false;
@@ -587,8 +605,7 @@ fn beforeMove(
587
605
  volatiles.Binding = false;
588
606
  volatiles.Invulnerable = false;
589
607
 
590
- options.chance.observe(.bide, player, .None);
591
- options.chance.observe(.thrashing, player, .None);
608
+ options.chance.observe(.attacking, player, .None);
592
609
  options.chance.observe(.binding, player, .None);
593
610
  {
594
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
@@ -597,19 +614,14 @@ fn beforeMove(
597
614
  const def = foe.active.stats.def;
598
615
  foe.active.stats.def = active.stats.def;
599
616
  defer foe.active.stats.def = def;
600
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
617
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
618
+ return .err;
619
+ }
601
620
  }
602
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
603
- // on the targeting behavior of the confused Pokémon's selected move which results
604
- // in the wrong behavior with respect to the Substitute + Confusion glitch
605
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
606
- player
607
- else
608
- player.foe();
609
621
 
610
622
  const uncapped = battle.last_damage;
611
623
  // Skipping adjustDamage / randomizeDamage / checkHit
612
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
613
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
614
626
  if (showdown) battle.last_damage = uncapped;
615
627
 
@@ -627,8 +639,7 @@ fn beforeMove(
627
639
  volatiles.Charging = false;
628
640
  volatiles.Binding = false;
629
641
 
630
- options.chance.observe(.bide, player, .None);
631
- options.chance.observe(.thrashing, player, .None);
642
+ options.chance.observe(.attacking, player, .None);
632
643
  options.chance.observe(.binding, player, .None);
633
644
 
634
645
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
@@ -648,8 +659,8 @@ fn beforeMove(
648
659
  volatiles.state +%= battle.last_damage;
649
660
  }
650
661
 
651
- volatiles.attacks = decrement(.bide, player, options, volatiles.attacks);
652
- options.chance.observe(.bide, player, observation(volatiles.attacks));
662
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
663
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
653
664
 
654
665
  if (volatiles.attacks != 0) {
655
666
  try log.activate(.{ ident, .Bide });
@@ -685,8 +696,8 @@ fn beforeMove(
685
696
  if (volatiles.Thrashing) {
686
697
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
687
698
 
688
- volatiles.attacks = decrement(.thrashing, player, options, volatiles.attacks);
689
- options.chance.observe(.thrashing, player, observation(volatiles.attacks));
699
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
700
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
690
701
 
691
702
  if (volatiles.attacks == 0) {
692
703
  const overwritten = volatiles.Confusion;
@@ -700,11 +711,11 @@ fn beforeMove(
700
711
  // can be expected to recognize this pattern and pass the information only to the
701
712
  // correct player)
702
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
703
- options.chance.observe(
704
- .confusion,
705
- player,
706
- if (overwritten) .overwritten else .started,
707
- );
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);
708
719
  }
709
720
 
710
721
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -716,7 +727,7 @@ fn beforeMove(
716
727
 
717
728
  if (volatiles.Binding) {
718
729
  volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
719
- options.chance.observe(.binding, player, observation(volatiles.attacks));
730
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
720
731
 
721
732
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
722
733
  if (showdown or battle.last_damage != 0) {
@@ -770,9 +781,14 @@ fn canMove(
770
781
 
771
782
  if (move.effect == .Thrashing) {
772
783
  Effects.thrashing(battle, player, options);
773
- } else if (!showdown and move.effect == .Binding) {
774
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
775
- 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
+ }
776
792
  }
777
793
 
778
794
  return true;
@@ -809,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
809
825
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
810
826
  }
811
827
 
812
- fn incrementPP(side: *Side, mslot: u4) void {
828
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
813
829
  var active = &side.active;
814
830
  const volatiles = &active.volatiles;
815
831
 
@@ -817,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
817
833
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
818
834
  if (showdown and volatiles.Transform) return;
819
835
 
836
+ const n = options.calc.overridden(player, .pp) orelse 1;
837
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
838
+
820
839
  assert(mslot > 0 and mslot <= 4);
821
- 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;
822
841
  }
823
842
 
824
843
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -872,7 +891,7 @@ fn doMove(
872
891
 
873
892
  var late = showdown and move.effect != .Explode;
874
893
  const skip = status or immune;
875
- if ((!showdown or (!skip or counter)) and !miss) blk: {
894
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
876
895
  if (showdown and move.effect.isMulti()) {
877
896
  try Effects.multiHit(battle, player, move, options);
878
897
  hits = side.active.volatiles.attacks;
@@ -894,8 +913,9 @@ fn doMove(
894
913
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
895
914
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
896
915
  battle.last_damage = if (ohko) 65535 else 0;
897
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
898
- } 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);
899
919
  return @as(?Result, Result.Error);
900
920
  }
901
921
  if (battle.last_damage == 0) {
@@ -909,11 +929,12 @@ fn doMove(
909
929
  }
910
930
  }
911
931
 
932
+ var missed: bool = undefined;
912
933
  const zero = battle.last_damage == 0;
913
- miss = if (showdown or skip)
914
- miss
915
- else
916
- (!try 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
+ }
917
938
 
918
939
  assert(showdown or miss or battle.last_damage > 0 or skip);
919
940
  assert((!showdown and miss) or !(ohko and immune));
@@ -940,24 +961,32 @@ fn doMove(
940
961
  try log.immune(.{ foe_ident, .OHKO });
941
962
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
942
963
  try log.immune(.{ foe_ident, .None });
943
- if (!showdown and move.effect == .Binding) {
944
- // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and then
945
- // simply clears it later on if the move happens to miss in moveHit. However,
946
- // moveHit can return true and we'll still end up in the outer miss handling block
947
- // because attacking into an immune Pokemon does zero damage (considered missing)
948
- assert(zero);
949
- // Thus we still need to actually save the hit result (but not the crit roll). Using
950
- // .miss for this commit would accomplish what we want, but is somewhat misleading
951
- // as its entirely possible the move actually did hit (and the target is now
952
- // trapped) so we use the .binding value instead which is effectively identical,
953
- // just doesn't claim to know about the actual results of the hit roll)
954
- try options.chance.commit(player, .binding);
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
+ }
955
984
  }
956
985
  } else if (mist) {
957
986
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
958
987
  try log.fail(.{ foe_ident, .None });
959
988
  } else {
960
- if (!showdown and !zero) try options.chance.commit(player, .miss);
989
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
961
990
  try log.lastmiss(.{});
962
991
  try log.miss(.{battle.active(player)});
963
992
  }
@@ -1019,22 +1048,20 @@ fn doMove(
1019
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1020
1049
  }
1021
1050
  try log.hitcount(.{ battle.active(player.foe()), hit });
1022
- } else if (showdown) {
1051
+ } else if (showdown and move.effect == .Binding) {
1023
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1024
- if (move.effect == .Binding) {
1025
- try Effects.binding(battle, player, rewrap, options);
1026
- if (immune) {
1027
- battle.last_damage = 0;
1028
- assert(foe.stored().hp > 0);
1029
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1030
- if (sub) {
1031
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1032
- } else {
1033
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1034
- try buildRage(battle, player.foe(), options);
1035
- }
1036
- 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);
1037
1063
  }
1064
+ return null;
1038
1065
  }
1039
1066
  }
1040
1067
 
@@ -1094,6 +1121,7 @@ fn calcDamage(
1094
1121
  target_player: Player,
1095
1122
  m: ?Move.Data,
1096
1123
  crit: bool,
1124
+ options: anytype,
1097
1125
  ) bool {
1098
1126
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1099
1127
  const cfz = m == null;
@@ -1105,28 +1133,8 @@ fn calcDamage(
1105
1133
 
1106
1134
  const special = move.type.special();
1107
1135
 
1108
- // zig fmt: off
1109
- var atk: u32 =
1110
- if (crit)
1111
- if (special) side.stored().stats.spc
1112
- else side.stored().stats.atk
1113
- else
1114
- if (special) side.active.stats.spc
1115
- else side.active.stats.atk;
1116
- var def: u32 =
1117
- if (crit)
1118
- if (special) target.stored().stats.spc
1119
- else target.stored().stats.def
1120
- else
1121
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1122
- if (special)
1123
- target.active.stats.spc *
1124
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1125
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1126
- else
1127
- target.active.stats.def *
1128
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1129
- // zig fmt: on
1136
+ var atk = attack(side, crit, special);
1137
+ var def = defense(target, crit, special, cfz);
1130
1138
 
1131
1139
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1132
1140
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1134,7 +1142,7 @@ fn calcDamage(
1134
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1135
1143
  atk = @max((atk / 4) & 255, 1);
1136
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1137
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1138
1146
  }
1139
1147
 
1140
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1151,11 +1159,41 @@ fn calcDamage(
1151
1159
  d = @min(997, d);
1152
1160
  d += 2;
1153
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
+
1154
1172
  battle.last_damage = @intCast(d);
1155
1173
 
1156
1174
  return true;
1157
1175
  }
1158
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
+
1159
1197
  fn adjustDamage(battle: anytype, player: Player) u16 {
1160
1198
  const side = battle.side(player);
1161
1199
  const foe = battle.foe(player);
@@ -1210,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1210
1248
  .Psywave => power: {
1211
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1212
1250
  // GLITCH: Psywave infinite glitch loop
1213
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1214
1252
  break :power try Rolls.psywave(battle, player, max, options);
1215
1253
  },
1216
1254
  else => unreachable,
1217
1255
  };
1218
1256
 
1219
- 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;
1220
1258
 
1221
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1222
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1253,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1253
1291
 
1254
1292
  if (!used or !selected) {
1255
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1256
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return Result.Error;
1257
1295
  try options.log.fail(.{ battle.active(player), .None });
1258
1296
  return null;
1259
1297
  }
1260
1298
 
1261
- 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;
1262
1304
 
1263
1305
  // Pokémon Showdown calls moveHit before Counter
1264
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1290,7 +1332,7 @@ fn applyDamage(
1290
1332
  subbed.active.volatiles.Substitute = false;
1291
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1292
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1293
- options.calc.capped(sub_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1294
1336
  return true;
1295
1337
  } else {
1296
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1324,7 +1366,7 @@ fn mirrorMove(
1324
1366
  try options.log.fail(.{ battle.active(player), .None });
1325
1367
  return null;
1326
1368
  } else if (!showdown or foe.last_used_move != .Struggle) {
1327
- incrementPP(side, mslot);
1369
+ incrementPP(side, player, mslot, options);
1328
1370
  }
1329
1371
 
1330
1372
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1345,7 +1387,7 @@ fn metronome(
1345
1387
  var side = battle.side(player);
1346
1388
 
1347
1389
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1348
- incrementPP(side, mslot);
1390
+ incrementPP(side, player, mslot, options);
1349
1391
 
1350
1392
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1351
1393
  return null;
@@ -1404,7 +1446,16 @@ fn moveHit(
1404
1446
  break :miss true;
1405
1447
  }
1406
1448
  if (!showdown) {
1407
- if (move.effect == .Swift) return true;
1449
+ if (move.effect == .Swift) {
1450
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1451
+ // Showdown, hence why the block above doesn't include this code), we need the
1452
+ // damage roll to be committed. However, the commit code will not save this roll if
1453
+ // the glitch occurs and the move was not marked as having hit. To work around this
1454
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1455
+ // the hit as zero to ensure it doesn't actually get saved.
1456
+ try options.chance.hit(player, true, 0);
1457
+ return true;
1458
+ }
1408
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1409
1460
  }
1410
1461
 
@@ -1446,7 +1497,10 @@ fn moveHit(
1446
1497
 
1447
1498
  if (!miss) return true;
1448
1499
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1500
+
1449
1501
  side.active.volatiles.Binding = false;
1502
+ options.chance.observe(.binding, player, .None);
1503
+
1450
1504
  return false;
1451
1505
  }
1452
1506
 
@@ -1497,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1497
1551
  assert(!foe_volatiles.MultiHit);
1498
1552
  if (foe_volatiles.Bide) {
1499
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1500
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1554
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1501
1555
  if (foe_volatiles.state != 0) return Result.Error;
1502
1556
  }
1503
1557
 
@@ -1588,12 +1642,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1588
1642
 
1589
1643
  battle.turn += 1;
1590
1644
 
1591
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1592
1646
  try options.log.tie(.{});
1593
1647
  return Result.Tie;
1594
1648
  }
1595
1649
 
1596
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1597
1651
  try options.log.tie(.{});
1598
1652
  return Result.Tie;
1599
1653
  } else if (battle.turn >= 65535) {
@@ -1773,7 +1827,7 @@ pub const Effects = struct {
1773
1827
 
1774
1828
  side.active.volatiles.state = 0;
1775
1829
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1776
- options.chance.observe(.bide, player, .started);
1830
+ options.chance.observe(.attacking, player, .started);
1777
1831
 
1778
1832
  try options.log.start(.{ battle.active(player), .Bide });
1779
1833
  }
@@ -1842,6 +1896,7 @@ pub const Effects = struct {
1842
1896
 
1843
1897
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1844
1898
  options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1845
1900
 
1846
1901
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1847
1902
  }
@@ -1887,12 +1942,11 @@ pub const Effects = struct {
1887
1942
 
1888
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1889
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1890
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1891
1946
  try options.log.fail(.{ foe_ident, .None });
1892
1947
  return null;
1893
1948
  } else if (err) {
1894
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1895
- assert(!showdown);
1896
1950
  return Result.Error;
1897
1951
  }
1898
1952
 
@@ -1970,7 +2024,7 @@ pub const Effects = struct {
1970
2024
  if (foe.active.types.includes(move.type)) return;
1971
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1972
2026
  // Freeze Clause Mod
1973
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2027
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1974
2028
 
1975
2029
  foe_stored.status = Status.init(.FRZ);
1976
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2311,7 +2365,7 @@ pub const Effects = struct {
2311
2365
  }
2312
2366
 
2313
2367
  // Sleep Clause Mod
2314
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2315
2369
  for (foe.pokemon) |p| {
2316
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2317
2371
  }
@@ -2378,7 +2432,7 @@ pub const Effects = struct {
2378
2432
  volatiles.Thrashing = true;
2379
2433
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2380
2434
 
2381
- options.chance.observe(.thrashing, player, .started);
2435
+ options.chance.observe(.attacking, player, .started);
2382
2436
  }
2383
2437
 
2384
2438
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2417,7 +2471,7 @@ pub const Effects = struct {
2417
2471
  if (side.active.volatiles.Binding) return;
2418
2472
  side.active.volatiles.Binding = true;
2419
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2420
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2421
2475
  if (!showdown) {
2422
2476
  foe.active.volatiles.Recharging = false;
2423
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -2717,10 +2771,6 @@ fn decrement(
2717
2771
  } else n - 1;
2718
2772
  }
2719
2773
 
2720
- fn observation(n: anytype) optional.Optional(chance.Confusion) {
2721
- return if (n == 0) .ended else .continuing;
2722
- }
2723
-
2724
2774
  pub const Rolls = struct {
2725
2775
  fn speedTie(battle: anytype, options: anytype) !bool {
2726
2776
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
@@ -2918,9 +2968,10 @@ pub const Rolls = struct {
2918
2968
  }
2919
2969
 
2920
2970
  fn sleepDuration(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), "sleepDuration"))
2971
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2972
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2973
+ else
2974
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2924
2975
  battle.rng.sleepDuration(player)
2925
2976
  else if (showdown)
2926
2977
  battle.rng.range(u3, 1, 8)
@@ -2937,9 +2988,10 @@ pub const Rolls = struct {
2937
2988
  }
2938
2989
 
2939
2990
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2940
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2941
- val
2942
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2991
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2992
+ if (val >= 1 and val <= 8) val else null
2993
+ else
2994
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2943
2995
  battle.rng.disableDuration(player)
2944
2996
  else if (showdown)
2945
2997
  battle.rng.range(u4, 1, 9)
@@ -2952,9 +3004,10 @@ pub const Rolls = struct {
2952
3004
  }
2953
3005
 
2954
3006
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2955
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2956
- @intCast(val)
2957
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3007
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3008
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3009
+ else
3010
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2958
3011
  battle.rng.confusionDuration(player)
2959
3012
  else if (showdown)
2960
3013
  battle.rng.range(u3, 2, 6)
@@ -2967,9 +3020,10 @@ pub const Rolls = struct {
2967
3020
  }
2968
3021
 
2969
3022
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2970
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2971
- @intCast(val)
2972
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3023
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3024
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3025
+ else
3026
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2973
3027
  battle.rng.attackingDuration(player)
2974
3028
  else if (showdown)
2975
3029
  battle.rng.range(u3, 2, 4)
@@ -2990,9 +3044,10 @@ pub const Rolls = struct {
2990
3044
  options: anytype,
2991
3045
  ) !u3 {
2992
3046
  const roll = if (effect == .Binding) .duration else .multi_hit;
2993
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
2994
- @intCast(val)
2995
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3047
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3048
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3049
+ else
3050
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2996
3051
  battle.rng.distribution(player)
2997
3052
  else if (showdown)
2998
3053
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3018,15 +3073,13 @@ pub const Rolls = struct {
3018
3073
  options: anytype,
3019
3074
  ) !u4 {
3020
3075
  // TODO: consider throwing error instead of rerolling?
3021
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3022
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3023
3078
  val
3024
3079
  else
3025
3080
  null
3026
3081
  else
3027
- null;
3028
-
3029
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3030
3083
  battle.rng.moveSlot(player, moves, check_pp)
3031
3084
  else slot: {
3032
3085
  if (showdown) {
@@ -3184,7 +3237,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3184
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3185
3238
  if (n == before) {
3186
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3187
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3188
3241
  while (slot <= 4) : (slot += 1) {
3189
3242
  if (active.moves[slot - 1].pp != 0) return n;
3190
3243
  }