@pkmn/engine 0.1.0-dev.ce4c0b2a → 0.1.0-dev.d127cb34

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/LICENSE +1 -1
  2. package/README.md +23 -15
  3. package/build/pkg/addon/browser.d.ts +1 -1
  4. package/build/pkg/addon/browser.js +6 -2
  5. package/build/pkg/addon/browser.js.map +1 -1
  6. package/build/pkg/addon/node.js +22 -8
  7. package/build/pkg/addon/node.js.map +1 -1
  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 -5
  12. package/build/pkg/addon.js +8 -17
  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.js.map +1 -1
  17. package/build/pkg/gen1.d.ts +15 -18
  18. package/build/pkg/gen1.js +28 -19
  19. package/build/pkg/gen1.js.map +1 -1
  20. package/build/pkg/index.d.ts +37 -100
  21. package/build/pkg/index.js +23 -75
  22. package/build/pkg/index.js.map +1 -1
  23. package/build/pkg/protocol.d.ts +11 -2
  24. package/build/pkg/protocol.js.map +1 -1
  25. package/build/tools/debug.js +56 -13
  26. package/build/tools/debug.js.map +1 -1
  27. package/build/tools/display/debug.jsx +19 -81
  28. package/build/tools/display/debug.jsx.map +1 -1
  29. package/build/tools/display/index.js +35 -15
  30. package/build/tools/display/index.js.map +1 -1
  31. package/build/tools/display/ui.jsx +27 -16
  32. package/build/tools/display/ui.jsx.map +1 -1
  33. package/build/tools/display/util.js +141 -2
  34. package/build/tools/display/util.js.map +1 -1
  35. package/build.zig +330 -245
  36. package/package.json +36 -128
  37. package/src/bin/install-pkmn-engine +25 -9
  38. package/src/lib/bindings/c.zig +201 -0
  39. package/src/lib/bindings/node.zig +117 -0
  40. package/src/lib/bindings/wasm.zig +51 -0
  41. package/src/lib/common/array.zig +5 -12
  42. package/src/lib/common/debug.zig +45 -34
  43. package/src/lib/common/js.zig +3 -9
  44. package/src/lib/common/optional.zig +26 -26
  45. package/src/lib/common/options.zig +2 -0
  46. package/src/lib/common/protocol.zig +228 -197
  47. package/src/lib/common/rational.zig +278 -285
  48. package/src/lib/common/rng.zig +1 -3
  49. package/src/lib/common/util.zig +34 -31
  50. package/src/lib/gen1/calc.zig +220 -195
  51. package/src/lib/gen1/chance.zig +184 -108
  52. package/src/lib/gen1/data/moves.zig +2 -4
  53. package/src/lib/gen1/data/species.zig +1 -3
  54. package/src/lib/gen1/data/types.zig +3 -4
  55. package/src/lib/gen1/data.zig +12 -16
  56. package/src/lib/gen1/helpers.zig +35 -40
  57. package/src/lib/gen1/mechanics.zig +147 -127
  58. package/src/lib/gen2/calc.zig +12 -23
  59. package/src/lib/gen2/chance.zig +38 -26
  60. package/src/lib/gen2/data/items.zig +1 -3
  61. package/src/lib/gen2/data/moves.zig +2 -4
  62. package/src/lib/gen2/data/species.zig +1 -3
  63. package/src/lib/gen2/data/types.zig +7 -5
  64. package/src/lib/gen2/data.zig +19 -22
  65. package/src/lib/gen2/generated.zig +18 -23
  66. package/src/lib/gen2/helpers.zig +32 -31
  67. package/src/lib/gen2/mechanics.zig +58 -55
  68. package/src/lib/napi.h +2 -0
  69. package/src/lib/node.zig +4 -121
  70. package/src/lib/pkmn.zig +98 -19
  71. package/src/lib/wasm.zig +9 -9
  72. package/src/tools/display/dom.ts +43 -31
  73. package/src/tools/display/ui.css +12 -9
