@pkmn/engine 0.1.0-dev.683fba11 → 0.1.0-dev.6b383c80

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 (61) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -16
  3. package/build/pkg/addon/browser.js +6 -3
  4. package/build/pkg/addon/browser.js.map +1 -1
  5. package/build/pkg/addon/node.js +23 -10
  6. package/build/pkg/addon/node.js.map +1 -1
  7. package/build/pkg/addon/wasm.js +23 -14
  8. package/build/pkg/addon/wasm.js.map +1 -1
  9. package/build/pkg/addon.d.ts +5 -5
  10. package/build/pkg/addon.js.map +1 -1
  11. package/build/pkg/data.js.map +1 -1
  12. package/build/pkg/gen1.d.ts +2 -1
  13. package/build/pkg/gen1.js +22 -9
  14. package/build/pkg/gen1.js.map +1 -1
  15. package/build/pkg/index.d.ts +2 -0
  16. package/build/pkg/index.js +17 -7
  17. package/build/pkg/index.js.map +1 -1
  18. package/build/pkg/protocol.d.ts +11 -2
  19. package/build/pkg/protocol.js.map +1 -1
  20. package/build/tools/debug.js +54 -11
  21. package/build/tools/debug.js.map +1 -1
  22. package/build/tools/display/debug.jsx +19 -94
  23. package/build/tools/display/debug.jsx.map +1 -1
  24. package/build/tools/display/index.js +18 -7
  25. package/build/tools/display/index.js.map +1 -1
  26. package/build/tools/display/ui.jsx +20 -9
  27. package/build/tools/display/ui.jsx.map +1 -1
  28. package/build/tools/display/util.js +140 -1
  29. package/build/tools/display/util.js.map +1 -1
  30. package/build.zig +255 -313
  31. package/package.json +29 -131
  32. package/src/bin/install-pkmn-engine +24 -22
  33. package/src/lib/bindings/c.zig +201 -0
  34. package/src/lib/bindings/node.zig +117 -0
  35. package/src/lib/bindings/wasm.zig +51 -0
  36. package/src/lib/common/array.zig +9 -16
  37. package/src/lib/common/data.zig +8 -8
  38. package/src/lib/common/debug.zig +46 -61
  39. package/src/lib/common/js.zig +3 -9
  40. package/src/lib/common/optional.zig +20 -25
  41. package/src/lib/common/protocol.zig +224 -189
  42. package/src/lib/common/rational.zig +275 -284
  43. package/src/lib/common/rng.zig +4 -4
  44. package/src/lib/common/util.zig +34 -31
  45. package/src/lib/gen1/calc.zig +141 -119
  46. package/src/lib/gen1/chance.zig +162 -101
  47. package/src/lib/gen1/data/types.zig +2 -2
  48. package/src/lib/gen1/data.zig +27 -24
  49. package/src/lib/gen1/helpers.zig +36 -26
  50. package/src/lib/gen1/mechanics.zig +45 -49
  51. package/src/lib/gen2/calc.zig +3 -6
  52. package/src/lib/gen2/chance.zig +55 -43
  53. package/src/lib/gen2/data/types.zig +6 -3
  54. package/src/lib/gen2/data.zig +20 -16
  55. package/src/lib/gen2/helpers.zig +27 -19
  56. package/src/lib/gen2/mechanics.zig +17 -17
  57. package/src/lib/napi.h +2 -0
  58. package/src/lib/node.zig +3 -118
  59. package/src/lib/pkmn.zig +94 -19
  60. package/src/lib/wasm.zig +9 -44
  61. package/src/tools/display/dom.ts +43 -31
@@ -21,15 +21,18 @@ pub const MAX_CHOICES: usize = 9; // move 1..4, switch 2..6
21
21
  /// The maximum number of bytes possibly logged by a single Generation I update.
22
22
  pub const MAX_LOGS: usize = 180;
23
23
 
24
+ const ReleaseSmall: std.builtin.OptimizeMode =
25
+ if (@hasDecl(std.builtin.OptimizeMode, "ReleaseSmall")) .small else .ReleaseSmall;
26
+
24
27
  /// The optimal size in bytes required to hold all Generation I choice options.
25
28
  /// At least as large as MAX_CHOICES.
26
- pub const CHOICES_SIZE = if (builtin.mode == .ReleaseSmall)
29
+ pub const CHOICES_SIZE = if (builtin.mode == ReleaseSmall)
27
30
  MAX_CHOICES
