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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +47 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +100 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +53 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -3
  12. package/build/pkg/addon.js +34 -65
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/data.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +15 -18
  19. package/build/pkg/gen1.js +11 -12
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +40 -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 +142 -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 +33 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +330 -85
  38. package/package.json +53 -134
  39. package/src/bin/install-pkmn-engine +10 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/c.zig +255 -0
  42. package/src/lib/bindings/node.zig +121 -0
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +8 -8
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +13 -9
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +44 -43
  51. package/src/lib/common/rational.zig +53 -51
  52. package/src/lib/common/rng.zig +5 -7
  53. package/src/lib/common/util.zig +50 -1
  54. package/src/lib/gen1/calc.zig +250 -111
  55. package/src/lib/gen1/chance.zig +130 -121
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +35 -38
  60. package/src/lib/gen1/helpers.zig +15 -27
  61. package/src/lib/gen1/mechanics.zig +118 -95
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +86 -66
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +26 -29
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +9 -18
  71. package/src/lib/gen2/mechanics.zig +81 -81
  72. package/src/lib/node.zig +3 -116
  73. package/src/lib/pkmn.zig +14 -7
  74. package/src/lib/wasm.zig +13 -9
  75. package/src/tools/display/dom.ts +96 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -405
  78. package/build/test/display.js.map +0 -1
  79. 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;
