@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.
- package/LICENSE +1 -1
- package/README.md +29 -18
- package/build/pkg/addon/browser.d.ts +1 -1
- package/build/pkg/addon/browser.js +6 -2
- package/build/pkg/addon/browser.js.map +1 -1
- package/build/pkg/addon/node.js +22 -8
- package/build/pkg/addon/node.js.map +1 -1
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -5
- package/build/pkg/addon.js +8 -17
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +37 -100
- package/build/pkg/index.js +23 -75
- 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 +55 -12
- 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 +35 -15
- 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 +351 -246
- package/package.json +34 -130
- package/src/bin/install-pkmn-engine +25 -9
- 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 +5 -12
- package/src/lib/common/debug.zig +45 -60
- package/src/lib/common/js.zig +3 -9
- package/src/lib/common/optional.zig +26 -26
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +228 -197
- package/src/lib/common/rational.zig +275 -284
- package/src/lib/common/rng.zig +1 -3
- package/src/lib/common/util.zig +34 -31
- package/src/lib/gen1/calc.zig +159 -163
- package/src/lib/gen1/chance.zig +181 -105
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +3 -4
- package/src/lib/gen1/data.zig +12 -16
- package/src/lib/gen1/helpers.zig +34 -32
- package/src/lib/gen1/mechanics.zig +103 -78
- package/src/lib/gen2/calc.zig +12 -23
- package/src/lib/gen2/chance.zig +38 -26
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +7 -5
- package/src/lib/gen2/data.zig +19 -22
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +32 -31
- package/src/lib/gen2/mechanics.zig +58 -55
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +4 -121
- package/src/lib/pkmn.zig +98 -19
- package/src/lib/wasm.zig +9 -9
- package/src/tools/display/dom.ts +43 -31
- package/src/tools/display/ui.css +0 -5
package/src/lib/gen1/helpers.zig
CHANGED
|
@@ -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 (@
|
|
173
|
-
|
|
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,
|
|
176
|
-
@field(pokemon.stats,
|
|
177
|
-
|
|
178
|
-
@field(species.stats,
|
|
179
|
-
if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs,
|
|
180
|
-
@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),
|
|
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 (@
|
|
209
|
-
@
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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,
|
|
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
|
|
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.
|
|
303
|
+
w: *protocol.Writer,
|
|
302
304
|
) !usize {
|
|
303
|
-
const pos = w.
|
|
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 =
|
|
311
|
-
if (val !=
|
|
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.
|
|
319
|
+
return w.pos - pos;
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
/// TODO
|
|
@@ -1,36 +1,29 @@
|
|
|
1
|
-
const
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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(!
|
|
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,
|
|
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 (
|
|
777
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
778
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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 !
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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)
|
|
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
|
|
1531
|
+
return .Tie;
|
|
1506
1532
|
} else if (player_out) {
|
|
1507
1533
|
try options.log.win(.{player.foe()});
|
|
1508
|
-
return if (player == .P1)
|
|
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)
|
|
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 (
|
|
1529
|
-
if (foe_volatiles.state != 0) return
|
|
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 (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1620
1646
|
try options.log.tie(.{});
|
|
1621
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1622
1648
|
}
|
|
1623
1649
|
|
|
1624
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1625
1651
|
try options.log.tie(.{});
|
|
1626
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1627
1653
|
} else if (battle.turn >= 65535) {
|
|
1628
|
-
return
|
|
1654
|
+
return .Error;
|
|
1629
1655
|
}
|
|
1630
1656
|
|
|
1631
1657
|
try options.log.turn(.{battle.turn});
|
|
1632
1658
|
|
|
1633
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
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 (!
|
|
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
|
}
|
package/src/lib/gen2/calc.zig
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
const
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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 =>
|
|
148
|
+
else => @FieldType(Action, @tagName(field)),
|
|
160
149
|
});
|
|
161
150
|
}
|