28
31
  else
29
32
  std.math.ceilPowerOfTwo(usize, MAX_CHOICES) catch unreachable;
30
33
  /// The optimal size in bytes required to hold the largest amount of log data possible from a
31
34
  /// single Generation I update. At least as large as MAX_LOGS.
32
- pub const LOGS_SIZE = if (builtin.mode == .ReleaseSmall)
35
+ pub const LOGS_SIZE = if (builtin.mode == ReleaseSmall)
33
36
  MAX_LOGS
34
37
  else
35
38
  std.math.ceilPowerOfTwo(usize, MAX_LOGS) catch unreachable;
@@ -62,7 +65,7 @@ pub fn Battle(comptime RNG: anytype) type {
62
65
  /// The last damage dealt by either side.
63
66
  last_damage: u16 = 0,
64
67
  /// Details about the last move selected/used by each player required to detect desyncs.
65
- last_moves: [2]MoveDetails = [_]MoveDetails{.{}} ** 2,
68
+ last_moves: [2]MoveDetails = @splat(.{}),
66
69
  /// The pseudo random number generator.
67
70
  rng: RNG,
68
71
 
@@ -122,12 +125,12 @@ test Battle {
122
125
  /// Representation of one side of a Generation I Pokémon battle.
123
126
  pub const Side = extern struct {
124
127
  /// The player's party in its original order
125
- pokemon: [6]Pokemon = [_]Pokemon{.{}} ** 6,
128
+ pokemon: [6]Pokemon = @splat(.{}),
126
129
  /// The active Pokémon for the side, zero initialized if the battle has yet to start.
127
130
  /// Note that fainted Pokémon are still consider "active" until their replacement switches in.
128
131
  active: ActivePokemon = .{},
129
132
  /// One-based slot indexes reflecting the current order of the player's party.
130
- order: [6]u8 = [_]u8{0} ** 6,
133
+ order: [6]u8 = @splat(0),
131
134
  /// The last move the player selected.
132
135
  last_selected_move: Move = .None,
133
136
  /// The last move the player used.
@@ -166,7 +169,7 @@ pub const ActivePokemon = extern struct {
166
169
  /// The active Pokémon's volatile statuses and associated data.
167
170
  volatiles: Volatiles = .{},
168
171
  /// The active Pokémon's current move slots.
169
- moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
172
+ moves: [4]MoveSlot = @splat(.{}),
170
173
 
171
174
  comptime {
172
175
  assert(@sizeOf(ActivePokemon) == 32);
@@ -186,7 +189,7 @@ pub const Pokemon = extern struct {
186
189
  /// The Pokémon's unmodified stats.
187
190
  stats: Stats(u16) = .{},
188
191
  /// The Pokémon's original stored move slots.
189
- moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
192
+ moves: [4]MoveSlot = @splat(.{}),
190
193
  /// The Pokémon's current HP.
191
194
  hp: u16 = 0,
192
195
  /// The Pokémon's current status.
@@ -313,12 +316,12 @@ test Status {
313
316
  try expect(Status.is(Status.slp(5), .SLP));
314
317
  try expect(!Status.is(Status.slp(5), .EXT));
315
318
  try expect(!Status.is(Status.slp(7), .PSN));
316
- try expectEqual(@as(u3, 5), Status.duration(Status.slp(5)));
319
+ try expectEqual(5, Status.duration(Status.slp(5)));
317
320
  try expect(Status.is(Status.slf(2), .SLP));
318
321
  try expect(Status.is(Status.slf(2), .EXT));
319
322
  try expect(Status.is(Status.TOX, .PSN));
320
323
  try expect(Status.is(Status.TOX, .EXT));
321
- try expectEqual(@as(u3, 1), Status.duration(Status.slp(1)));
324
+ try expectEqual(1, Status.duration(Status.slp(1)));
322
325
  }
323
326
 
324
327
  /// Bitfield representation of volatile statuses and associated data in Generation I.
@@ -402,13 +405,13 @@ test Volatiles {
402
405
  try expect(!volatiles.Transform);
403
406
  try expect(!volatiles.MultiHit);
404
407
 
405
- try expectEqual(@as(u16, 235), volatiles.state);
406
- try expectEqual(@as(u8, 42), volatiles.substitute);
407
- try expectEqual(@as(u4, 2), volatiles.disable_move);
408
- try expectEqual(@as(u4, 4), volatiles.disable_duration);
409
- try expectEqual(@as(u3, 2), volatiles.confusion);
410
- try expectEqual(@as(u5, 4), volatiles.toxic);
411
- try expectEqual(@as(u3, 3), volatiles.attacks);
408
+ try expectEqual(235, volatiles.state);
409
+ try expectEqual(42, volatiles.substitute);
410
+ try expectEqual(2, volatiles.disable_move);
411
+ try expectEqual(4, volatiles.disable_duration);
412
+ try expectEqual(2, volatiles.confusion);
413
+ try expectEqual(4, volatiles.toxic);
414
+ try expectEqual(3, volatiles.attacks);
412
415
  }
413
416
 
414
417
  /// Representation of a Pokémon's stats in Generation I.
@@ -439,9 +442,9 @@ test Stats {
439
442
  try expectEqual(0, stats.def);
440
443
 
441
444
  var base = Species.get(.Gyarados).stats;
442
- try expectEqual(@as(u16, 127), Stats(u16).calc("hp", base.hp, 0, 5120, 38));
445
+ try expectEqual(127, Stats(u16).calc("hp", base.hp, 0, 5120, 38));
443
446
  base = Species.get(.Pidgeot).stats;
444
- try expectEqual(@as(u16, 279), Stats(u16).calc("spe", base.spe, 15, 63001, 100));
447
+ try expectEqual(279, Stats(u16).calc("spe", base.spe, 15, 63001, 100));
445
448
  }
446
449
 
447
450
  /// Representation of a Pokémon's boosts in Generation I.
@@ -534,7 +537,7 @@ pub const Species = species.Species;
534
537
 
535
538
  test Species {
536
539
  try expectEqual(2, @intFromEnum(Species.Ivysaur));
537
- try expectEqual(@as(u8, 100), Species.get(.Mew).stats.def);
540
+ try expectEqual(100, Species.get(.Mew).stats.def);
538
541
  }
539
542
 
540
543
  /// Representation of a Generation I type in Pokémon.
@@ -573,8 +576,8 @@ test Types {
573
576
  try expectEqual(Effectiveness.Resisted, Type.effectiveness(.Fire, .Water));
574
577
  try expectEqual(Effectiveness.Neutral, Type.effectiveness(.Normal, .Grass));
575
578
 
576
- try expectEqual(@as(u8, 3), Type.precedence(.Water, .Water));
577
- try expectEqual(@as(u8, 26), Type.precedence(.Bug, .Ghost));
579
+ try expectEqual(3, Type.precedence(.Water, .Water));
580
+ try expectEqual(26, Type.precedence(.Bug, .Ghost));
578
581
  try expect(Type.precedence(.Poison, .Bug) > Type.precedence(.Poison, .Poison));
579
582
 
580
583
  const t: Types = .{ .type1 = .Rock, .type2 = .Ground };
@@ -614,18 +617,18 @@ pub const DVs = struct {
614
617
 
615
618
  test DVs {
616
619
  var dvs: DVs = .{ .spc = 15, .spe = 15 };
617
- try expectEqual(@as(u4, 15), dvs.hp());
620
+ try expectEqual(15, dvs.hp());
618
621
  dvs = .{
619
622
  .atk = 5,
620
623
  .def = 15,
621
624
  .spe = 13,
622
625
  .spc = 13,
623
626
  };
624
- try expectEqual(@as(u4, 15), dvs.hp());
627
+ try expectEqual(15, dvs.hp());
625
628
  dvs = .{
626
629
  .def = 3,
627
630
  .spe = 10,
628
631
  .spc = 11,
629
632
  };
630
- try expectEqual(@as(u4, 13), dvs.hp());
633
+ try expectEqual(13, dvs.hp());
631
634
  }
@@ -19,8 +19,6 @@ const Stats = data.Stats;
19
19
 
20
20
  const endian = builtin.cpu.arch.endian();
21
21
 
22
- const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
23
-
24
22
  /// Configuration options controlling the domain of valid Pokémon that can be generated by the
25
23
  /// random generator helpers.
26
24
  pub const Options = struct {
@@ -163,15 +161,20 @@ pub const Pokemon = struct {
163
161
  var pokemon = data.Pokemon{};
164
162
  pokemon.species = p.species;
165
163
  const species = Species.get(p.species);
166
- inline for (@field(@typeInfo(@TypeOf(pokemon.stats)), @tagName(Struct)).fields) |field| {
167
- 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");
168
171
  const spc =
169
- comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
170
- @field(pokemon.stats, field.name) = Stats(u16).calc(
171
- field.name,
172
- @field(species.stats, field.name),
173
- if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field.name),
174
- @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),
175
178
  p.level,
176
179
  );
177
180
  }
@@ -199,20 +202,25 @@ pub const Pokemon = struct {
199
202
  const lvl = if (rand.chance(u8, 1, 20)) rand.range(u8, 1, 99 + 1) else 100;
200
203
  var stats: Stats(u16) = .{};
201
204
  const dvs = DVs.random(rand);
202
- inline for (@field(@typeInfo(@TypeOf(stats)), @tagName(Struct)).fields) |field| {
203
- @field(stats, field.name) = Stats(u16).calc(
204
- field.name,
205
- @field(species.stats, field.name),
206
- 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"))
207
215
  dvs.hp()
208
216
  else
209
- @field(dvs, field.name),
217
+ @field(dvs, field_name),
210
218
  if (rand.chance(u8, 1, 20)) rand.range(u16, 0, EXP + 1) else EXP,
211
219
  lvl,
212
220
  );
213
221
  }
214
222
 
215
- var ms = [_]MoveSlot{.{}} ** 4;
223
+ var ms: [4]MoveSlot = @splat(.{});
216
224
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
217
225
  for (0..n) |i| {
218
226
  var m: Move = .None;
@@ -281,9 +289,12 @@ pub fn swtch(slot: u4) Choice {
281
289
  /// Generation I battles.
282
290
  pub const MAX_DIFFS: usize = 120;
283
291
 
292
+ const ReleaseSmall: std.builtin.OptimizeMode =
293
+ if (@hasDecl(std.builtin.OptimizeMode, "ReleaseSmall")) .small else .ReleaseSmall;
294
+
284
295
  /// The optimal size in bytes required to hold the diff between two Generation I battles.
285
296
  /// At least as large as MAX_DIFFS.
286
- pub const DIFFS_SIZE = if (builtin.mode == .ReleaseSmall)
297
+ pub const DIFFS_SIZE = if (builtin.mode == ReleaseSmall)
287
298
  MAX_DIFFS
288
299
  else
289
300
  std.math.ceilPowerOfTwo(usize, MAX_DIFFS) catch unreachable;
@@ -292,23 +303,23 @@ else
292
303
  pub fn diff(
293
304
  a: *const data.Battle(data.PRNG),
294
305
  b: *const data.Battle(data.PRNG),
295
- w: protocol.ByteStream.Writer,
306
+ w: *protocol.Writer,
296
307
  ) !usize {
297
- const pos = w.stream.pos;
308
+ const pos = w.pos;
298
309
  const size = @sizeOf(data.Battle(data.PRNG)) - @sizeOf(data.PRNG);
299
310
  assert(size % 2 == 0);
300
311
  assert(size / 2 < 255);
301
312
 
302
313
  var i: usize = 0;
303
314
  while (i < size) : (i += 2) {
304
- const val = util.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
305
- if (val != util.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
315
+ const val = std.mem.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
316
+ if (val != std.mem.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
306
317
  try w.writeByte(@intCast(i / 2));
307
318
  try w.writeInt(u16, val, endian);
308
319
  }
309
320
  }
310
321
 
311
- return w.stream.pos - pos;
322
+ return w.pos - pos;
312
323
  }
313
324
 
314
325
  /// TODO
@@ -316,8 +327,7 @@ pub fn patch(battle: *data.Battle(data.PRNG), buf: []u8) void {
316
327
  var i: usize = 0;
317
328
  while (i < buf.len) : (i += 3) {
318
329
  const off = @as(u16, @intCast(buf[i])) * 2;
319
- const val = std.mem.readInt(u16, @as(*const [2]u8, @ptrCast(buf[i + 1 .. i + 3])), endian);
320
- const bytes: *[2]u8 = @ptrCast(std.mem.asBytes(battle)[off .. off + 2]);
321
- std.mem.writeInt(u16, bytes, val, endian);
330
+ const val = std.mem.readInt(u16, @ptrCast(buf[i + 1 .. i + 3]), endian);
331
+ std.mem.writeInt(u16, @ptrCast(std.mem.asBytes(battle)[off .. off + 2]), val, endian);
322
332
  }
323
333
  }
@@ -1,6 +1,5 @@
1
1
  const DEBUG = @import("../common/debug.zig").print;
2
2
 
3
- const builtin = @import("builtin");
4
3
  const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
5
  const data = @import("data.zig");
@@ -84,11 +83,11 @@ fn start(battle: anytype, options: anytype) !Result {
84
83
 
85
84
  const p1_slot = findFirstAlive(p1);
86
85
  assert(!showdown or p1_slot == 1);
87
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
86
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
88
87
 
89
88
  const p2_slot = findFirstAlive(p2);
90
89
  assert(!showdown or p2_slot == 1);
91
- if (p2_slot == 0) return Result.Win;
90
+ if (p2_slot == 0) return .Win;
92
91
 
93
92
  try switchIn(battle, .P1, p1_slot, true, options);
94
93
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -167,7 +166,7 @@ fn selectMove(
167
166
  const last = side.active.move(battle.lastMove(player).index);
168
167
  if (last.id == .Metronome) side.last_selected_move = last.id;
169
168
  if (last.id == .MirrorMove) {
170
- if (!pkmn.options.mod) return Result.Error;
169
+ if (!pkmn.options.mod) return .Error;
171
170
  side.last_selected_move = last.id;
172
171
  }
173
172
  }
@@ -432,7 +431,7 @@ fn executeMove(
432
431
  // technically be from this Pokémon because it must be exactly 1 to not desync and
433
432
  // every Pokémon must have at least one move
434
433
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
435
- return Result.Error;
434
+ return .Error;
436
435
  } else {
437
436
  auto = true;
438
437
  }
@@ -450,14 +449,14 @@ fn executeMove(
450
449
  .skip_can => skip_can = true,
451
450
  .skip_pp => skip_pp = true,
452
451
  .ok => {},
453
- .err => return @as(?Result, Result.Error),
452
+ .err => return .Error,
454
453
  }
455
454
 
456
455
  // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
457
456
  // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
458
457
  // from the Hyper Beam slot if present or skips PP deduction entirely...
459
458
  if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
460
- side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
459
+ side.last_selected_move != side.active.move(mslot).id)
461
460
  {
462
461
  assert(mslot == 1);
463
462
  const has_beam = has_beam: {
@@ -618,17 +617,10 @@ fn beforeMove(
618
617
  return .err;
619
618
  }
620
619
  }
621
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
622
- // on the targeting behavior of the confused Pokémon's selected move which results
623
- // in the wrong behavior with respect to the Substitute + Confusion glitch
624
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
625
- player
626
- else
627
- player.foe();
628
620
 
629
621
  const uncapped = battle.last_damage;
630
622
  // Skipping adjustDamage / randomizeDamage / checkHit
631
- _ = try applyDamage(battle, player, target, .Confusion, options);
623
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
632
624
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
633
625
  if (showdown) battle.last_damage = uncapped;
634
626
 
@@ -885,7 +877,7 @@ fn doMove(
885
877
  var miss = showdown and miss: {
886
878
  immune = move.target != .Self and !status and !counter and
887
879
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
888
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
880
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
889
881
  if (immune and move.effect != .Binding) break :miss true;
890
882
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
891
883
  battle.last_damage = 0;
@@ -923,7 +915,7 @@ fn doMove(
923
915
  if (showdown) break :damage; // skip adjustDamage / randomizeDamage
924
916
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
925
917
  if (!showdown) try options.chance.commit(player, .err);
926
- return @as(?Result, Result.Error);
918
+ return .Error;
927
919
  }
928
920
  if (battle.last_damage == 0) {
929
921
  immune = true;
@@ -1026,7 +1018,7 @@ fn doMove(
1026
1018
  const sub = showdown and foe.active.volatiles.Substitute;
1027
1019
 
1028
1020
  var nullified = false;
1029
- var hit: u4 = 0;
1021
+ var hit: usize = 0;
1030
1022
  while (hit < hits) {
1031
1023
  if (hit == 0) {
1032
1024
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1054,7 +1046,7 @@ fn doMove(
1054
1046
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1055
1047
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1056
1048
  }
1057
- try log.hitcount(.{ battle.active(player.foe()), hit });
1049
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1058
1050
  } else if (showdown and move.effect == .Binding) {
1059
1051
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1060
1052
  try Effects.binding(battle, player, rewrap, options);
@@ -1170,7 +1162,7 @@ fn calcDamage(
1170
1162
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1171
1163
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1172
1164
  // that results in counter intuitive probabilities in the common case
1173
- if (pkmn.options.chance and !pkmn.options.mod) {
1165
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1174
1166
  atk = attack(side, !crit, special);
1175
1167
  def = defense(target, !crit, special, false);
1176
1168
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1255,13 +1247,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1255
1247
  .Psywave => power: {
1256
1248
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1257
1249
  // GLITCH: Psywave infinite glitch loop
1258
- if (!pkmn.options.mod and max <= 1) return Result.Error;
1250
+ if (!pkmn.options.mod and max <= 1) return .Error;
1259
1251
  break :power try Rolls.psywave(battle, player, max, options);
1260
1252
  },
1261
1253
  else => unreachable,
1262
1254
  };
1263
1255
 
1264
- if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1256
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
1265
1257
 
1266
1258
  const sub = showdown and foe.active.volatiles.Substitute;
1267
1259
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1281,15 +1273,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1281
1273
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1282
1274
  const foe_last_selected_move =
1283
1275
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1284
- .Splash
1285
- else
1286
- foe.last_selected_move);
1276
+ .Splash
1277
+ else
1278
+ foe.last_selected_move);
1287
1279
 
1288
1280
  const used = battle.lastMove(player.foe()).counterable != 0;
1289
1281
  const selected = foe_last_selected_move.bp > 0 and
1290
1282
  foe.last_selected_move != .Counter and
1291
1283
  (foe_last_selected_move.type == .Normal or
1292
- foe_last_selected_move.type == .Fighting);
1284
+ foe_last_selected_move.type == .Fighting);
1293
1285
 
1294
1286
  if (!used and !selected) {
1295
1287
  try options.log.fail(.{ battle.active(player), .None });
@@ -1298,17 +1290,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1298
1290
 
1299
1291
  if (!used or !selected) {
1300
1292
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1301
- if (!pkmn.options.mod) return Result.Error;
1293
+ if (!pkmn.options.mod) return .Error;
1302
1294
  try options.log.fail(.{ battle.active(player), .None });
1303
1295
  return null;
1304
1296
  }
1305
1297
 
1306
- // NOTE: llvm/llvm-project#58557
1307
- if (comptime builtin.target.isWasm()) {
1308
- battle.last_damage = @max(battle.last_damage *% 2, std.math.maxInt(u16));
1309
- } else {
1310
- battle.last_damage *|= 2;
1311
- }
1298
+ battle.last_damage *|= 2;
1312
1299
 
1313
1300
  // Pokémon Showdown calls moveHit before Counter
1314
1301
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1454,7 +1441,16 @@ fn moveHit(
1454
1441
  break :miss true;
1455
1442
  }
1456
1443
  if (!showdown) {
1457
- if (move.effect == .Swift) return true;
1444
+ if (move.effect == .Swift) {
1445
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1446
+ // Showdown, hence why the block above doesn't include this code), we need the
1447
+ // damage roll to be committed. However, the commit code will not save this roll if
1448
+ // the glitch occurs and the move was not marked as having hit. To work around this
1449
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1450
+ // the hit as zero to ensure it doesn't actually get saved.
1451
+ try options.chance.hit(player, true, 0);
1452
+ return true;
1453
+ }
1458
1454
  if (foe.active.volatiles.Invulnerable) break :miss true;
1459
1455
  }
1460
1456
 
@@ -1527,13 +1523,13 @@ fn checkFaint(
1527
1523
 
1528
1524
  if (player_out and foe_out) {
1529
1525
  try options.log.tie(.{});
1530
- return Result.Tie;
1526
+ return .Tie;
1531
1527
  } else if (player_out) {
1532
1528
  try options.log.win(.{player.foe()});
1533
- return if (player == .P1) Result.Lose else Result.Win;
1529
+ return if (player == .P1) .Lose else .Win;
1534
1530
  } else if (foe_out) {
1535
1531
  try options.log.win(.{player});
1536
- return if (player == .P1) Result.Win else Result.Lose;
1532
+ return if (player == .P1) .Win else .Lose;
1537
1533
  }
1538
1534
 
1539
1535
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1551,7 +1547,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1551
1547
  if (foe_volatiles.Bide) {
1552
1548
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1553
1549
  foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1554
- if (foe_volatiles.state != 0) return Result.Error;
1550
+ if (foe_volatiles.state != 0) return .Error;
1555
1551
  }
1556
1552
 
1557
1553
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1643,19 +1639,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1643
1639
 
1644
1640
  if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1645
1641
  try options.log.tie(.{});
1646
- return Result.Tie;
1642
+ return .Tie;
1647
1643
  }
1648
1644
 
1649
1645
  if (pkmn.options.mod and battle.turn >= 1000) {
1650
1646
  try options.log.tie(.{});
1651
- return Result.Tie;
1647
+ return .Tie;
1652
1648
  } else if (battle.turn >= 65535) {
1653
- return Result.Error;
1649
+ return .Error;
1654
1650
  }
1655
1651
 
1656
1652
  try options.log.turn(.{battle.turn});
1657
1653
 
1658
- return Result.Default;
1654
+ return .Default;
1659
1655
  }
1660
1656
 
1661
1657
  fn checkEBC(battle: anytype) bool {
@@ -1946,7 +1942,7 @@ pub const Effects = struct {
1946
1942
  return null;
1947
1943
  } else if (err) {
1948
1944
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1949
- return Result.Error;
1945
+ return .Error;
1950
1946
  }
1951
1947
 
1952
1948
  volatiles.disable_move =
@@ -3144,11 +3140,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3144
3140
  },
3145
3141
  .Switch => {
3146
3142
  const side = battle.side(player);
3147
- var slot: u4 = 2;
3143
+ var slot: usize = 2;
3148
3144
  while (slot <= 6) : (slot += 1) {
3149
3145
  const id = side.order[slot - 1];
3150
3146
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3151
- out[n] = .{ .type = .Switch, .data = slot };
3147
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3152
3148
  n += 1;
3153
3149
  }
3154
3150
  if (n == 0) {
@@ -3179,11 +3175,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3179
3175
  return n;
3180
3176
  }
3181
3177
 
3182
- var slot: u4 = 2;
3178
+ var slot: usize = 2;
3183
3179
  while (slot <= 6) : (slot += 1) {
3184
3180
  const id = side.order[slot - 1];
3185
3181
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3186
- out[n] = .{ .type = .Switch, .data = slot };
3182
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3187
3183
  n += 1;
3188
3184
  }
3189
3185
 
@@ -3216,7 +3212,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3216
3212
  const struggle =
3217
3213
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3218
3214
  const s = if (struggle) 0 else slot;
3219
- out[n] = .{ .type = .Move, .data = s };
3215
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3220
3216
  n += 1;
3221
3217
  return n;
3222
3218
  }
@@ -3230,7 +3226,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3230
3226
  if (m.id == .None) break;
3231
3227
  if (m.pp == 0) continue;
3232
3228
  if (active.volatiles.disable_move == slot) continue;
3233
- out[n] = .{ .type = .Move, .data = slot };
3229
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3234
3230
  n += 1;
3235
3231
  }
3236
3232
  // Struggle (Pokémon Showdown would use 'move 1' here)
@@ -15,9 +15,6 @@ const isPointerTo = util.isPointerTo;
15
15
  const Player = common.Player;
16
16
  const PointerType = util.PointerType;
17
17
 
18
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
19
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
20
-
21
18
  /// Information relevant to damage calculation that occured during a Generation II battle `update`.
22
19
  pub const Summaries = extern struct {
23
20
  /// Relevant information for Player 1.
@@ -91,8 +88,8 @@ pub const Calc = struct {
91
88
  };
92
89
 
93
90
  return if (switch (@typeInfo(@TypeOf(val))) {
94
- Enum => val != .None,
95
- Int => val != 0,
91
+ .@"enum" => val != .None,
92
+ .int => val != 0,
96
93
  else => unreachable,
97
94
  }) val else null;
98
95
  }
@@ -148,6 +145,6 @@ fn ReturnType(comptime field: Action.Field) type {
148
145
  return ?(switch (field) {
149
146
  .damages => u8,
150
147
  .critical_hits => Optional(bool),
151
- else => std.meta.FieldType(Action, field),
148
+ else => @FieldType(Action, @tagName(field)),
152
149
  });
153
150
  }