@@ -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;
@@ -91,11 +84,11 @@ fn start(battle: anytype, options: anytype) !Result {
91
84
 
92
85
  const p1_slot = findFirstAlive(p1);
93
86
  assert(!showdown or p1_slot == 1);
94
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
87
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
95
88
 
96
89
  const p2_slot = findFirstAlive(p2);
97
90
  assert(!showdown or p2_slot == 1);
98
- if (p2_slot == 0) return Result.Win;
91
+ if (p2_slot == 0) return .Win;
99
92
 
100
93
  try switchIn(battle, .P1, p1_slot, true, options);
101
94
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -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 .Error;
171
+ side.last_selected_move = last.id;
172
+ }
177
173
  }
178
174
  }
179
175
  return null;
@@ -341,7 +337,6 @@ fn doTurn(
341
337
  player_choice,
342
338
  player_rewrap,
343
339
  player_skip,
344
- true,
345
340
  &residual,
346
341
  options,
347
342
  )) |r| return r;
@@ -364,7 +359,6 @@ fn doTurn(
364
359
  foe_choice,
365
360
  foe_rewrap,
366
361
  foe_skip,
367
- false,
368
362
  &residual,
369
363
  options,
370
364
  )) |r| return r;
@@ -392,7 +386,6 @@ fn executeMove(
392
386
  choice: Choice,
393
387
  rewrap: bool,
394
388
  skip: bool,
395
- first: bool,
396
389
  residual: *bool,
397
390
  options: anytype,
398
391
  ) !?Result {
@@ -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);
@@ -439,7 +432,7 @@ fn executeMove(
439
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
440
433
  // every Pokémon must have at least one move
441
434
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
442
- return Result.Error;
435
+ return .Error;
443
436
  } else {
444
437
  auto = true;
445
438
  }
