@pkmn/engine 0.1.0-dev.3393edb3 → 0.1.0-dev.3846c8e6

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 (72) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +28 -13
  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/gen1.d.ts +15 -18
  17. package/build/pkg/gen1.js +28 -19
  18. package/build/pkg/gen1.js.map +1 -1
  19. package/build/pkg/index.d.ts +37 -100
  20. package/build/pkg/index.js +23 -75
  21. package/build/pkg/index.js.map +1 -1
  22. package/build/pkg/protocol.d.ts +11 -2
  23. package/build/pkg/protocol.js.map +1 -1
  24. package/build/tools/debug.js +55 -12
  25. package/build/tools/debug.js.map +1 -1
  26. package/build/tools/display/debug.jsx +19 -94
  27. package/build/tools/display/debug.jsx.map +1 -1
  28. package/build/tools/display/index.js +35 -15
  29. package/build/tools/display/index.js.map +1 -1
  30. package/build/tools/display/ui.jsx +20 -9
  31. package/build/tools/display/ui.jsx.map +1 -1
  32. package/build/tools/display/util.js +140 -1
  33. package/build/tools/display/util.js.map +1 -1
  34. package/build.zig +236 -216
  35. package/package.json +35 -131
  36. package/src/bin/install-pkmn-engine +25 -9
  37. package/src/lib/bindings/c.zig +200 -0
  38. package/src/lib/bindings/node.zig +117 -0
  39. package/src/lib/bindings/wasm.zig +50 -0
  40. package/src/lib/common/array.zig +5 -12
  41. package/src/lib/common/debug.zig +33 -54
  42. package/src/lib/common/js.zig +3 -9
  43. package/src/lib/common/optional.zig +11 -21
  44. package/src/lib/common/options.zig +2 -0
  45. package/src/lib/common/protocol.zig +228 -197
  46. package/src/lib/common/rational.zig +278 -285
  47. package/src/lib/common/rng.zig +1 -3
  48. package/src/lib/common/util.zig +14 -31
  49. package/src/lib/gen1/calc.zig +205 -189
  50. package/src/lib/gen1/chance.zig +146 -93
  51. package/src/lib/gen1/data/moves.zig +2 -4
  52. package/src/lib/gen1/data/species.zig +1 -3
  53. package/src/lib/gen1/data/types.zig +3 -4
  54. package/src/lib/gen1/data.zig +12 -16
  55. package/src/lib/gen1/helpers.zig +12 -21
  56. package/src/lib/gen1/mechanics.zig +112 -93
  57. package/src/lib/gen2/calc.zig +12 -23
  58. package/src/lib/gen2/chance.zig +12 -18
  59. package/src/lib/gen2/data/items.zig +1 -3
  60. package/src/lib/gen2/data/moves.zig +2 -4
  61. package/src/lib/gen2/data/species.zig +1 -3
  62. package/src/lib/gen2/data/types.zig +7 -5
  63. package/src/lib/gen2/data.zig +19 -22
  64. package/src/lib/gen2/generated.zig +18 -23
  65. package/src/lib/gen2/helpers.zig +9 -18
  66. package/src/lib/gen2/mechanics.zig +58 -55
  67. package/src/lib/napi.h +2 -0
  68. package/src/lib/node.zig +4 -121
  69. package/src/lib/pkmn.zig +98 -19
  70. package/src/lib/wasm.zig +9 -9
  71. package/src/tools/display/dom.ts +46 -34
  72. package/src/tools/display/ui.css +8 -5
