@pkmn/engine 0.1.0-dev.f3aab252 → 0.1.0-dev.f3b63b86

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 (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +35 -8
  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 +26 -4
  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 +39 -89
  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 +68 -74
  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 +235 -213
  39. package/package.json +59 -141
  40. package/src/bin/install-pkmn-engine +25 -9
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +200 -0
  43. package/src/lib/bindings/node.zig +117 -0
  44. package/src/lib/bindings/wasm.zig +50 -0
  45. package/src/lib/common/array.zig +5 -12
  46. package/src/lib/common/debug.zig +30 -9
  47. package/src/lib/common/js.zig +3 -9
  48. package/src/lib/common/optional.zig +11 -21
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +180 -171
  51. package/src/lib/common/rational.zig +278 -285
  52. package/src/lib/common/rng.zig +1 -3
  53. package/src/lib/common/util.zig +14 -31
  54. package/src/lib/gen1/calc.zig +258 -106
  55. package/src/lib/gen1/chance.zig +57 -54
  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 +3 -4
  59. package/src/lib/gen1/data.zig +12 -16
  60. package/src/lib/gen1/helpers.zig +13 -28
  61. package/src/lib/gen1/mechanics.zig +131 -101
  62. package/src/lib/gen2/calc.zig +12 -23
  63. package/src/lib/gen2/chance.zig +12 -18
  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 +7 -5
  68. package/src/lib/gen2/data.zig +19 -22
  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 +50 -47
  72. package/src/lib/napi.h +2 -0
  73. package/src/lib/node.zig +4 -121
  74. package/src/lib/pkmn.zig +98 -19
  75. package/src/lib/wasm.zig +9 -9
  76. package/src/tools/display/dom.ts +93 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -405
  79. package/build/test/display.js.map +0 -1
  80. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,36 +1,29 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const builtin = @import("builtin");
4
+ const chance = @import("chance.zig");
5
5
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
6
+ const data = @import("data.zig");
7
+ const pkmn = @import("../pkmn.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
-
10
- const data = @import("data.zig");
11
- const chance = @import("chance.zig");
10
+ const std = @import("std");
12
11
 
13
12
  const assert = std.debug.assert;
14
-
15
- const expectEqual = std.testing.expectEqual;
16
-
17
- const showdown = pkmn.options.showdown;
18
-
19
- const Choice = common.Choice;
20
- const ID = common.ID;
21
- const Player = common.Player;
22
- const Result = common.Result;
23
-
24
13
  const Boost = protocol.Boost;
14
+ const Choice = common.Choice;
25
15
  const Damage = protocol.Damage;
16
+ const Effectiveness = data.Effectiveness;
17
+ const expectEqual = std.testing.expectEqual;
26
18
  const Fail = protocol.Fail;
27
- const Heal = protocol.Heal;
28
-
29
19
  const Gen12 = rng.Gen12;
30
-
31
- const Effectiveness = data.Effectiveness;
20
+ const Heal = protocol.Heal;
21
+ const ID = common.ID;
32
22
  const Move = data.Move;
33
23
  const MoveSlot = data.MoveSlot;
24
+ const Player = common.Player;
25
+ const Result = common.Result;
26
+ const showdown = pkmn.options.showdown;
34
27
  const Side = data.Side;
35
28
  const Species = data.Species;
36
29
  const Stats = data.Stats;
@@ -173,7 +166,10 @@ fn selectMove(
173
166
  if (foe_choice.type == .Switch) {
174
167
  const last = side.active.move(battle.lastMove(player).index);
175
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
176
- 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
+ }
177
173
  }
178
174
  }
179
175
  return null;
@@ -424,7 +420,7 @@ fn executeMove(
424
420
  mslot = @intCast(battle.lastMove(player).index);
425
421
  const stored = side.stored();
426
422
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
427
- auto = isForced(&side.active) or
423
+ auto = pkmn.options.mod or isForced(&side.active) or
428
424
  side.active.volatiles.Binding or side.active.volatiles.Bide or
429
425
  side.last_selected_move == .HyperBeam or
430
426
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
@@ -457,6 +453,25 @@ fn executeMove(
457
453
  .err => return @as(?Result, Result.Error),
458
454
  }
459
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
+
460
475
  const can = skip_can or
461
476
  try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
477
  if (!can) return null;
@@ -554,18 +569,17 @@ fn beforeMove(
554
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
555
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
556
571
  if (side.last_selected_move == .None) {
557
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
558
573
  return .err;
559
574
  }
560
575
 
561
576
  if (volatiles.Confusion) {
562
577
  assert(volatiles.confusion > 0);
563
578
  if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
564
- .started => volatiles.confusion -= 1,
579
+ .started, .ended => volatiles.confusion = 0,
565
580
  .continuing => if (volatiles.confusion > 1) {
566
581
  volatiles.confusion -= 1;
567
582
  },
568
- .ended => volatiles.confusion = 0,
569
583
  .overwritten => {},
570
584
  else => unreachable,
571
585
  } else {
@@ -604,17 +618,10 @@ fn beforeMove(
604
618
  return .err;
605
619
  }
606
620
  }
607
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
608
- // on the targeting behavior of the confused Pokémon's selected move which results
609
- // in the wrong behavior with respect to the Substitute + Confusion glitch
610
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
611
- player
612
- else
613
- player.foe();
614
621
 
615
622
  const uncapped = battle.last_damage;
616
623
  // Skipping adjustDamage / randomizeDamage / checkHit
617
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
618
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
619
626
  if (showdown) battle.last_damage = uncapped;
620
627
 
@@ -704,7 +711,11 @@ fn beforeMove(
704
711
  // can be expected to recognize this pattern and pass the information only to the
705
712
  // correct player)
706
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
707
- options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
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
@@ -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;
@@ -862,7 +878,7 @@ fn doMove(
862
878
  var miss = showdown and miss: {
863
879
  immune = move.target != .Self and !status and !counter and
864
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
865
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
866
882
  if (immune and move.effect != .Binding) break :miss true;
867
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
868
884
  battle.last_damage = 0;
@@ -1003,7 +1019,7 @@ fn doMove(
1003
1019
  const sub = showdown and foe.active.volatiles.Substitute;
1004
1020
 
1005
1021
  var nullified = false;
1006
- var hit: u4 = 0;
1022
+ var hit: usize = 0;
1007
1023
  while (hit < hits) {
1008
1024
  if (hit == 0) {
1009
1025
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1031,23 +1047,21 @@ fn doMove(
1031
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1032
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1033
1049
  }
1034
- try log.hitcount(.{ battle.active(player.foe()), hit });
1035
- } else if (showdown) {
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1051
+ } else if (showdown and move.effect == .Binding) {
1036
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1037
- if (move.effect == .Binding) {
1038
- try Effects.binding(battle, player, rewrap, options);
1039
- if (immune) {
1040
- battle.last_damage = 0;
1041
- assert(foe.stored().hp > 0);
1042
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1043
- if (sub) {
1044
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1045
- } else {
1046
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1047
- try buildRage(battle, player.foe(), options);
1048
- }
1049
- 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);
1050
1063
  }
1064
+ return null;
1051
1065
  }
1052
1066
  }
1053
1067
 
@@ -1128,7 +1142,7 @@ fn calcDamage(
1128
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1129
1143
  atk = @max((atk / 4) & 255, 1);
1130
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1131
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1132
1146
  }
1133
1147
 
1134
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1149,7 +1163,7 @@ fn calcDamage(
1149
1163
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1150
1164
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1151
1165
  // that results in counter intuitive probabilities in the common case
1152
- if (pkmn.options.chance and !showdown) {
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1153
1167
  atk = attack(side, !crit, special);
1154
1168
  def = defense(target, !crit, special, false);
1155
1169
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1234,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1248
  .Psywave => power: {
1235
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1236
1250
  // GLITCH: Psywave infinite glitch loop
1237
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return Result.Error;
1238
1252
  break :power try Rolls.psywave(battle, player, max, options);
1239
1253
  },
1240
1254
  else => unreachable,
1241
1255
  };
1242
1256
 
1243
- 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;
1244
1258
 
1245
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1246
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1260,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1260
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1261
1275
  const foe_last_selected_move =
1262
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1263
- .Splash
1264
- else
1265
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1266
1280
 
1267
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1268
1282
  const selected = foe_last_selected_move.bp > 0 and
1269
1283
  foe.last_selected_move != .Counter and
1270
1284
  (foe_last_selected_move.type == .Normal or
1271
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1272
1286
 
1273
1287
  if (!used and !selected) {
1274
1288
  try options.log.fail(.{ battle.active(player), .None });
@@ -1277,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1277
1291
 
1278
1292
  if (!used or !selected) {
1279
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1280
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return Result.Error;
1281
1295
  try options.log.fail(.{ battle.active(player), .None });
1282
1296
  return null;
1283
1297
  }
1284
1298
 
1285
- 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;
1286
1304
 
1287
1305
  // Pokémon Showdown calls moveHit before Counter
1288
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1314,7 +1332,7 @@ fn applyDamage(
1314
1332
  subbed.active.volatiles.Substitute = false;
1315
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1316
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1317
- options.calc.capped(sub_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1318
1336
  return true;
1319
1337
  } else {
1320
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1428,7 +1446,16 @@ fn moveHit(
1428
1446
  break :miss true;
1429
1447
  }
1430
1448
  if (!showdown) {
1431
- 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
+ }
1432
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1433
1460
  }
1434
1461
 
@@ -1524,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1524
1551
  assert(!foe_volatiles.MultiHit);
1525
1552
  if (foe_volatiles.Bide) {
1526
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1527
- 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;
1528
1555
  if (foe_volatiles.state != 0) return Result.Error;
1529
1556
  }
1530
1557
 
@@ -1615,12 +1642,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1615
1642
 
1616
1643
  battle.turn += 1;
1617
1644
 
1618
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1619
1646
  try options.log.tie(.{});
1620
1647
  return Result.Tie;
1621
1648
  }
1622
1649
 
1623
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1624
1651
  try options.log.tie(.{});
1625
1652
  return Result.Tie;
1626
1653
  } else if (battle.turn >= 65535) {
@@ -1869,6 +1896,7 @@ pub const Effects = struct {
1869
1896
 
1870
1897
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1871
1898
  options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1872
1900
 
1873
1901
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1874
1902
  }
@@ -1914,12 +1942,11 @@ pub const Effects = struct {
1914
1942
 
1915
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1916
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1917
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1918
1946
  try options.log.fail(.{ foe_ident, .None });
1919
1947
  return null;
1920
1948
  } else if (err) {
1921
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1922
- assert(!showdown);
1923
1950
  return Result.Error;
1924
1951
  }
1925
1952
 
@@ -1997,7 +2024,7 @@ pub const Effects = struct {
1997
2024
  if (foe.active.types.includes(move.type)) return;
1998
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
1999
2026
  // Freeze Clause Mod
2000
- 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;
2001
2028
 
2002
2029
  foe_stored.status = Status.init(.FRZ);
2003
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2338,7 +2365,7 @@ pub const Effects = struct {
2338
2365
  }
2339
2366
 
2340
2367
  // Sleep Clause Mod
2341
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2342
2369
  for (foe.pokemon) |p| {
2343
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2344
2371
  }
@@ -2444,7 +2471,7 @@ pub const Effects = struct {
2444
2471
  if (side.active.volatiles.Binding) return;
2445
2472
  side.active.volatiles.Binding = true;
2446
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2447
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2448
2475
  if (!showdown) {
2449
2476
  foe.active.volatiles.Recharging = false;
2450
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -2941,9 +2968,10 @@ pub const Rolls = struct {
2941
2968
  }
2942
2969
 
2943
2970
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2944
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2945
- @intCast(val)
2946
- 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"))
2947
2975
  battle.rng.sleepDuration(player)
2948
2976
  else if (showdown)
2949
2977
  battle.rng.range(u3, 1, 8)
@@ -2960,9 +2988,10 @@ pub const Rolls = struct {
2960
2988
  }
2961
2989
 
2962
2990
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2963
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2964
- val
2965
- 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"))
2966
2995
  battle.rng.disableDuration(player)
2967
2996
  else if (showdown)
2968
2997
  battle.rng.range(u4, 1, 9)
@@ -2975,9 +3004,10 @@ pub const Rolls = struct {
2975
3004
  }
2976
3005
 
2977
3006
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2978
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2979
- @intCast(val)
2980
- 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"))
2981
3011
  battle.rng.confusionDuration(player)
2982
3012
  else if (showdown)
2983
3013
  battle.rng.range(u3, 2, 6)
@@ -2990,9 +3020,10 @@ pub const Rolls = struct {
2990
3020
  }
2991
3021
 
2992
3022
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2993
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2994
- @intCast(val)
2995
- 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"))
2996
3027
  battle.rng.attackingDuration(player)
2997
3028
  else if (showdown)
2998
3029
  battle.rng.range(u3, 2, 4)
@@ -3013,9 +3044,10 @@ pub const Rolls = struct {
3013
3044
  options: anytype,
3014
3045
  ) !u3 {
3015
3046
  const roll = if (effect == .Binding) .duration else .multi_hit;
3016
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
3017
- @intCast(val)
3018
- 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"))
3019
3051
  battle.rng.distribution(player)
3020
3052
  else if (showdown)
3021
3053
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3041,15 +3073,13 @@ pub const Rolls = struct {
3041
3073
  options: anytype,
3042
3074
  ) !u4 {
3043
3075
  // TODO: consider throwing error instead of rerolling?
3044
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3045
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3046
3078
  val
3047
3079
  else
3048
3080
  null
3049
3081
  else
3050
- null;
3051
-
3052
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3053
3083
  battle.rng.moveSlot(player, moves, check_pp)
3054
3084
  else slot: {
3055
3085
  if (showdown) {
@@ -3115,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3115
3145
  },
3116
3146
  .Switch => {
3117
3147
  const side = battle.side(player);
3118
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3119
3149
  while (slot <= 6) : (slot += 1) {
3120
3150
  const id = side.order[slot - 1];
3121
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3122
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3123
3153
  n += 1;
3124
3154
  }
3125
3155
  if (n == 0) {
@@ -3150,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3150
3180
  return n;
3151
3181
  }
3152
3182
 
3153
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3154
3184
  while (slot <= 6) : (slot += 1) {
3155
3185
  const id = side.order[slot - 1];
3156
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3157
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3158
3188
  n += 1;
3159
3189
  }
3160
3190
 
@@ -3187,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3187
3217
  const struggle =
3188
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3189
3219
  const s = if (struggle) 0 else slot;
3190
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3191
3221
  n += 1;
3192
3222
  return n;
3193
3223
  }
@@ -3201,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3201
3231
  if (m.id == .None) break;
3202
3232
  if (m.pp == 0) continue;
3203
3233
  if (active.volatiles.disable_move == slot) continue;
3204
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3205
3235
  n += 1;
3206
3236
  }
3207
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3208
3238
  if (n == before) {
3209
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3210
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3211
3241
  while (slot <= 4) : (slot += 1) {
3212
3242
  if (active.moves[slot - 1].pp != 0) return n;
3213
3243
  }
@@ -1,30 +1,19 @@
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");
6
- const util = @import("../common/util.zig");
7
-
8
3
  const Optional = @import("../common/optional.zig").Optional;
4
+ const pkmn = @import("../pkmn.zig");
5
+ const std = @import("std");
6
+ const util = @import("../common/util.zig");
9
7
 
10
- const chance = @import("chance.zig");
11
-
8
+ const Action = chance.Action;
9
+ const Actions = chance.Actions;
12
10
  const assert = std.debug.assert;
13
-
11
+ const Criticals = chance.Criticals;
12
+ const Damages = chance.Damages;
14
13
  const enabled = pkmn.options.calc;
15
-
14
+ const isPointerTo = util.isPointerTo;
16
15
  const Player = common.Player;
17
-
18
16
  const PointerType = util.PointerType;
19
- const isPointerTo = util.isPointerTo;
20
-
21
- const Actions = chance.Actions;
22
- const Action = chance.Action;
23
- const Criticals = chance.Criticals;
24
- const Damages = chance.Damages;
25
-
26
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
27
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
28
17
 
29
18
  /// Information relevant to damage calculation that occured during a Generation II battle `update`.
30
19
  pub const Summaries = extern struct {
@@ -99,8 +88,8 @@ pub const Calc = struct {
99
88
  };
100
89
 
101
90
  return if (switch (@typeInfo(@TypeOf(val))) {
102
- Enum => val != .None,
103
- Int => val != 0,
91
+ .@"enum" => val != .None,
92
+ .int => val != 0,
104
93
  else => unreachable,
105
94
  }) val else null;
106
95
  }
@@ -156,6 +145,6 @@ fn ReturnType(comptime field: Action.Field) type {
156
145
  return ?(switch (field) {
157
146
  .damages => u8,
158
147
  .critical_hits => Optional(bool),
159
- else => std.meta.FieldType(Action, field),
148
+ else => @FieldType(Action, @tagName(field)),
160
149
  });
161
150
  }