@@ -452,12 +445,31 @@ fn executeMove(
452
445
 
453
446
  var skip_can = false;
454
447
  var skip_pp = false;
455
- switch (try beforeMove(battle, player, skip, first, residual, options)) {
448
+ switch (try beforeMove(battle, player, skip, residual, options)) {
456
449
  .done => return null,
457
450
  .skip_can => skip_can = true,
458
451
  .skip_pp => skip_pp = true,
459
452
  .ok => {},
460
- .err => return @as(?Result, Result.Error),
453
+ .err => return .Error,
454
+ }
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;
461
473
  }
462
474
 
463
475
  const can = skip_can or
@@ -473,7 +485,6 @@ fn beforeMove(
473
485
  battle: anytype,
474
486
  player: Player,
475
487
  skip: bool,
476
- first: bool,
477
488
  residual: *bool,
478
489
  options: anytype,
479
490
  ) !BeforeMove {
@@ -558,24 +569,17 @@ fn beforeMove(
558
569
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
559
570
  // before the side had a selected a move - we simply need to assume this leads to a desync
560
571
  if (side.last_selected_move == .None) {
561
- assert(!showdown);
572
+ assert(!pkmn.options.mod);
562
573
  return .err;
563
574
  }
564
575
 
565
576
  if (volatiles.Confusion) {
566
577
  assert(volatiles.confusion > 0);
567
578
  if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
568
- .started => {
569
- if (!first or (options.calc.overridden(player, .confused) orelse .None) != .None) {
570
- volatiles.confusion -= 1;
571
- } else {
572
- volatiles.confusion = 0;
573
- }
574
- },
579
+ .started, .ended => volatiles.confusion = 0,
575
580
  .continuing => if (volatiles.confusion > 1) {
576
581
  volatiles.confusion -= 1;
577
582
  },
578
- .ended => volatiles.confusion = 0,
579
583
  .overwritten => {},
580
584
  else => unreachable,
581
585
  } else {
@@ -614,17 +618,10 @@ fn beforeMove(
614
618
  return .err;
615
619
  }
616
620
  }
617
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
618
- // on the targeting behavior of the confused Pokémon's selected move which results
619
- // in the wrong behavior with respect to the Substitute + Confusion glitch
620
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
621
- player
622
- else
623
- player.foe();
624
621
 
625
622
  const uncapped = battle.last_damage;
626
623
  // Skipping adjustDamage / randomizeDamage / checkHit
627
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
628
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
629
626
  if (showdown) battle.last_damage = uncapped;
630
627
 
@@ -703,7 +700,7 @@ fn beforeMove(
703
700
  try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
704
701
 
705
702
  if (volatiles.attacks == 0) {
706
- const overwritten = pkmn.options.overwrite and volatiles.Confusion;
703
+ const overwritten = volatiles.Confusion;
707
704
 
708
705
  volatiles.Thrashing = false;
709
706
  volatiles.Confusion = true;
@@ -714,7 +711,11 @@ fn beforeMove(
714
711
  // can be expected to recognize this pattern and pass the information only to the
715
712
  // correct player)
716
713
  try log.start(.{ battle.active(player), .ConfusionSilent });
717
- 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);
718
719
  }
719
720
 
720
721
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
@@ -780,9 +781,14 @@ fn canMove(
780
781
 
781
782
  if (move.effect == .Thrashing) {
782
783
  Effects.thrashing(battle, player, options);
783
- } else if (!showdown and move.effect == .Binding) {
784
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
785
- 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
+ }
786
792
  }
787
793
 
788
794
  return true;
@@ -872,7 +878,7 @@ fn doMove(
872
878
  var miss = showdown and miss: {
873
879
  immune = move.target != .Self and !status and !counter and
874
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
875
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
876
882
  if (immune and move.effect != .Binding) break :miss true;
877
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
878
884
  battle.last_damage = 0;
@@ -910,7 +916,7 @@ fn doMove(
910
916
  if (showdown) break :damage; // skip adjustDamage / randomizeDamage
911
917
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
912
918
  if (!showdown) try options.chance.commit(player, .err);
913
- return @as(?Result, Result.Error);
919
+ return .Error;
914
920
  }
915
921
  if (battle.last_damage == 0) {
916
922
  immune = true;
@@ -1013,7 +1019,7 @@ fn doMove(
1013
1019
  const sub = showdown and foe.active.volatiles.Substitute;
1014
1020
 
1015
1021
  var nullified = false;
1016
- var hit: u4 = 0;
1022
+ var hit: usize = 0;
1017
1023
  while (hit < hits) {
1018
1024
  if (hit == 0) {
1019
1025
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1041,23 +1047,21 @@ fn doMove(
1041
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1042
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1043
1049
  }
1044
- try log.hitcount(.{ battle.active(player.foe()), hit });
1045
- } else if (showdown) {
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1051
+ } else if (showdown and move.effect == .Binding) {
1046
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1047
- if (move.effect == .Binding) {
1048
- try Effects.binding(battle, player, rewrap, options);
1049
- if (immune) {
1050
- battle.last_damage = 0;
1051
- assert(foe.stored().hp > 0);
1052
- // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
1053
- if (sub) {
1054
- try log.activate(.{ battle.active(player.foe()), .Substitute });
1055
- } else {
1056
- try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
1057
- try buildRage(battle, player.foe(), options);
1058
- }
1059
- 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);
1060
1063
  }
1064
+ return null;
1061
1065
  }
1062
1066
  }
1063
1067
 
@@ -1138,7 +1142,7 @@ fn calcDamage(
1138
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1139
1143
  atk = @max((atk / 4) & 255, 1);
1140
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1141
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1142
1146
  }
1143
1147
 
1144
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1159,7 +1163,7 @@ fn calcDamage(
1159
1163
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1160
1164
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1161
1165
  // that results in counter intuitive probabilities in the common case
1162
- if (pkmn.options.chance and !showdown) {
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1163
1167
  atk = attack(side, !crit, special);
1164
1168
  def = defense(target, !crit, special, false);
1165
1169
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1244,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1244
1248
  .Psywave => power: {
1245
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1246
1250
  // GLITCH: Psywave infinite glitch loop
1247
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return .Error;
1248
1252
  break :power try Rolls.psywave(battle, player, max, options);
1249
1253
  },
1250
1254
  else => unreachable,
1251
1255
  };
1252
1256
 
1253
- 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 .Error;
1254
1258
 
1255
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1256
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1270,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1270
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1271
1275
  const foe_last_selected_move =
1272
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1273
- .Splash
1274
- else
1275
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1276
1280
 
1277
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1278
1282
  const selected = foe_last_selected_move.bp > 0 and
1279
1283
  foe.last_selected_move != .Counter and
1280
1284
  (foe_last_selected_move.type == .Normal or
1281
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1282
1286
 
1283
1287
  if (!used and !selected) {
1284
1288
  try options.log.fail(.{ battle.active(player), .None });
@@ -1287,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1287
1291
 
1288
1292
  if (!used or !selected) {
1289
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1290
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return .Error;
1291
1295
  try options.log.fail(.{ battle.active(player), .None });
1292
1296
  return null;
1293
1297
  }
1294
1298
 
1295
- 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;
1296
1304
 
1297
1305
  // Pokémon Showdown calls moveHit before Counter
1298
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1324,7 +1332,7 @@ fn applyDamage(
1324
1332
  subbed.active.volatiles.Substitute = false;
1325
1333
  // battle.last_damage is not updated with the amount of HP the Substitute had
1326
1334
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1327
- options.calc.capped(sub_player);
1335
+ if (reason != .Confusion) options.calc.capped(sub_player);
1328
1336
  return true;
1329
1337
  } else {
1330
1338
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1438,7 +1446,16 @@ fn moveHit(
1438
1446
  break :miss true;
1439
1447
  }
1440
1448
  if (!showdown) {
1441
- 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
+ }
1442
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1443
1460
  }
1444
1461
 
@@ -1511,13 +1528,13 @@ fn checkFaint(
1511
1528
 
1512
1529
  if (player_out and foe_out) {
1513
1530
  try options.log.tie(.{});
1514
- return Result.Tie;
1531
+ return .Tie;
1515
1532
  } else if (player_out) {
1516
1533
  try options.log.win(.{player.foe()});
1517
- return if (player == .P1) Result.Lose else Result.Win;
1534
+ return if (player == .P1) .Lose else .Win;
1518
1535
  } else if (foe_out) {
1519
1536
  try options.log.win(.{player});
1520
- return if (player == .P1) Result.Win else Result.Lose;
1537
+ return if (player == .P1) .Win else .Lose;
1521
1538
  }
1522
1539
 
1523
1540
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1534,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1534
1551
  assert(!foe_volatiles.MultiHit);
1535
1552
  if (foe_volatiles.Bide) {
1536
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1537
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1538
- if (foe_volatiles.state != 0) return Result.Error;
1554
+ foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1555
+ if (foe_volatiles.state != 0) return .Error;
1539
1556
  }
1540
1557
 
1541
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1625,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1625
1642
 
1626
1643
  battle.turn += 1;
1627
1644
 
1628
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1629
1646
  try options.log.tie(.{});
1630
- return Result.Tie;
1647
+ return .Tie;
1631
1648
  }
1632
1649
 
1633
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1634
1651
  try options.log.tie(.{});
1635
- return Result.Tie;
1652
+ return .Tie;
1636
1653
  } else if (battle.turn >= 65535) {
1637
- return Result.Error;
1654
+ return .Error;
1638
1655
  }
1639
1656
 
1640
1657
  try options.log.turn(.{battle.turn});
1641
1658
 
1642
- return Result.Default;
1659
+ return .Default;
1643
1660
  }
1644
1661
 
1645
1662
  fn checkEBC(battle: anytype) bool {
@@ -1879,6 +1896,7 @@ pub const Effects = struct {
1879
1896
 
1880
1897
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1881
1898
  options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1882
1900
 
1883
1901
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1884
1902
  }
@@ -1924,13 +1942,12 @@ pub const Effects = struct {
1924
1942
 
1925
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1926
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1927
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1928
1946
  try options.log.fail(.{ foe_ident, .None });
1929
1947
  return null;
1930
1948
  } else if (err) {
1931
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1932
- assert(!showdown);
1933
- return Result.Error;
1950
+ return .Error;
1934
1951
  }
1935
1952
 
1936
1953
  volatiles.disable_move =
@@ -2007,7 +2024,7 @@ pub const Effects = struct {
2007
2024
  if (foe.active.types.includes(move.type)) return;
2008
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2009
2026
  // Freeze Clause Mod
2010
- 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;
2011
2028
 
2012
2029
  foe_stored.status = Status.init(.FRZ);
2013
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2348,7 +2365,7 @@ pub const Effects = struct {
2348
2365
  }
2349
2366
 
2350
2367
  // Sleep Clause Mod
2351
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2352
2369
  for (foe.pokemon) |p| {
2353
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2354
2371
  }
@@ -2454,7 +2471,7 @@ pub const Effects = struct {
2454
2471
  if (side.active.volatiles.Binding) return;
2455
2472
  side.active.volatiles.Binding = true;
2456
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2457
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2458
2475
  if (!showdown) {
2459
2476
  foe.active.volatiles.Recharging = false;
2460
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -2951,9 +2968,10 @@ pub const Rolls = struct {
2951
2968
  }
2952
2969
 
2953
2970
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2954
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2955
- @intCast(val)
2956
- 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"))
2957
2975
  battle.rng.sleepDuration(player)
2958
2976
  else if (showdown)
2959
2977
  battle.rng.range(u3, 1, 8)
@@ -2970,9 +2988,10 @@ pub const Rolls = struct {
2970
2988
  }
2971
2989
 
2972
2990
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2973
- const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2974
- val
2975
- 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"))
2976
2995
  battle.rng.disableDuration(player)
2977
2996
  else if (showdown)
2978
2997
  battle.rng.range(u4, 1, 9)
@@ -2985,9 +3004,10 @@ pub const Rolls = struct {
2985
3004
  }
2986
3005
 
2987
3006
  fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2988
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2989
- @intCast(val)
2990
- 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"))
2991
3011
  battle.rng.confusionDuration(player)
2992
3012
  else if (showdown)
2993
3013
  battle.rng.range(u3, 2, 6)
@@ -3000,9 +3020,10 @@ pub const Rolls = struct {
3000
3020
  }
3001
3021
 
3002
3022
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
3003
- const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
3004
- @intCast(val)
3005
- 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"))
3006
3027
  battle.rng.attackingDuration(player)
3007
3028
  else if (showdown)
3008
3029
  battle.rng.range(u3, 2, 4)
@@ -3023,9 +3044,10 @@ pub const Rolls = struct {
3023
3044
  options: anytype,
3024
3045
  ) !u3 {
3025
3046
  const roll = if (effect == .Binding) .duration else .multi_hit;
3026
- const n: u3 = if (options.calc.overridden(player, roll)) |val|
3027
- @intCast(val)
3028
- 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"))
3029
3051
  battle.rng.distribution(player)
3030
3052
  else if (showdown)
3031
3053
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
@@ -3051,15 +3073,13 @@ pub const Rolls = struct {
3051
3073
  options: anytype,
3052
3074
  ) !u4 {
3053
3075
  // TODO: consider throwing error instead of rerolling?
3054
- const overridden = if (options.calc.overridden(player, .move_slot)) |val|
3076
+ const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
3055
3077
  if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
3056
3078
  val
3057
3079
  else
3058
3080
  null
3059
3081
  else
3060
- null;
3061
-
3062
- const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3082
+ null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3063
3083
  battle.rng.moveSlot(player, moves, check_pp)
3064
3084
  else slot: {
3065
3085
  if (showdown) {
@@ -3125,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3125
3145
  },
3126
3146
  .Switch => {
3127
3147
  const side = battle.side(player);
3128
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3129
3149
  while (slot <= 6) : (slot += 1) {
3130
3150
  const id = side.order[slot - 1];
3131
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3132
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3133
3153
  n += 1;
3134
3154
  }
3135
3155
  if (n == 0) {
@@ -3160,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3160
3180
  return n;
3161
3181
  }
3162
3182
 
3163
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3164
3184
  while (slot <= 6) : (slot += 1) {
3165
3185
  const id = side.order[slot - 1];
3166
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3167
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3168
3188
  n += 1;
3169
3189
  }
3170
3190
 
@@ -3197,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3197
3217
  const struggle =
3198
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3199
3219
  const s = if (struggle) 0 else slot;
3200
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3201
3221
  n += 1;
3202
3222
  return n;
3203
3223
  }
@@ -3211,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3211
3231
  if (m.id == .None) break;
3212
3232
  if (m.pp == 0) continue;
3213
3233
  if (active.volatiles.disable_move == slot) continue;
3214
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3215
3235
  n += 1;
3216
3236
  }
3217
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3218
3238
  if (n == before) {
3219
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3220
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3221
3241
  while (slot <= 4) : (slot += 1) {
3222
3242
  if (active.moves[slot - 1].pp != 0) return n;
3223
3243
  }