@@ -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,7 +1047,7 @@ 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 });
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1045
1051
  } else if (showdown and move.effect == .Binding) {
1046
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1047
1053
  try Effects.binding(battle, player, rewrap, options);
@@ -1136,7 +1142,7 @@ fn calcDamage(
1136
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1137
1143
  atk = @max((atk / 4) & 255, 1);
1138
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1139
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1140
1146
  }
1141
1147
 
1142
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1157,7 +1163,7 @@ fn calcDamage(
1157
1163
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1158
1164
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1159
1165
  // that results in counter intuitive probabilities in the common case
1160
- if (pkmn.options.chance and !showdown) {
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1161
1167
  atk = attack(side, !crit, special);
1162
1168
  def = defense(target, !crit, special, false);
1163
1169
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1242,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1242
1248
  .Psywave => power: {
1243
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1244
1250
  // GLITCH: Psywave infinite glitch loop
1245
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return .Error;
1246
1252
  break :power try Rolls.psywave(battle, player, max, options);
1247
1253
  },
1248
1254
  else => unreachable,
1249
1255
  };
1250
1256
 
1251
- 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;
1252
1258
 
1253
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1254
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1268,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1268
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1269
1275
  const foe_last_selected_move =
1270
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1271
- .Splash
1272
- else
1273
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1274
1280
 
1275
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1276
1282
  const selected = foe_last_selected_move.bp > 0 and
1277
1283
  foe.last_selected_move != .Counter and
1278
1284
  (foe_last_selected_move.type == .Normal or
1279
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1280
1286
 
1281
1287
  if (!used and !selected) {
1282
1288
  try options.log.fail(.{ battle.active(player), .None });
@@ -1285,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1285
1291
 
1286
1292
  if (!used or !selected) {
1287
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1288
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return .Error;
1289
1295
  try options.log.fail(.{ battle.active(player), .None });
1290
1296
  return null;
1291
1297
  }
1292
1298
 
1293
- 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;
1294
1304
 
1295
1305
  // Pokémon Showdown calls moveHit before Counter
1296
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1436,7 +1446,16 @@ fn moveHit(
1436
1446
  break :miss true;
1437
1447
  }
1438
1448
  if (!showdown) {
1439
- 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
+ }
1440
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1441
1460
  }
1442
1461
 
@@ -1509,13 +1528,13 @@ fn checkFaint(
1509
1528
 
1510
1529
  if (player_out and foe_out) {
1511
1530
  try options.log.tie(.{});
1512
- return Result.Tie;
1531
+ return .Tie;
1513
1532
  } else if (player_out) {
1514
1533
  try options.log.win(.{player.foe()});
1515
- return if (player == .P1) Result.Lose else Result.Win;
1534
+ return if (player == .P1) .Lose else .Win;
1516
1535
  } else if (foe_out) {
1517
1536
  try options.log.win(.{player});
1518
- return if (player == .P1) Result.Win else Result.Lose;
1537
+ return if (player == .P1) .Win else .Lose;
1519
1538
  }
1520
1539
 
1521
1540
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1532,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1532
1551
  assert(!foe_volatiles.MultiHit);
1533
1552
  if (foe_volatiles.Bide) {
1534
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1535
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1536
- 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;
1537
1556
  }
1538
1557
 
1539
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1623,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1623
1642
 
1624
1643
  battle.turn += 1;
1625
1644
 
1626
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1627
1646
  try options.log.tie(.{});
1628
- return Result.Tie;
1647
+ return .Tie;
1629
1648
  }
1630
1649
 
1631
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1632
1651
  try options.log.tie(.{});
1633
- return Result.Tie;
1652
+ return .Tie;
1634
1653
  } else if (battle.turn >= 65535) {
1635
- return Result.Error;
1654
+ return .Error;
1636
1655
  }
1637
1656
 
1638
1657
  try options.log.turn(.{battle.turn});
1639
1658
 
1640
- return Result.Default;
1659
+ return .Default;
1641
1660
  }
1642
1661
 
1643
1662
  fn checkEBC(battle: anytype) bool {
@@ -1877,6 +1896,7 @@ pub const Effects = struct {
1877
1896
 
1878
1897
  foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1879
1898
  options.chance.observe(.confusion, player.foe(), .started);
1899
+ options.calc.confusion(player.foe());
1880
1900
 
1881
1901
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1882
1902
  }
@@ -1922,13 +1942,12 @@ pub const Effects = struct {
1922
1942
 
1923
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1924
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1925
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1926
1946
  try options.log.fail(.{ foe_ident, .None });
1927
1947
  return null;
1928
1948
  } else if (err) {
1929
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1930
- assert(!showdown);
1931
- return Result.Error;
1950
+ return .Error;
1932
1951
  }
1933
1952
 
1934
1953
  volatiles.disable_move =
@@ -2005,7 +2024,7 @@ pub const Effects = struct {
2005
2024
  if (foe.active.types.includes(move.type)) return;
2006
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2007
2026
  // Freeze Clause Mod
2008
- 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;
2009
2028
 
2010
2029
  foe_stored.status = Status.init(.FRZ);
2011
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2346,7 +2365,7 @@ pub const Effects = struct {
2346
2365
  }
2347
2366
 
2348
2367
  // Sleep Clause Mod
2349
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2350
2369
  for (foe.pokemon) |p| {
2351
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2352
2371
  }
@@ -2452,7 +2471,7 @@ pub const Effects = struct {
2452
2471
  if (side.active.volatiles.Binding) return;
2453
2472
  side.active.volatiles.Binding = true;
2454
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2455
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2456
2475
  if (!showdown) {
2457
2476
  foe.active.volatiles.Recharging = false;
2458
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -3126,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3126
3145
  },
3127
3146
  .Switch => {
3128
3147
  const side = battle.side(player);
3129
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3130
3149
  while (slot <= 6) : (slot += 1) {
3131
3150
  const id = side.order[slot - 1];
3132
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3133
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3134
3153
  n += 1;
3135
3154
  }
3136
3155
  if (n == 0) {
@@ -3161,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3161
3180
  return n;
3162
3181
  }
3163
3182
 
3164
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3165
3184
  while (slot <= 6) : (slot += 1) {
3166
3185
  const id = side.order[slot - 1];
3167
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3168
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3169
3188
  n += 1;
3170
3189
  }
3171
3190
 
@@ -3198,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3198
3217
  const struggle =
3199
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3200
3219
  const s = if (struggle) 0 else slot;
3201
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3202
3221
  n += 1;
3203
3222
  return n;
3204
3223
  }
@@ -3212,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3212
3231
  if (m.id == .None) break;
3213
3232
  if (m.pp == 0) continue;
3214
3233
  if (active.volatiles.disable_move == slot) continue;
3215
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3216
3235
  n += 1;
3217
3236
  }
3218
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3219
3238
  if (n == before) {
3220
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3221
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3222
3241
  while (slot <= 4) : (slot += 1) {
3223
3242
  if (active.moves[slot - 1].pp != 0) return n;
3224
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
  }
@@ -27,10 +27,6 @@ const Type = data.Type;
27
27
  const Effectiveness = data.Effectiveness;
28
28
  const TriAttack = data.TriAttack;
29
29
 
30
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
31
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
32
- const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
33
-
34
30
  /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
35
31
  /// were observed during a Generation II battle `update`. This can additionally be provided as input
36
32
  /// to the `update` call to override the normal behavior of the RNG in order to force specific
@@ -57,15 +53,15 @@ pub const Actions = extern struct {
57
53
  /// same shape if they have the same fields set (though those fields need not necessarily be
58
54
  /// set to the same value).
59
55
  pub fn matches(a: Actions, b: Actions) bool {
60
- inline for (@field(@typeInfo(Actions), @tagName(Struct)).fields) |player| {
61
- inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
56
+ inline for (@typeInfo(Actions).@"struct".fields) |player| {
57
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
62
58
  const a_val = @field(@field(a, player.name), field.name);
63
59
  const b_val = @field(@field(b, player.name), field.name);
64
60
 
65
61
  switch (@typeInfo(@TypeOf(a_val))) {
66
- Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
62
+ .@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
67
63
  return false,
68
- Int => if ((a_val > 0) != (b_val > 0)) return false,
64
+ .int => if ((a_val > 0) != (b_val > 0)) return false,
69
65
  else => unreachable,
70
66
  }
71
67
  }
@@ -73,7 +69,7 @@ pub const Actions = extern struct {
73
69
  return true;
74
70
  }
75
71
 
76
- pub fn fmt(self: Actions, writer: anytype, shape: bool) !void {
72
+ pub fn fmt(self: Actions, writer: *std.Io.Writer, shape: bool) !void {
77
73
  try writer.writeAll("<P1 = ");
78
74
  try self.p1.fmt(writer, shape);
79
75
  try writer.writeAll(", P2 = ");
@@ -81,8 +77,7 @@ pub const Actions = extern struct {
81
77
  try writer.writeAll(">");
82
78
  }
83
79
 
84
- pub fn format(a: Actions, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
85
- _ = .{ f, o };
80
+ pub fn format(a: Actions, w: *std.Io.Writer) !void {
86
81
  try fmt(a, w, false);
87
82
  }
88
83
  };
@@ -186,18 +181,17 @@ pub const Action = packed struct(u128) {
186
181
 
187
182
  pub const Field = std.meta.FieldEnum(Action);
188
183
 
189
- pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
190
- _ = .{ f, o };
184
+ pub fn format(a: Action, w: *std.Io.Writer) !void {
191
185
  try fmt(a, w, false);
192
186
  }
193
187
 
194
- pub fn fmt(self: Action, writer: anytype, shape: bool) !void {
188
+ pub fn fmt(self: Action, writer: *std.Io.Writer, shape: bool) !void {
195
189
  try writer.writeByte('(');
196
190
  var printed = false;
197
- inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
191
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
198
192
  const val = @field(self, field.name);
199
193
  switch (@typeInfo(@TypeOf(val))) {
200
- Enum => if (val != .None) {
194
+ .@"enum" => if (val != .None) {
201
195
  if (printed) try writer.writeAll(", ");
202
196
  if (shape) {
203
197
  try writer.print("{s}:?", .{field.name});
@@ -211,7 +205,7 @@ pub const Action = packed struct(u128) {
211
205
  }
212
206
  printed = true;
213
207
  },
214
- Int => if (val != 0) {
208
+ .int => if (val != 0) {
215
209
  if (printed) try writer.writeAll(", ");
216
210
  if (shape) {
217
211
  try writer.print("{s}:?", .{field.name});
@@ -786,7 +780,7 @@ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
786
780
 
787
781
  r.reduce();
788
782
  if (r.p != p or r.q != q) {
789
- print("expected {d}/{d}, found {}\n", .{ p, q, r });
783
+ print("expected {d}/{d}, found {f}\n", .{ p, q, r });
790
784
  return error.TestExpectedEqual;
791
785
  }
792
786
  }