@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.
- package/LICENSE +1 -1
- package/README.md +22 -16
- package/build/pkg/addon/browser.js +6 -3
- package/build/pkg/addon/browser.js.map +1 -1
- package/build/pkg/addon/node.js +23 -10
- package/build/pkg/addon/node.js.map +1 -1
- package/build/pkg/addon/wasm.js +23 -14
- package/build/pkg/addon/wasm.js.map +1 -1
- package/build/pkg/addon.d.ts +5 -5
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +2 -1
- package/build/pkg/gen1.js +22 -9
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +2 -0
- package/build/pkg/index.js +17 -7
- package/build/pkg/index.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 +54 -11
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +19 -94
- package/build/tools/display/debug.jsx.map +1 -1
- package/build/tools/display/index.js +18 -7
- package/build/tools/display/index.js.map +1 -1
- package/build/tools/display/ui.jsx +20 -9
- package/build/tools/display/ui.jsx.map +1 -1
- package/build/tools/display/util.js +140 -1
- package/build/tools/display/util.js.map +1 -1
- package/build.zig +255 -313
- package/package.json +29 -131
- package/src/bin/install-pkmn-engine +24 -22
- package/src/lib/bindings/c.zig +201 -0
- package/src/lib/bindings/node.zig +117 -0
- package/src/lib/bindings/wasm.zig +51 -0
- 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 +275 -284
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +34 -31
- package/src/lib/gen1/calc.zig +141 -119
- 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 +45 -49
- 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 +17 -17
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +3 -118
- package/src/lib/pkmn.zig +94 -19
- package/src/lib/wasm.zig +9 -44
- package/src/tools/display/dom.ts +43 -31
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,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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
1526
|
+
return .Tie;
|
|
1531
1527
|
} else if (player_out) {
|
|
1532
1528
|
try options.log.win(.{player.foe()});
|
|
1533
|
-
return if (player == .P1)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
1647
|
+
return .Tie;
|
|
1652
1648
|
} else if (battle.turn >= 65535) {
|
|
1653
|
-
return
|
|
1649
|
+
return .Error;
|
|
1654
1650
|
}
|
|
1655
1651
|
|
|
1656
1652
|
try options.log.turn(.{battle.turn});
|
|
1657
1653
|
|
|
1658
|
-
return
|
|
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
|
|
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:
|
|
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:
|
|
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)
|
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
|
}
|