@pkmn/engine 0.1.0-dev.645b4750 → 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.
- package/LICENSE +1 -1
- package/README.md +22 -61
- package/build/pkg/addon/browser.js +2 -2
- package/build/pkg/addon/browser.js.map +1 -1
- package/build/pkg/addon/node.js +3 -3
- package/build/pkg/addon/node.js.map +1 -1
- package/build/pkg/addon/wasm.js.map +1 -1
- package/build/pkg/addon.d.ts +4 -4
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/protocol.d.ts +11 -2
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +37 -3
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +1 -87
- package/build/tools/display/debug.jsx.map +1 -1
- package/build/tools/display/ui.jsx +3 -2
- package/build/tools/display/ui.jsx.map +1 -1
- package/build/tools/display/util.js +125 -1
- package/build/tools/display/util.js.map +1 -1
- package/build.zig +224 -350
- package/package.json +22 -22
- package/src/bin/install-pkmn-engine +23 -22
- package/src/lib/bindings/c.zig +28 -82
- package/src/lib/bindings/node.zig +7 -11
- package/src/lib/bindings/wasm.zig +7 -15
- package/src/lib/common/array.zig +9 -16
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +46 -61
- package/src/lib/common/js.zig +3 -9
- package/src/lib/common/optional.zig +20 -25
- package/src/lib/common/protocol.zig +224 -189
- package/src/lib/common/rational.zig +91 -173
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +34 -46
- package/src/lib/gen1/calc.zig +128 -115
- package/src/lib/gen1/chance.zig +162 -101
- package/src/lib/gen1/data/types.zig +2 -2
- package/src/lib/gen1/data.zig +27 -24
- package/src/lib/gen1/helpers.zig +36 -26
- package/src/lib/gen1/mechanics.zig +42 -38
- package/src/lib/gen2/calc.zig +3 -6
- package/src/lib/gen2/chance.zig +55 -43
- package/src/lib/gen2/data/types.zig +6 -3
- package/src/lib/gen2/data.zig +20 -16
- package/src/lib/gen2/helpers.zig +27 -19
- package/src/lib/gen2/mechanics.zig +25 -36
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +1 -5
- package/src/lib/pkmn.zig +91 -17
- package/src/lib/wasm.zig +2 -6
- package/src/tools/display/dom.ts +40 -25
package/src/lib/gen1/data.zig
CHANGED
|
@@ -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 ==
|
|
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 ==
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
406
|
-
try expectEqual(
|
|
407
|
-
try expectEqual(
|
|
408
|
-
try expectEqual(
|
|
409
|
-
try expectEqual(
|
|
410
|
-
try expectEqual(
|
|
411
|
-
try expectEqual(
|
|
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(
|
|
445
|
+
try expectEqual(127, Stats(u16).calc("hp", base.hp, 0, 5120, 38));
|
|
443
446
|
base = Species.get(.Pidgeot).stats;
|
|
444
|
-
try expectEqual(
|
|
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(
|
|
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(
|
|
577
|
-
try expectEqual(
|
|
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(
|
|
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(
|
|
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(
|
|
633
|
+
try expectEqual(13, dvs.hp());
|
|
631
634
|
}
|
package/src/lib/gen1/helpers.zig
CHANGED
|
@@ -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 (@
|
|
167
|
-
|
|
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,
|
|
170
|
-
@field(pokemon.stats,
|
|
171
|
-
|
|
172
|
-
@field(species.stats,
|
|
173
|
-
if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs,
|
|
174
|
-
@field(p.stats,
|
|
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 (@
|
|
203
|
-
@
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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,
|
|
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
|
|
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 ==
|
|
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.
|
|
306
|
+
w: *protocol.Writer,
|
|
296
307
|
) !usize {
|
|
297
|
-
const pos = w.
|
|
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 =
|
|
305
|
-
if (val !=
|
|
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.
|
|
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, @
|
|
320
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
434
|
+
return .Error;
|
|
436
435
|
} else {
|
|
437
436
|
auto = true;
|
|
438
437
|
}
|
|
@@ -450,7 +449,7 @@ fn executeMove(
|
|
|
450
449
|
.skip_can => skip_can = true,
|
|
451
450
|
.skip_pp => skip_pp = true,
|
|
452
451
|
.ok => {},
|
|
453
|
-
.err => return
|
|
452
|
+
.err => return .Error,
|
|
454
453
|
}
|
|
455
454
|
|
|
456
455
|
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
@@ -878,7 +877,7 @@ fn doMove(
|
|
|
878
877
|
var miss = showdown and miss: {
|
|
879
878
|
immune = move.target != .Self and !status and !counter and
|
|
880
879
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
881
|
-
|
|
880
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
882
881
|
if (immune and move.effect != .Binding) break :miss true;
|
|
883
882
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
884
883
|
battle.last_damage = 0;
|
|
@@ -916,7 +915,7 @@ fn doMove(
|
|
|
916
915
|
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
916
|
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
917
|
if (!showdown) try options.chance.commit(player, .err);
|
|
919
|
-
return
|
|
918
|
+
return .Error;
|
|
920
919
|
}
|
|
921
920
|
if (battle.last_damage == 0) {
|
|
922
921
|
immune = true;
|
|
@@ -1019,7 +1018,7 @@ fn doMove(
|
|
|
1019
1018
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1020
1019
|
|
|
1021
1020
|
var nullified = false;
|
|
1022
|
-
var hit:
|
|
1021
|
+
var hit: usize = 0;
|
|
1023
1022
|
while (hit < hits) {
|
|
1024
1023
|
if (hit == 0) {
|
|
1025
1024
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -1047,7 +1046,7 @@ fn doMove(
|
|
|
1047
1046
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1048
1047
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1049
1048
|
}
|
|
1050
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1049
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
1050
|
} else if (showdown and move.effect == .Binding) {
|
|
1052
1051
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1053
1052
|
try Effects.binding(battle, player, rewrap, options);
|
|
@@ -1248,13 +1247,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1248
1247
|
.Psywave => power: {
|
|
1249
1248
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1250
1249
|
// GLITCH: Psywave infinite glitch loop
|
|
1251
|
-
if (!pkmn.options.mod and max <= 1) return
|
|
1250
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1252
1251
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1253
1252
|
},
|
|
1254
1253
|
else => unreachable,
|
|
1255
1254
|
};
|
|
1256
1255
|
|
|
1257
|
-
if (battle.last_damage == 0) return if (pkmn.options.mod) null else
|
|
1256
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1258
1257
|
|
|
1259
1258
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1260
1259
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1274,15 +1273,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1274
1273
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1275
1274
|
const foe_last_selected_move =
|
|
1276
1275
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1276
|
+
.Splash
|
|
1277
|
+
else
|
|
1278
|
+
foe.last_selected_move);
|
|
1280
1279
|
|
|
1281
1280
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1282
1281
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1283
1282
|
foe.last_selected_move != .Counter and
|
|
1284
1283
|
(foe_last_selected_move.type == .Normal or
|
|
1285
|
-
|
|
1284
|
+
foe_last_selected_move.type == .Fighting);
|
|
1286
1285
|
|
|
1287
1286
|
if (!used and !selected) {
|
|
1288
1287
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1291,16 +1290,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1291
1290
|
|
|
1292
1291
|
if (!used or !selected) {
|
|
1293
1292
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1294
|
-
if (!pkmn.options.mod) return
|
|
1293
|
+
if (!pkmn.options.mod) return .Error;
|
|
1295
1294
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1296
1295
|
return null;
|
|
1297
1296
|
}
|
|
1298
1297
|
|
|
1299
|
-
|
|
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;
|
|
1298
|
+
battle.last_damage *|= 2;
|
|
1304
1299
|
|
|
1305
1300
|
// Pokémon Showdown calls moveHit before Counter
|
|
1306
1301
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1446,7 +1441,16 @@ fn moveHit(
|
|
|
1446
1441
|
break :miss true;
|
|
1447
1442
|
}
|
|
1448
1443
|
if (!showdown) {
|
|
1449
|
-
if (move.effect == .Swift)
|
|
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
|
+
}
|
|
1450
1454
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1451
1455
|
}
|
|
1452
1456
|
|
|
@@ -1519,13 +1523,13 @@ fn checkFaint(
|
|
|
1519
1523
|
|
|
1520
1524
|
if (player_out and foe_out) {
|
|
1521
1525
|
try options.log.tie(.{});
|
|
1522
|
-
return
|
|
1526
|
+
return .Tie;
|
|
1523
1527
|
} else if (player_out) {
|
|
1524
1528
|
try options.log.win(.{player.foe()});
|
|
1525
|
-
return if (player == .P1)
|
|
1529
|
+
return if (player == .P1) .Lose else .Win;
|
|
1526
1530
|
} else if (foe_out) {
|
|
1527
1531
|
try options.log.win(.{player});
|
|
1528
|
-
return if (player == .P1)
|
|
1532
|
+
return if (player == .P1) .Win else .Lose;
|
|
1529
1533
|
}
|
|
1530
1534
|
|
|
1531
1535
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1543,7 +1547,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1543
1547
|
if (foe_volatiles.Bide) {
|
|
1544
1548
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1545
1549
|
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1546
|
-
if (foe_volatiles.state != 0) return
|
|
1550
|
+
if (foe_volatiles.state != 0) return .Error;
|
|
1547
1551
|
}
|
|
1548
1552
|
|
|
1549
1553
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1635,19 +1639,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1635
1639
|
|
|
1636
1640
|
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1637
1641
|
try options.log.tie(.{});
|
|
1638
|
-
return
|
|
1642
|
+
return .Tie;
|
|
1639
1643
|
}
|
|
1640
1644
|
|
|
1641
1645
|
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1642
1646
|
try options.log.tie(.{});
|
|
1643
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1644
1648
|
} else if (battle.turn >= 65535) {
|
|
1645
|
-
return
|
|
1649
|
+
return .Error;
|
|
1646
1650
|
}
|
|
1647
1651
|
|
|
1648
1652
|
try options.log.turn(.{battle.turn});
|
|
1649
1653
|
|
|
1650
|
-
return
|
|
1654
|
+
return .Default;
|
|
1651
1655
|
}
|
|
1652
1656
|
|
|
1653
1657
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1938,7 +1942,7 @@ pub const Effects = struct {
|
|
|
1938
1942
|
return null;
|
|
1939
1943
|
} else if (err) {
|
|
1940
1944
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1941
|
-
return
|
|
1945
|
+
return .Error;
|
|
1942
1946
|
}
|
|
1943
1947
|
|
|
1944
1948
|
volatiles.disable_move =
|
|
@@ -3136,11 +3140,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3136
3140
|
},
|
|
3137
3141
|
.Switch => {
|
|
3138
3142
|
const side = battle.side(player);
|
|
3139
|
-
var slot:
|
|
3143
|
+
var slot: usize = 2;
|
|
3140
3144
|
while (slot <= 6) : (slot += 1) {
|
|
3141
3145
|
const id = side.order[slot - 1];
|
|
3142
3146
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3143
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3147
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3144
3148
|
n += 1;
|
|
3145
3149
|
}
|
|
3146
3150
|
if (n == 0) {
|
|
@@ -3171,11 +3175,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3171
3175
|
return n;
|
|
3172
3176
|
}
|
|
3173
3177
|
|
|
3174
|
-
var slot:
|
|
3178
|
+
var slot: usize = 2;
|
|
3175
3179
|
while (slot <= 6) : (slot += 1) {
|
|
3176
3180
|
const id = side.order[slot - 1];
|
|
3177
3181
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3178
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3182
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3179
3183
|
n += 1;
|
|
3180
3184
|
}
|
|
3181
3185
|
|
|
@@ -3208,7 +3212,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3208
3212
|
const struggle =
|
|
3209
3213
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3210
3214
|
const s = if (struggle) 0 else slot;
|
|
3211
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3215
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3212
3216
|
n += 1;
|
|
3213
3217
|
return n;
|
|
3214
3218
|
}
|
|
@@ -3222,7 +3226,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3222
3226
|
if (m.id == .None) break;
|
|
3223
3227
|
if (m.pp == 0) continue;
|
|
3224
3228
|
if (active.volatiles.disable_move == slot) continue;
|
|
3225
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3229
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3226
3230
|
n += 1;
|
|
3227
3231
|
}
|
|
3228
3232
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
package/src/lib/gen2/calc.zig
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
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 =>
|
|
148
|
+
else => @FieldType(Action, @tagName(field)),
|
|
152
149
|
});
|
|
153
150
|
}
|