@pkmn/engine 0.1.0-dev.a0447192 → 0.1.0-dev.a19bc07d

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 +29 -18
  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 +55 -12
  26. package/build/tools/debug.js.map +1 -1
  27. package/build/tools/display/debug.jsx +19 -94
  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 +20 -9
  32. package/build/tools/display/ui.jsx.map +1 -1
  33. package/build/tools/display/util.js +140 -1
  34. package/build/tools/display/util.js.map +1 -1
  35. package/build.zig +351 -246
  36. package/package.json +34 -130
  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 -60
  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 +275 -284
  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 +159 -163
  51. package/src/lib/gen1/chance.zig +181 -105
  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 +34 -32
  57. package/src/lib/gen1/mechanics.zig +103 -78
  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 +0 -5
@@ -1,32 +1,24 @@
1
- const std = @import("std");
2
1
  const builtin = @import("builtin");
3
-
4
2
  const common = @import("../common/data.zig");
3
+ const data = @import("data.zig");
5
4
  const options = @import("../common/options.zig");
6
5
  const protocol = @import("../common/protocol.zig");
7
6
  const rng = @import("../common/rng.zig");
7
+ const std = @import("std");
8
8
  const util = @import("../common/util.zig");
9
9
 
10
- const data = @import("data.zig");
11
-
12
10
  const assert = std.debug.assert;
13
-
14
11
  const Choice = common.Choice;
15
-
16
- const showdown = options.showdown;
17
-
18
- const PSRNG = rng.PSRNG;
19
-
20
12
  const DVs = data.DVs;
21
13
  const Move = data.Move;
22
14
  const MoveSlot = data.MoveSlot;
15
+ const PSRNG = rng.PSRNG;
16
+ const showdown = options.showdown;
23
17
  const Species = data.Species;
24
18
  const Stats = data.Stats;
25
19
 
26
20
  const endian = builtin.cpu.arch.endian();
27
21
 
28
- const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
29
-
30
22
  /// Configuration options controlling the domain of valid Pokémon that can be generated by the
31
23
  /// random generator helpers.
