@pkmn/engine 0.1.0-dev.3de821b9 → 0.1.0-dev.3ed876db

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 (73) hide show
  1. package/README.md +4 -2
  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 +21 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +22 -0
  12. package/build/pkg/addon.js +29 -51
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data.d.ts +23 -1
  15. package/build/pkg/data.js.map +1 -1
  16. package/build/pkg/gen1.js +1 -1
  17. package/build/pkg/gen1.js.map +1 -1
  18. package/build/pkg/index.d.ts +23 -9
  19. package/build/pkg/index.js +17 -11
  20. package/build/pkg/index.js.map +1 -1
  21. package/build/pkg/protocol.d.ts +20 -1
  22. package/build/pkg/protocol.js +105 -103
  23. package/build/pkg/protocol.js.map +1 -1
  24. package/build/tools/debug.js +35 -68
  25. package/build/tools/debug.js.map +1 -1
  26. package/build/tools/display/debug.jsx +141 -0
  27. package/build/tools/display/debug.jsx.map +1 -0
  28. package/build/tools/display/index.js +141 -0
  29. package/build/tools/display/index.js.map +1 -0
  30. package/build/tools/display/ui.jsx +349 -0
  31. package/build/tools/display/ui.jsx.map +1 -0
  32. package/build/tools/display/util.js +18 -0
  33. package/build/tools/display/util.js.map +1 -0
  34. package/build.zig +249 -77
  35. package/package.json +51 -31
  36. package/src/bin/install-pkmn-engine +7 -5
  37. package/src/bin/pkmn-debug +1 -1
  38. package/src/lib/common/array.zig +77 -0
  39. package/src/lib/common/data.zig +8 -8
  40. package/src/lib/common/debug.zig +39 -8
  41. package/src/lib/common/js.zig +4 -2
  42. package/src/lib/common/optional.zig +13 -9
  43. package/src/lib/common/options.zig +4 -0
  44. package/src/lib/common/protocol.zig +44 -43
  45. package/src/lib/common/rational.zig +53 -51
  46. package/src/lib/common/rng.zig +5 -7
  47. package/src/lib/common/util.zig +50 -1
  48. package/src/lib/gen1/calc.zig +247 -108
  49. package/src/lib/gen1/chance.zig +130 -121
  50. package/src/lib/gen1/data/moves.zig +2 -4
  51. package/src/lib/gen1/data/species.zig +1 -3
  52. package/src/lib/gen1/data/types.zig +1 -2
  53. package/src/lib/gen1/data.zig +35 -38
  54. package/src/lib/gen1/helpers.zig +15 -27
  55. package/src/lib/gen1/mechanics.zig +112 -94
  56. package/src/lib/gen2/calc.zig +37 -27
  57. package/src/lib/gen2/chance.zig +86 -66
  58. package/src/lib/gen2/data/items.zig +1 -3
  59. package/src/lib/gen2/data/moves.zig +2 -4
  60. package/src/lib/gen2/data/species.zig +1 -3
  61. package/src/lib/gen2/data/types.zig +1 -2
  62. package/src/lib/gen2/data.zig +26 -29
  63. package/src/lib/gen2/generated.zig +18 -23
  64. package/src/lib/gen2/helpers.zig +9 -18
  65. package/src/lib/gen2/mechanics.zig +62 -73
  66. package/src/lib/node.zig +2 -4
  67. package/src/lib/pkmn.zig +11 -5
  68. package/src/lib/wasm.zig +1 -2
  69. package/src/tools/display/dom.ts +96 -0
  70. package/src/tools/display/ui.css +354 -0
  71. package/build/test/display.js +0 -405
  72. package/build/test/display.js.map +0 -1
  73. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,39 +1,28 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
7
- const optional = @import("../common/optional.zig");
5
+ const data = @import("data.zig");
6
+ const pkmn = @import("../pkmn.zig");
8
7
  const protocol = @import("../common/protocol.zig");
9
8
  const rng = @import("../common/rng.zig");
10
-
11
- const data = @import("data.zig");
12
- const chance = @import("chance.zig");
9
+ const std = @import("std");
13
10
 
14
11
  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
12
  const Boost = protocol.Boost;
13
+ const Choice = common.Choice;
26
14
  const Damage = protocol.Damage;