@@ -427,7 +420,7 @@ fn executeMove(
427
420
  mslot = @intCast(battle.lastMove(player).index);
428
421
  const stored = side.stored();
429
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
430
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
431
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
432
425
  side.last_selected_move == .HyperBeam or
433
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -460,6 +453,25 @@ fn executeMove(
460
453
  .err => return @as(?Result, Result.Error),
461
454
  }
462
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
+
463
475
  const can = skip_can or
464
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
465
477
  if (!can) return null;
@@ -557,18 +569,17 @@ fn beforeMove(
557
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
558
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
559
571
  if (side.last_selected_move == .None) {
560
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
561
573
  return .err;
562
574
  }
563
575
 
564
576
  if (volatiles.Confusion) {
565
577
  assert(volatiles.confusion > 0);
566
578
  if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
- .started => volatiles.confusion -= 1,
579
+ .started, .ended => volatiles.confusion = 0,
568
580
  .continuing => if (volatiles.confusion > 1) {
569
581
  volatiles.confusion -= 1;
570
582
  },
571
- .ended => volatiles.confusion = 0,
572
583
  .overwritten => {},
573
584
  else => unreachable,
574
585
  } else {
@@ -594,8 +605,7 @@ fn beforeMove(
594
605
  volatiles.Binding = false;
595
606
  volatiles.Invulnerable = false;
596
607
 
597
- options.chance.observe(.bide, player, .None);
598
- options.chance.observe(.thrashing, player, .None);
608
+ options.chance.observe(.attacking, player, .None);
599
609
  options.chance.observe(.binding, player, .None);
600
610
  {
601
611
  // This feels (and is) disgusting but the cartridge literally just overwrites
@@ -636,8 +646,7 @@ fn beforeMove(
636
646
  volatiles.Charging = false;
637
647
  volatiles.Binding = false;
638
648
 
639
- options.chance.observe(.bide, player, .None);
640
- options.chance.observe(.thrashing, player, .None);
649
+ options.chance.observe(.attacking, player, .None);
641
650
  options.chance.observe(.binding, player, .None);
642
651
 
643
652
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
@@ -657,8 +666,8 @@ fn beforeMove(
657
666
  volatiles.state +%= battle.last_damage;
658
667
  }
659
668
 
660
- volatiles.attacks = decrement(.bide, player, options, volatiles.attacks);
661
- try options.chance.bide(player, if (volatiles.attacks == 0) .ended else .continuing);
669
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
670
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
662
671
 
663
672
  if (volatiles.attacks != 0) {
664
673
  try log.activate(.{ ident, .Bide });
@@ -694,8 +703,8 @@ fn beforeMove(
694
703
  if (volatiles.Thrashing) {
695
704
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
696
705
 
697
- volatiles.attacks = decrement(.thrashing, player, options, volatiles.attacks);
698
- try options.chance.thrashing(player, if (volatiles.attacks == 0) .ended else .continuing);
706
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
707
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
699
708
 
700
709
  if (volatiles.attacks == 0) {
701
710
  const overwritten = volatiles.Confusion;
@@ -709,7 +718,11 @@ fn beforeMove(
709
718
  // can be expected to recognize this pattern and pass the information only to the
710
719
  // correct player)
711
720
  try log.start(.{ battle.active(player), .ConfusionSilent });
712
- options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
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);
713
726
  }
714
727
 
715
728
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -775,9 +788,14 @@ fn canMove(
775
788
 
776
789
  if (move.effect == .Thrashing) {
777
790
  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);
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
+ }
781
799
  }
782
800
 
783
801
  return true;
@@ -823,7 +841,7 @@ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
823
841
  if (showdown and volatiles.Transform) return;
824
842
 
825
843
  const n = options.calc.overridden(player, .pp) orelse 1;
826
- if (volatiles.Transform and !options.chance.incrementPP(player, n)) return;
844
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
827
845
 
828
846
  assert(mslot > 0 and mslot <= 4);
829
847
  side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
@@ -880,7 +898,7 @@ fn doMove(
880
898
 
881
899
  var late = showdown and move.effect != .Explode;
882
900
  const skip = status or immune;
883
- if ((!showdown or (!skip or counter)) and !miss) blk: {
901
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
884
902
  if (showdown and move.effect.isMulti()) {
885
903
  try Effects.multiHit(battle, player, move, options);
886
904
  hits = side.active.volatiles.attacks;
@@ -902,7 +920,7 @@ fn doMove(
902
920
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
903
921
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
904
922
  battle.last_damage = if (ohko) 65535 else 0;
905
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
923
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
906
924
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
907
925
  if (!showdown) try options.chance.commit(player, .err);
908
926
  return @as(?Result, Result.Error);
@@ -1037,22 +1055,20 @@ fn doMove(
1037
1055
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1038
1056
  }
1039
1057
  try log.hitcount(.{ battle.active(player.foe()), hit });
1040
- } else if (showdown) {
1058
+ } else if (showdown and move.effect == .Binding) {
1041
1059
  // 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;
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);
1055
1070
  }
1071
+ return null;
1056
1072
  }
1057
1073
  }
1058
1074
 
@@ -1133,7 +1149,7 @@ fn calcDamage(
1133
1149
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1134
1150
  atk = @max((atk / 4) & 255, 1);
1135
1151
  // 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);
1152
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1137
1153
  }
1138
1154
 
1139
1155
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1154,7 +1170,7 @@ fn calcDamage(
1154
1170
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1155
1171
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1156
1172
  // that results in counter intuitive probabilities in the common case
1157
- if (pkmn.options.chance and !showdown) {
1173
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1158
1174
  atk = attack(side, !crit, special);
1159
1175
  def = defense(target, !crit, special, false);
1160
1176
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1239,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1239
1255
  .Psywave => power: {
1240
1256
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1241
1257
  // GLITCH: Psywave infinite glitch loop
1242
- if (!showdown and max <= 1) return Result.Error;
1258
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1243
1259
  break :power try Rolls.psywave(battle, player, max, options);
1244
1260
  },
1245
1261
  else => unreachable,
1246
1262
  };
1247
1263
 
1248
- 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;
1249
1265
 
1250
1266
  const sub = showdown and foe.active.volatiles.Substitute;
1251
1267
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1282,12 +1298,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1282
1298
 
1283
1299
  if (!used or !selected) {
1284
1300
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1285
- if (!showdown) return Result.Error;
1301
+ if (!pkmn.options.mod) return Result.Error;
1286
1302
  try options.log.fail(.{ battle.active(player), .None });
1287
1303
  return null;
1288
1304
  }
1289
1305
 
1290
- 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;
1291
1311
 
1292
1312
  // Pokémon Showdown calls moveHit before Counter
1293
1313
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1319,7 +1339,7 @@ fn applyDamage(
1319
1339
  subbed.active.volatiles.Substitute = false;
1320
1340
  // battle.last_damage is not updated with the amount of HP the Substitute had
1321
1341
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1322
- options.calc.capped(sub_player);
1342
+ if (reason != .Confusion) options.calc.capped(sub_player);
1323
1343
  return true;
1324
1344
  } else {
1325
1345
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1529,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1529
1549
  assert(!foe_volatiles.MultiHit);
1530
1550
  if (foe_volatiles.Bide) {
1531
1551
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1532
- 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;
1533
1553
  if (foe_volatiles.state != 0) return Result.Error;
1534
1554
  }
1535
1555
 
@@ -1620,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1620
1640
 
1621
1641
  battle.turn += 1;
1622
1642
 
1623
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1643
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1624
1644
  try options.log.tie(.{});
1625
1645
  return Result.Tie;
1626
1646
  }
1627
1647
 
1628
- if (showdown and battle.turn >= 1000) {
1648
+ if (pkmn.options.mod and battle.turn >= 1000) {
1629
1649
  try options.log.tie(.{});
1630
1650
  return Result.Tie;
1631
1651
  } else if (battle.turn >= 65535) {
@@ -1805,7 +1825,7 @@ pub const Effects = struct {
1805
1825
 
1806
1826
  side.active.volatiles.state = 0;
1807
1827
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1808
- options.chance.observe(.bide, player, .started);
1828
+ options.chance.observe(.attacking, player, .started);
1809
1829
 
1810
1830
  try options.log.start(.{ battle.active(player), .Bide });
1811
1831
  }
@@ -1874,6 +1894,7 @@ pub const Effects = struct {
1874
1894
 
1875
1895
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1876
1896
  options.chance.observe(.confusion, player.foe(), .started);
1897
+ options.calc.confusion(player.foe());
1877
1898
 
1878
1899
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1879
1900
  }
@@ -1919,12 +1940,11 @@ pub const Effects = struct {
1919
1940
 
1920
1941
  // Technically this is still considered simply a "miss" on the cartridge,
1921
1942
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1922
- if (n == 0) {
1943
+ if (n == 0 or (pkmn.options.mod and err)) {
1923
1944
  try options.log.fail(.{ foe_ident, .None });
1924
1945
  return null;
1925
1946
  } else if (err) {
1926
1947
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1927
- assert(!showdown);
1928
1948
  return Result.Error;
1929
1949
  }
1930
1950
 
@@ -2002,7 +2022,7 @@ pub const Effects = struct {
2002
2022
  if (foe.active.types.includes(move.type)) return;
2003
2023
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2004
2024
  // Freeze Clause Mod
2005
- 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;
2006
2026
 
2007
2027
  foe_stored.status = Status.init(.FRZ);
2008
2028
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2343,7 +2363,7 @@ pub const Effects = struct {
2343
2363
  }
2344
2364
 
2345
2365
  // Sleep Clause Mod
2346
- if (showdown) {
2366
+ if (pkmn.options.mod) {
2347
2367
  for (foe.pokemon) |p| {
2348
2368
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2349
2369
  }
@@ -2410,7 +2430,7 @@ pub const Effects = struct {
2410
2430
  volatiles.Thrashing = true;
2411
2431
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2412
2432
 
2413
- options.chance.observe(.thrashing, player, .started);
2433
+ options.chance.observe(.attacking, player, .started);
2414
2434
  }
2415
2435
 
2416
2436
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2449,7 +2469,7 @@ pub const Effects = struct {
2449
2469
  if (side.active.volatiles.Binding) return;
2450
2470
  side.active.volatiles.Binding = true;
2451
2471
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2452
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2472
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2453
2473
  if (!showdown) {
2454
2474
  foe.active.volatiles.Recharging = false;
2455
2475
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -2946,9 +2966,10 @@ pub const Rolls = struct {
2946
2966
  }
2947
2967
 
2948
2968
  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"))
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"))
2952
2973
  battle.rng.sleepDuration(player)
2953
2974
  else if (showdown)
2954
2975
  battle.rng.range(u3, 1, 8)
@@ -2965,9 +2986,10 @@ pub const Rolls = struct {
2965
2986
  }
2966
2987
 
2967
2988
  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"))
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"))
2971
2993
  battle.rng.disableDuration(player)
2972
2994
  else if (showdown)
2973
2995
  battle.rng.range(u4, 1, 9)
@@ -2980,9 +3002,10 @@ pub const Rolls = struct {
2980
3002
  }
2981
3003
 
2982
3004
  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"))
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"))
2986
3009
  battle.rng.confusionDuration(player)
2987
3010
  else if (showdown)
2988
3011
  battle.rng.range(u3, 2, 6)
@@ -2995,9 +3018,10 @@ pub const Rolls = struct {
2995
3018
  }
2996
3019
 
2997
3020
  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"))
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"))
3001
3025
  battle.rng.attackingDuration(player)
3002
3026
  else if (showdown)
3003
3027
  battle.rng.range(u3, 2, 4)
@@ -3018,9 +3042,10 @@ pub const Rolls = struct {
3018
3042
  options: anytype,
3019
3043
  ) !u3 {
3020
3044
  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"))
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"))
3024
3049
  battle.rng.distribution(player)
3025
3050
  else if (showdown)
3026
3051
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3046,15 +3071,13 @@ pub const Rolls = struct {
3046
3071
  options: anytype,
3047
3072
  ) !u4 {
3048
3073
  // TODO: consider throwing error instead of rerolling?
3049
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3074
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3050
3075
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3051
3076
  val
3052
3077
  else
3053
3078
  null
3054
3079
  else
3055
- null;
3056
-
3057
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3080
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3058
3081
  battle.rng.moveSlot(player, moves, check_pp)
3059
3082
  else slot: {
3060
3083
  if (showdown) {
@@ -3212,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3212
3235
  // Struggle (Pokémon Showdown would use 'move 1' here)
3213
3236
  if (n == before) {
3214
3237
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3215
- if (!showdown) {
3238
+ if (!pkmn.options.mod) {
3216
3239
  while (slot <= 4) : (slot += 1) {
3217
3240
  if (active.moves[slot - 1].pp != 0) return n;
3218
3241
  }
@@ -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
+ }