32
24
  pub const Options = struct {
@@ -169,15 +161,20 @@ pub const Pokemon = struct {
169
161
  var pokemon = data.Pokemon{};
170
162
  pokemon.species = p.species;
171
163
  const species = Species.get(p.species);
172
- inline for (@field(@typeInfo(@TypeOf(pokemon.stats)), @tagName(Struct)).fields) |field| {
173
- const hp = comptime std.mem.eql(u8, field.name, "hp");
164
+ inline for (if (@hasField(@TypeOf(@typeInfo(@TypeOf(pokemon.stats)).@"struct"), "fields"))
165
+ @typeInfo(@TypeOf(pokemon.stats)).@"struct".fields
166
+ else
167
+ @typeInfo(@TypeOf(pokemon.stats)).@"struct".field_names) |field|
168
+ {
169
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
170
+ const hp = comptime std.mem.eql(u8, field_name, "hp");
174
171
  const spc =
175
- comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
176
- @field(pokemon.stats, field.name) = Stats(u16).calc(
177
- field.name,
178
- @field(species.stats, field.name),
179
- if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field.name),
180
- @field(p.stats, field.name),
172
+ comptime std.mem.eql(u8, field_name, "spa") or std.mem.eql(u8, field_name, "spd");
173
+ @field(pokemon.stats, field_name) = Stats(u16).calc(
174
+ field_name,
175
+ @field(species.stats, field_name),
176
+ if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field_name),
177
+ @field(p.stats, field_name),
181
178
  p.level,
182
179
  );
183
180
  }
@@ -205,20 +202,25 @@ pub const Pokemon = struct {
205
202
  const lvl = if (rand.chance(u8, 1, 20)) rand.range(u8, 1, 99 + 1) else 100;
206
203
  var stats: Stats(u16) = .{};
207
204
  const dvs = DVs.random(rand);
208
- inline for (@field(@typeInfo(@TypeOf(stats)), @tagName(Struct)).fields) |field| {
209
- @field(stats, field.name) = Stats(u16).calc(
210
- field.name,
211
- @field(species.stats, field.name),
212
- if (comptime std.mem.eql(u8, field.name, "hp"))
205
+ inline for (if (@hasField(@TypeOf(@typeInfo(@TypeOf(stats)).@"struct"), "fields"))
206
+ @typeInfo(@TypeOf(stats)).@"struct".fields
207
+ else
208
+ @typeInfo(@TypeOf(stats)).@"struct".field_names) |field|
209
+ {
210
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
211
+ @field(stats, field_name) = Stats(u16).calc(
212
+ field_name,
213
+ @field(species.stats, field_name),
214
+ if (comptime std.mem.eql(u8, field_name, "hp"))
213
215
  dvs.hp()
214
216
  else
215
- @field(dvs, field.name),
217
+ @field(dvs, field_name),
216
218
  if (rand.chance(u8, 1, 20)) rand.range(u16, 0, EXP + 1) else EXP,
217
219
  lvl,
218
220
  );
219
221
  }
220
222
 
221
- var ms = [_]MoveSlot{.{}} ** 4;
223
+ var ms: [4]MoveSlot = @splat(.{});
222
224
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
223
225
  for (0..n) |i| {
224
226
  var m: Move = .None;
@@ -298,23 +300,23 @@ else
298
300
  pub fn diff(
299
301
  a: *const data.Battle(data.PRNG),
300
302
  b: *const data.Battle(data.PRNG),
301
- w: protocol.ByteStream.Writer,
303
+ w: *protocol.Writer,
302
304
  ) !usize {
303
- const pos = w.stream.pos;
305
+ const pos = w.pos;
304
306
  const size = @sizeOf(data.Battle(data.PRNG)) - @sizeOf(data.PRNG);
305
307
  assert(size % 2 == 0);
306
308
  assert(size / 2 < 255);
307
309
 
308
310
  var i: usize = 0;
309
311
  while (i < size) : (i += 2) {
310
- const val = util.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
311
- if (val != util.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
312
+ const val = std.mem.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
313
+ if (val != std.mem.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
312
314
  try w.writeByte(@intCast(i / 2));
313
315
  try w.writeInt(u16, val, endian);
314
316
  }
315
317
  }
316
318
 
317
- return w.stream.pos - pos;
319
+ return w.pos - pos;
318
320
  }
319
321
 
320
322
  /// TODO
@@ -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;
@@ -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);
@@ -436,7 +432,7 @@ fn executeMove(
436
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
437
433
  // every Pokémon must have at least one move
438
434
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
439
- return Result.Error;
435
+ return .Error;
440
436
  } else {
441
437
  auto = true;
442
438
  }
@@ -454,7 +450,26 @@ fn executeMove(
454
450
  .skip_can => skip_can = true,
455
451
  .skip_pp => skip_pp = true,
456
452
  .ok => {},
457
- .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;
458
473
  }
459
474
 
460
475
  const can = skip_can or
@@ -554,7 +569,7 @@ 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
 
@@ -603,17 +618,10 @@ fn beforeMove(
603
618
  return .err;
604
619
  }
605
620
  }
606
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
607
- // on the targeting behavior of the confused Pokémon's selected move which results
608
- // in the wrong behavior with respect to the Substitute + Confusion glitch
609
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
610
- player
611
- else
612
- player.foe();
613
621
 
614
622
  const uncapped = battle.last_damage;
615
623
  // Skipping adjustDamage / randomizeDamage / checkHit
616
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
617
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
618
626
  if (showdown) battle.last_damage = uncapped;
619
627
 
@@ -773,9 +781,14 @@ fn canMove(
773
781
 
774
782
  if (move.effect == .Thrashing) {
775
783
  Effects.thrashing(battle, player, options);
776
- } else if (!showdown and move.effect == .Binding) {
777
- // Pokémon Showdown handles this after hit/miss checks and damage calculation
778
- 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
+ }
779
792
  }
780
793
 
781
794
  return true;
@@ -865,7 +878,7 @@ fn doMove(
865
878
  var miss = showdown and miss: {
866
879
  immune = move.target != .Self and !status and !counter and
867
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
868
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
869
882
  if (immune and move.effect != .Binding) break :miss true;
870
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
871
884
  battle.last_damage = 0;
@@ -903,7 +916,7 @@ fn doMove(
903
916
  if (showdown) break :damage; // skip adjustDamage / randomizeDamage
904
917
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
905
918
  if (!showdown) try options.chance.commit(player, .err);
906
- return @as(?Result, Result.Error);
919
+ return .Error;
907
920
  }
908
921
  if (battle.last_damage == 0) {
909
922
  immune = true;
@@ -1006,7 +1019,7 @@ fn doMove(
1006
1019
  const sub = showdown and foe.active.volatiles.Substitute;
1007
1020
 
1008
1021
  var nullified = false;
1009
- var hit: u4 = 0;
1022
+ var hit: usize = 0;
1010
1023
  while (hit < hits) {
1011
1024
  if (hit == 0) {
1012
1025
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1034,7 +1047,7 @@ fn doMove(
1034
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1035
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1036
1049
  }
1037
- try log.hitcount(.{ battle.active(player.foe()), hit });
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1038
1051
  } else if (showdown and move.effect == .Binding) {
1039
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1040
1053
  try Effects.binding(battle, player, rewrap, options);
@@ -1129,7 +1142,7 @@ fn calcDamage(
1129
1142
  if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
1130
1143
  atk = @max((atk / 4) & 255, 1);
1131
1144
  // GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
1132
- def = @max((def / 4) & 255, if (showdown) 1 else 0);
1145
+ def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
1133
1146
  }
1134
1147
 
1135
1148
  const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
@@ -1150,7 +1163,7 @@ fn calcDamage(
1150
1163
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1151
1164
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1152
1165
  // that results in counter intuitive probabilities in the common case
1153
- if (pkmn.options.chance and !showdown) {
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1154
1167
  atk = attack(side, !crit, special);
1155
1168
  def = defense(target, !crit, special, false);
1156
1169
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1235,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1235
1248
  .Psywave => power: {
1236
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1237
1250
  // GLITCH: Psywave infinite glitch loop
1238
- if (!showdown and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return .Error;
1239
1252
  break :power try Rolls.psywave(battle, player, max, options);
1240
1253
  },
1241
1254
  else => unreachable,
1242
1255
  };
1243
1256
 
1244
- 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;
1245
1258
 
1246
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1247
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1261,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1261
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1262
1275
  const foe_last_selected_move =
1263
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1264
- .Splash
1265
- else
1266
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1267
1280
 
1268
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1269
1282
  const selected = foe_last_selected_move.bp > 0 and
1270
1283
  foe.last_selected_move != .Counter and
1271
1284
  (foe_last_selected_move.type == .Normal or
1272
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1273
1286
 
1274
1287
  if (!used and !selected) {
1275
1288
  try options.log.fail(.{ battle.active(player), .None });
@@ -1278,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1278
1291
 
1279
1292
  if (!used or !selected) {
1280
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1281
- if (!showdown) return Result.Error;
1294
+ if (!pkmn.options.mod) return .Error;
1282
1295
  try options.log.fail(.{ battle.active(player), .None });
1283
1296
  return null;
1284
1297
  }
1285
1298
 
1286
- 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;
1287
1304
 
1288
1305
  // Pokémon Showdown calls moveHit before Counter
1289
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1429,7 +1446,16 @@ fn moveHit(
1429
1446
  break :miss true;
1430
1447
  }
1431
1448
  if (!showdown) {
1432
- 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
+ }
1433
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1434
1460
  }
1435
1461
 
@@ -1502,13 +1528,13 @@ fn checkFaint(
1502
1528
 
1503
1529
  if (player_out and foe_out) {
1504
1530
  try options.log.tie(.{});
1505
- return Result.Tie;
1531
+ return .Tie;
1506
1532
  } else if (player_out) {
1507
1533
  try options.log.win(.{player.foe()});
1508
- return if (player == .P1) Result.Lose else Result.Win;
1534
+ return if (player == .P1) .Lose else .Win;
1509
1535
  } else if (foe_out) {
1510
1536
  try options.log.win(.{player});
1511
- return if (player == .P1) Result.Win else Result.Lose;
1537
+ return if (player == .P1) .Win else .Lose;
1512
1538
  }
1513
1539
 
1514
1540
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1525,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1525
1551
  assert(!foe_volatiles.MultiHit);
1526
1552
  if (foe_volatiles.Bide) {
1527
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1528
- foe_volatiles.state = if (showdown) 0 else foe_volatiles.state & 255;
1529
- 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;
1530
1556
  }
1531
1557
 
1532
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1616,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1616
1642
 
1617
1643
  battle.turn += 1;
1618
1644
 
1619
- if (showdown and pkmn.options.ebc and checkEBC(battle)) {
1645
+ if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1620
1646
  try options.log.tie(.{});
1621
- return Result.Tie;
1647
+ return .Tie;
1622
1648
  }
1623
1649
 
1624
- if (showdown and battle.turn >= 1000) {
1650
+ if (pkmn.options.mod and battle.turn >= 1000) {
1625
1651
  try options.log.tie(.{});
1626
- return Result.Tie;
1652
+ return .Tie;
1627
1653
  } else if (battle.turn >= 65535) {
1628
- return Result.Error;
1654
+ return .Error;
1629
1655
  }
1630
1656
 
1631
1657
  try options.log.turn(.{battle.turn});
1632
1658
 
1633
- return Result.Default;
1659
+ return .Default;
1634
1660
  }
1635
1661
 
1636
1662
  fn checkEBC(battle: anytype) bool {
@@ -1916,13 +1942,12 @@ pub const Effects = struct {
1916
1942
 
1917
1943
  // Technically this is still considered simply a "miss" on the cartridge,
1918
1944
  // but diverging from Pokémon Showdown here would mostly just be pedantic
1919
- if (n == 0) {
1945
+ if (n == 0 or (pkmn.options.mod and err)) {
1920
1946
  try options.log.fail(.{ foe_ident, .None });
1921
1947
  return null;
1922
1948
  } else if (err) {
1923
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1924
- assert(!showdown);
1925
- return Result.Error;
1950
+ return .Error;
1926
1951
  }
1927
1952
 
1928
1953
  volatiles.disable_move =
@@ -1999,7 +2024,7 @@ pub const Effects = struct {
1999
2024
  if (foe.active.types.includes(move.type)) return;
2000
2025
  if (!try Rolls.secondaryChance(battle, player, true, options)) return;
2001
2026
  // Freeze Clause Mod
2002
- 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;
2003
2028
 
2004
2029
  foe_stored.status = Status.init(.FRZ);
2005
2030
  // GLITCH: Hyper Beam recharging status is not cleared
@@ -2340,7 +2365,7 @@ pub const Effects = struct {
2340
2365
  }
2341
2366
 
2342
2367
  // Sleep Clause Mod
2343
- if (showdown) {
2368
+ if (pkmn.options.mod) {
2344
2369
  for (foe.pokemon) |p| {
2345
2370
  if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
2346
2371
  }
@@ -2446,7 +2471,7 @@ pub const Effects = struct {
2446
2471
  if (side.active.volatiles.Binding) return;
2447
2472
  side.active.volatiles.Binding = true;
2448
2473
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2449
- // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2474
+ // (Pokémon Showdown clears Recharging in onTryMove instead)
2450
2475
  if (!showdown) {
2451
2476
  foe.active.volatiles.Recharging = false;
2452
2477
  } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
@@ -3120,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3120
3145
  },
3121
3146
  .Switch => {
3122
3147
  const side = battle.side(player);
3123
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3124
3149
  while (slot <= 6) : (slot += 1) {
3125
3150
  const id = side.order[slot - 1];
3126
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3127
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3128
3153
  n += 1;
3129
3154
  }
3130
3155
  if (n == 0) {
@@ -3155,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3155
3180
  return n;
3156
3181
  }
3157
3182
 
3158
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3159
3184
  while (slot <= 6) : (slot += 1) {
3160
3185
  const id = side.order[slot - 1];
3161
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3162
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3163
3188
  n += 1;
3164
3189
  }
3165
3190
 
@@ -3192,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3192
3217
  const struggle =
3193
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3194
3219
  const s = if (struggle) 0 else slot;
3195
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3196
3221
  n += 1;
3197
3222
  return n;
3198
3223
  }
@@ -3206,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3206
3231
  if (m.id == .None) break;
3207
3232
  if (m.pp == 0) continue;
3208
3233
  if (active.volatiles.disable_move == slot) continue;
3209
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3210
3235
  n += 1;
3211
3236
  }
3212
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
3213
3238
  if (n == before) {
3214
3239
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
3215
- if (!showdown) {
3240
+ if (!pkmn.options.mod) {
3216
3241
  while (slot <= 4) : (slot += 1) {
3217
3242
  if (active.moves[slot - 1].pp != 0) return n;
3218
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
  }