15
+ const Effectiveness = data.Effectiveness;
16
+ const expectEqual = std.testing.expectEqual;
27
17
  const Fail = protocol.Fail;
28
- const Heal = protocol.Heal;
29
-
30
18
  const Gen12 = rng.Gen12;
31
-
32
- const ActivePokemon = data.ActivePokemon;
33
- const Effectiveness = data.Effectiveness;
19
+ const Heal = protocol.Heal;
20
+ const ID = common.ID;
34
21
  const Move = data.Move;
35
22
  const MoveSlot = data.MoveSlot;
36
- const Pokemon = data.Pokemon;
23
+ const Player = common.Player;
24
+ const Result = common.Result;
25
+ const showdown = pkmn.options.showdown;
37
26
  const Side = data.Side;
38
27
  const Species = data.Species;
39
28
  const Stats = data.Stats;
@@ -176,7 +165,10 @@ fn selectMove(
176
165
  if (foe_choice.type == .Switch) {
177
166
  const last = side.active.move(battle.lastMove(player).index);
178
167
  if (last.id == .Metronome) side.last_selected_move = last.id;
179
- if (last.id == .MirrorMove) return Result.Error;
168
+ if (last.id == .MirrorMove) {
169
+ if (!pkmn.options.mod) return Result.Error;
170
+ side.last_selected_move = last.id;
171
+ }
180
172
  }
181
173
  }
182
174
  return null;
@@ -427,7 +419,7 @@ fn executeMove(
427
419
  mslot = @intCast(battle.lastMove(player).index);
428
420
  const stored = side.stored();
429
421
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
430
- auto = isForced(&side.active) or
422
+ auto = pkmn.options.mod or isForced(&side.active) or
431
423
  side.active.volatiles.Binding or side.active.volatiles.Bide or
432
424
  side.last_selected_move == .HyperBeam or
433
425
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -460,6 +452,25 @@ fn executeMove(
460
452
  .err => return @as(?Result, Result.Error),
461
453
  }
462
454
 
455
+ // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
456
+ // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
457
+ // from the Hyper Beam slot if present or skips PP deduction entirely...
458
+ if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
459
+ side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
460
+ {
461
+ assert(mslot == 1);
462
+ const has_beam = has_beam: {
463
+ for (side.active.moves, 0..) |m, i| {
464
+ if (m.id == .HyperBeam) {
465
+ mslot = @intCast(i + 1);
466
+ break :has_beam true;
467
+ }
468
+ }
469
+ break :has_beam false;
470
+ };
471
+ if (!has_beam) skip_pp = true;
472
+ }
473
+
463
474
  const can = skip_can or
464
475
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
465
476
  if (!can) return null;
@@ -557,18 +568,17 @@ fn beforeMove(
557
568
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
558
569
  // before the side had a selected a move - we simply need to assume this leads to a desync
559
570
  if (side.last_selected_move == .None) {
560
- assert(!showdown);
571
+ assert(!pkmn.options.mod);
561
572
  return .err;
562
573
  }
563
574
 
564
575
  if (volatiles.Confusion) {
565
576
  assert(volatiles.confusion > 0);
566
577
  if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
- .started => volatiles.confusion -= 1,
578
+ .started, .ended => volatiles.confusion = 0,
568
579
  .continuing => if (volatiles.confusion > 1) {
569
580
  volatiles.confusion -= 1;
570
581
  },
571
- .ended => volatiles.confusion = 0,
572
582
  .overwritten => {},
573
583
  else => unreachable,
574
584
  } else {
@@ -594,8 +604,7 @@ fn beforeMove(
594
604
  volatiles.Binding = false;
595
605
  volatiles.Invulnerable = false;
596
606
 
597
- options.chance.observe(.bide, player, .None);
598
- options.chance.observe(.thrashing, player, .None);
607
+ options.chance.observe(.attacking, player, .None);
599
608
  options.chance.observe(.binding, player, .None);
600
609
  {
601
610
  // This feels (and is) disgusting but the cartridge literally just overwrites
@@ -636,8 +645,7 @@ fn beforeMove(
636
645
  volatiles.Charging = false;
637
646
  volatiles.Binding = false;
638
647
 
639
- options.chance.observe(.bide, player, .None);
640
- options.chance.observe(.thrashing, player, .None);
648
+ options.chance.observe(.attacking, player, .None);
641
649
  options.chance.observe(.binding, player, .None);
642
650
 
643
651
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
@@ -657,8 +665,8 @@ fn beforeMove(
657
665
  volatiles.state +%= battle.last_damage;
658
666
  }
659
667
 
660
- volatiles.attacks = decrement(.bide, player, options, volatiles.attacks);
661
- try options.chance.bide(player, if (volatiles.attacks == 0) .ended else .continuing);
668
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
669
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
662
670
 
663
671
  if (volatiles.attacks != 0) {
664
672
  try log.activate(.{ ident, .Bide });
@@ -694,8 +702,8 @@ fn beforeMove(
694
702
  if (volatiles.Thrashing) {
695
703
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
696
704
 
697
- volatiles.attacks = decrement(.thrashing, player, options, volatiles.attacks);
698
- try options.chance.thrashing(player, if (volatiles.attacks == 0) .ended else .continuing);
705
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
706
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
699
707
 
700
708
  if (volatiles.attacks == 0) {
701
709
  const overwritten = volatiles.Confusion;
@@ -709,7 +717,11 @@ fn beforeMove(
709
717
  // can be expected to recognize this pattern and pass the information only to the
710
718
  // correct player)
711
719
  try log.start(.{ battle.active(player), .ConfusionSilent });
712
- options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
720
+ options.chance.observe(.confusion, player, if (overwritten)
721
+ if (pkmn.options.overwrite) .overwritten else .continuing
722
+ else
723
+ .started);
724
+ if (!overwritten) options.calc.confusion(player);
713
725
  }
714
726
 
715
727
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -775,9 +787,14 @@ fn canMove(
775
787
 
776
788
  if (move.effect == .Thrashing) {
777
789
  Effects.thrashing(battle, player, options);
778
- } else if (!showdown and move.effect == .Binding) {
779
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
780
- try Effects.binding(battle, player, false, options);
790
+ } else if (move.effect == .Binding) {
791
+ // Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
792
+ // Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
793
+ if (showdown) {
794
+ battle.foe(player).active.volatiles.Recharging = false;
795
+ } else {
796
+ try Effects.binding(battle, player, false, options);
797
+ }
781
798
  }
782
799
 
783
800
  return true;
@@ -823,7 +840,7 @@ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
823
840
  if (showdown and volatiles.Transform) return;
824
841
 
825
842
  const n = options.calc.overridden(player, .pp) orelse 1;
826
- if (volatiles.Transform and !options.chance.incrementPP(player, n)) return;
843
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
827
844
 
828
845
  assert(mslot > 0 and mslot <= 4);
829
846
  side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
@@ -880,7 +897,7 @@ fn doMove(
880
897
 
881
898
  var late = showdown and move.effect != .Explode;
882
899
  const skip = status or immune;
883
- if ((!showdown or (!skip or counter)) and !miss) blk: {
900
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
884
901
  if (showdown and move.effect.isMulti()) {
885
902
  try Effects.multiHit(battle, player, move, options);
886
903
  hits = side.active.volatiles.attacks;
@@ -902,7 +919,7 @@ fn doMove(
902
919
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
903
920
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
904
921
  battle.last_damage = if (ohko) 65535 else 0;
905
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
922
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
906
923
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
907
924
  if (!showdown) try options.chance.commit(player, .err);
908
925
  return @as(?Result, Result.Error);
@@ -1037,22 +1054,20 @@ fn doMove(
1037
1054
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1055
  }
1039
1056
  try log.hitcount(.{ battle.active(player.foe()), hit });
1040
- } else if (showdown) {
1057
+ } else if (showdown and move.effect == .Binding) {
1041
1058
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1042
- if (move.effect == .Binding) {
1043
- try Effects.binding(battle, player, rewrap, options);
1044
- if (immune) {
1045
- battle.last_damage = 0;
1046
- assert(foe.stored().hp > 0);
1047
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1048
- if (sub) {
1049
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1050
- } else {
1051
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1052
- try buildRage(battle, player.foe(), options);
1053
- }
1054
- return null;
1059
+ try Effects.binding(battle, player, rewrap, options);
1060
+ if (immune) {
1061
+ battle.last_damage = 0;
1062
+ assert(foe.stored().hp > 0);
1063
+ // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1064
+ if (sub) {
1065
+ try log.activate(.{ battle.active(player.foe()), .Substitute });
1066
+ } else {
1067
+ try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1068
+ try buildRage(battle, player.foe(), options);
1055
1069
  }
1070
+ return null;
1056
1071
  }
1057
1072
  }
1058
1073
 
@@ -1133,7 +1148,7 @@ fn calcDamage(
1133
1148
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1134
1149
  atk = @max((atk / 4) & 255, 1);
1135
1150
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1136
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1151
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1137
1152
  }
1138
1153
 
1139
1154
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1154,7 +1169,7 @@ fn calcDamage(
1154
1169
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1155
1170
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1156
1171
  // that results in counter intuitive probabilities in the common case
1157
- if (pkmn.options.chance and !showdown) {
1172
+ if (pkmn.options.chance and !pkmn.options.mod) {
1158
1173
  atk = attack(side, !crit, special);
1159
1174
  def = defense(target, !crit, special, false);
1160
1175
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1239,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1239
1254
  .Psywave => power: {
1240
1255
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1241
1256
  // GLITCH: Psywave infinite glitch loop
1242
- if (!showdown and max <= 1) return Result.Error;
1257
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1243
1258
  break :power try Rolls.psywave(battle, player, max, options);
1244
1259
  },
1245
1260
  else => unreachable,
1246
1261
  };
1247
1262
 
1248
- if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1263
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1249
1264
 
1250
1265
  const sub = showdown and foe.active.volatiles.Substitute;
1251
1266
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1282,7 +1297,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1282
1297
 
1283
1298
  if (!used or !selected) {
1284
1299
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1285
- if (!showdown) return Result.Error;
1300
+ if (!pkmn.options.mod) return Result.Error;
1286
1301
  try options.log.fail(.{ battle.active(player), .None });
1287
1302
  return null;
1288
1303
  }
@@ -1319,7 +1334,7 @@ fn applyDamage(
1319
1334
  subbed.active.volatiles.Substitute = false;
1320
1335
  // battle.last_damage is not updated with the amount of HP the Substitute had
1321
1336
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1322
- options.calc.capped(sub_player);
1337
+ if (reason != .Confusion) options.calc.capped(sub_player);
1323
1338
  return true;
1324
1339
  } else {
1325
1340
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1529,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1529
1544
  assert(!foe_volatiles.MultiHit);
1530
1545
  if (foe_volatiles.Bide) {
1531
1546
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1532
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1547
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1533
1548
  if (foe_volatiles.state != 0) return Result.Error;
1534
1549
  }
1535
1550
 
@@ -1620,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1620
1635
 
1621
1636
  battle.turn += 1;
1622
1637
 
1623
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1638
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1624
1639
  try options.log.tie(.{});
1625
1640
  return Result.Tie;
1626
1641
  }
1627
1642
 
1628
- if (showdown and battle.turn >= 1000) {
1643
+ if (pkmn.options.mod and battle.turn >= 1000) {
1629
1644
  try options.log.tie(.{});
1630
1645
  return Result.Tie;
1631
1646
  } else if (battle.turn >= 65535) {
@@ -1805,7 +1820,7 @@ pub const Effects = struct {
1805
1820
 
1806
1821
  side.active.volatiles.state = 0;
1807
1822
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1808
- options.chance.observe(.bide, player, .started);
1823
+ options.chance.observe(.attacking, player, .started);
1809
1824
 
1810
1825
  try options.log.start(.{ battle.active(player), .Bide });
1811
1826
  }
@@ -1874,6 +1889,7 @@ pub const Effects = struct {
1874
1889
 
1875
1890
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1876
1891
  options.chance.observe(.confusion, player.foe(), .started);
1892
+ options.calc.confusion(player.foe());
1877
1893
 
1878
1894
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1895
  }
@@ -1919,12 +1935,11 @@ pub const Effects = struct {
1919
1935
 
1920
1936
  // Technically this is still considered simply a "miss" on the cartridge,
1921
1937
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1922
- if (n == 0) {
1938
+ if (n == 0 or (pkmn.options.mod and err)) {
1923
1939
  try options.log.fail(.{ foe_ident, .None });
1924
1940
  return null;
1925
1941
  } else if (err) {
1926
1942
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1927
- assert(!showdown);
1928
1943
  return Result.Error;
1929
1944
  }
1930
1945
 
@@ -2002,7 +2017,7 @@ pub const Effects = struct {
2002
2017
  if (foe.active.types.includes(move.type)) return;
2003
2018
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2004
2019
  // Freeze Clause Mod
2005
- if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2020
+ if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
2006
2021
 
2007
2022
  foe_stored.status = Status.init(.FRZ);
2008
2023
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2343,7 +2358,7 @@ pub const Effects = struct {
2343
2358
  }
2344
2359
 
2345
2360
  // Sleep Clause Mod
2346
- if (showdown) {
2361
+ if (pkmn.options.mod) {
2347
2362
  for (foe.pokemon) |p| {
2348
2363
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2349
2364
  }
@@ -2410,7 +2425,7 @@ pub const Effects = struct {
2410
2425
  volatiles.Thrashing = true;
2411
2426
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2412
2427
 
2413
- options.chance.observe(.thrashing, player, .started);
2428
+ options.chance.observe(.attacking, player, .started);
2414
2429
  }
2415
2430
 
2416
2431
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2449,7 +2464,7 @@ pub const Effects = struct {
2449
2464
  if (side.active.volatiles.Binding) return;
2450
2465
  side.active.volatiles.Binding = true;
2451
2466
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2452
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2467
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2453
2468
  if (!showdown) {
2454
2469
  foe.active.volatiles.Recharging = false;
2455
2470
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -2946,9 +2961,10 @@ pub const Rolls = struct {
2946
2961
  }
2947
2962
 
2948
2963
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2949
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2950
- @intCast(val)
2951
- else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2964
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
2965
+ if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
2966
+ else
2967
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2952
2968
  battle.rng.sleepDuration(player)
2953
2969
  else if (showdown)
2954
2970
  battle.rng.range(u3, 1, 8)
@@ -2965,9 +2981,10 @@ pub const Rolls = struct {
2965
2981
  }
2966
2982
 
2967
2983
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2968
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2969
- val
2970
- else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2984
+ const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
2985
+ if (val >= 1 and val <= 8) val else null
2986
+ else
2987
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2971
2988
  battle.rng.disableDuration(player)
2972
2989
  else if (showdown)
2973
2990
  battle.rng.range(u4, 1, 9)
@@ -2980,9 +2997,10 @@ pub const Rolls = struct {
2980
2997
  }
2981
2998
 
2982
2999
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2983
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2984
- @intCast(val)
2985
- else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
3000
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3001
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3002
+ else
3003
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2986
3004
  battle.rng.confusionDuration(player)
2987
3005
  else if (showdown)
2988
3006
  battle.rng.range(u3, 2, 6)
@@ -2995,9 +3013,10 @@ pub const Rolls = struct {
2995
3013
  }
2996
3014
 
2997
3015
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2998
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2999
- @intCast(val)
3000
- else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3016
+ const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
3017
+ if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
3018
+ else
3019
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
3001
3020
  battle.rng.attackingDuration(player)
3002
3021
  else if (showdown)
3003
3022
  battle.rng.range(u3, 2, 4)
@@ -3018,9 +3037,10 @@ pub const Rolls = struct {
3018
3037
  options: anytype,
3019
3038
  ) !u3 {
3020
3039
  const roll = if (effect == .Binding) .duration else .multi_hit;
3021
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
3022
- @intCast(val)
3023
- else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3040
+ const n: u3 = (if (options.calc.overridden(player, roll)) |val|
3041
+ if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
3042
+ else
3043
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3024
3044
  battle.rng.distribution(player)
3025
3045
  else if (showdown)
3026
3046
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3046,15 +3066,13 @@ pub const Rolls = struct {
3046
3066
  options: anytype,
3047
3067
  ) !u4 {
3048
3068
  // TODO: consider throwing error instead of rerolling?
3049
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3069
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3050
3070
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3051
3071
  val
3052
3072
  else
3053
3073
  null
3054
3074
  else
3055
- null;
3056
-
3057
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3075
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3058
3076
  battle.rng.moveSlot(player, moves, check_pp)
3059
3077
  else slot: {
3060
3078
  if (showdown) {
@@ -3212,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3212
3230
  // Struggle (Pokémon Showdown would use 'move 1' here)
3213
3231
  if (n == before) {
3214
3232
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3215
- if (!showdown) {
3233
+ if (!pkmn.options.mod) {
3216
3234
  while (slot <= 4) : (slot += 1) {
3217
3235
  if (active.moves[slot - 1].pp != 0) return n;
3218
3236
  }
@@ -1,22 +1,22 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
4
-
1
+ const chance = @import("chance.zig");
5
2
  const common = @import("../common/data.zig");
3
+ const Optional = @import("../common/optional.zig").Optional;
4
+ const pkmn = @import("../pkmn.zig");
5
+ const std = @import("std");
6
6
  const util = @import("../common/util.zig");
7
7
 
8
- const chance = @import("chance.zig");
9
-
8
+ const Action = chance.Action;
9
+ const Actions = chance.Actions;
10
10
  const assert = std.debug.assert;
11
-
11
+ const Criticals = chance.Criticals;
12
+ const Damages = chance.Damages;
12
13
  const enabled = pkmn.options.calc;
13
-
14
+ const isPointerTo = util.isPointerTo;
14
15
  const Player = common.Player;
15
-
16
16
  const PointerType = util.PointerType;
17
17
 
18
- const Actions = chance.Actions;
19
- const Action = chance.Action;
18
+ const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
19
+ const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
20
20
 
21
21
  /// Information relevant to damage calculation that occured during a Generation II battle `update`.
22
22
  pub const Summaries = extern struct {
@@ -31,7 +31,7 @@ pub const Summaries = extern struct {
31
31
 
32
32
  /// Returns the `Summary` for the given `player`.
33
33
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
34
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
34
+ assert(isPointerTo(self, Summaries));
35
35
  return if (player == .P1) &self.p1 else &self.p2;
36
36
  }
37
37
  };
@@ -65,33 +65,34 @@ pub const Summary = extern struct {
65
65
  }
66
66
  };
67
67
 
68
- /// TODO
69
- pub const Overrides = extern struct {
70
- /// TODO
71
- actions: Actions = .{},
72
- };
73
-
74
68
  /// Allows for forcing the value of specific RNG events during a Generation II battle `update` via
75
69
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
76
70
  pub const Calc = struct {
77
71
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
78
- overrides: Overrides = .{},
72
+ overrides: Actions = .{},
79
73
  /// Information relevant to damage calculation.
80
74
  summaries: Summaries = .{},
81
75
 
82
76
  pub fn overridden(
83
77
  self: Calc,
84
78
  player: Player,
79
+ hit: u3,
85
80
  comptime field: Action.Field,
86
- ) ?std.meta.FieldType(Action, field) {
81
+ ) ReturnType(field) {
87
82
  if (!enabled) return null;
88
83
 
89
- const overrides =
90
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
91
- const val = @field(overrides, @tagName(field));
84
+ const val = switch (field) {
85
+ .damages => Damages.get(self.overrides.get(player).damages, hit),
86
+ .critical_hits => Criticals.get(self.overrides.get(player).critical_hits, hit),
87
+ else => val: {
88
+ assert(hit == 0);
89
+ break :val @field(self.overrides.get(player), @tagName(field));
90
+ },
91
+ };
92
+
92
93
  return if (switch (@typeInfo(@TypeOf(val))) {
93
- .Enum => val != .None,
94
- .Int => val != 0,
94
+ Enum => val != .None,
95
+ Int => val != 0,
95
96
  else => unreachable,
96
97
  }) val else null;
97
98
  }
@@ -123,9 +124,10 @@ const Null = struct {
123
124
  pub fn overridden(
124
125
  self: Null,
125
126
  player: Player,
127
+ hit: u3,
126
128
  comptime field: Action.Field,
127
- ) ?std.meta.FieldType(Action, field) {
128
- _ = .{ self, player };
129
+ ) ReturnType(field) {
130
+ _ = .{ self, player, hit };
129
131
  return null;
130
132
  }
131
133
 
@@ -141,3 +143,11 @@ const Null = struct {
141
143
  _ = .{ self, player };
142
144
  }
143
145
  };
146
+
147
+ fn ReturnType(comptime field: Action.Field) type {
148
+ return ?(switch (field) {
149
+ .damages => u8,
150
+ .critical_hits => Optional(bool),
151
+ else => std.meta.FieldType(Action, field),
152
+ });
153
+ }