@pkmn/engine 0.1.0-dev.3393edb3 → 0.1.0-dev.3846c8e6
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 +28 -13
- 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/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 +236 -216
- package/package.json +35 -131
- package/src/bin/install-pkmn-engine +25 -9
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/bindings/node.zig +117 -0
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +5 -12
- package/src/lib/common/debug.zig +33 -54
- package/src/lib/common/js.zig +3 -9
- package/src/lib/common/optional.zig +11 -21
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +228 -197
- package/src/lib/common/rational.zig +278 -285
- package/src/lib/common/rng.zig +1 -3
- package/src/lib/common/util.zig +14 -31
- package/src/lib/gen1/calc.zig +205 -189
- package/src/lib/gen1/chance.zig +146 -93
- 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 +12 -21
- package/src/lib/gen1/mechanics.zig +112 -93
- package/src/lib/gen2/calc.zig +12 -23
- package/src/lib/gen2/chance.zig +12 -18
- 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 +9 -18
- 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 +46 -34
- package/src/tools/display/ui.css +8 -5
|
@@ -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;
|
|
@@ -341,7 +337,6 @@ fn doTurn(
|
|
|
341
337
|
player_choice,
|
|
342
338
|
player_rewrap,
|
|
343
339
|
player_skip,
|
|
344
|
-
true,
|
|
345
340
|
&residual,
|
|
346
341
|
options,
|
|
347
342
|
)) |r| return r;
|
|
@@ -364,7 +359,6 @@ fn doTurn(
|
|
|
364
359
|
foe_choice,
|
|
365
360
|
foe_rewrap,
|
|
366
361
|
foe_skip,
|
|
367
|
-
false,
|
|
368
362
|
&residual,
|
|
369
363
|
options,
|
|
370
364
|
)) |r| return r;
|
|
@@ -392,7 +386,6 @@ fn executeMove(
|
|
|
392
386
|
choice: Choice,
|
|
393
387
|
rewrap: bool,
|
|
394
388
|
skip: bool,
|
|
395
|
-
first: bool,
|
|
396
389
|
residual: *bool,
|
|
397
390
|
options: anytype,
|
|
398
391
|
) !?Result {
|
|
@@ -427,7 +420,7 @@ fn executeMove(
|
|
|
427
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
428
421
|
const stored = side.stored();
|
|
429
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
430
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
431
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
432
425
|
side.last_selected_move == .HyperBeam or
|
|
433
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
@@ -439,7 +432,7 @@ fn executeMove(
|
|
|
439
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
440
433
|
// every Pokémon must have at least one move
|
|
441
434
|
if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
|
|
442
|
-
return
|
|
435
|
+
return .Error;
|
|
443
436
|
} else {
|
|
444
437
|
auto = true;
|
|
445
438
|
}
|
|
@@ -452,12 +445,31 @@ fn executeMove(
|
|
|
452
445
|
|
|
453
446
|
var skip_can = false;
|
|
454
447
|
var skip_pp = false;
|
|
455
|
-
switch (try beforeMove(battle, player, skip,
|
|
448
|
+
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
456
449
|
.done => return null,
|
|
457
450
|
.skip_can => skip_can = true,
|
|
458
451
|
.skip_pp => skip_pp = true,
|
|
459
452
|
.ok => {},
|
|
460
|
-
.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;
|
|
461
473
|
}
|
|
462
474
|
|
|
463
475
|
const can = skip_can or
|
|
@@ -473,7 +485,6 @@ fn beforeMove(
|
|
|
473
485
|
battle: anytype,
|
|
474
486
|
player: Player,
|
|
475
487
|
skip: bool,
|
|
476
|
-
first: bool,
|
|
477
488
|
residual: *bool,
|
|
478
489
|
options: anytype,
|
|
479
490
|
) !BeforeMove {
|
|
@@ -558,24 +569,17 @@ fn beforeMove(
|
|
|
558
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
559
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
560
571
|
if (side.last_selected_move == .None) {
|
|
561
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
562
573
|
return .err;
|
|
563
574
|
}
|
|
564
575
|
|
|
565
576
|
if (volatiles.Confusion) {
|
|
566
577
|
assert(volatiles.confusion > 0);
|
|
567
578
|
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
568
|
-
.started =>
|
|
569
|
-
if (!first or (options.calc.overridden(player, .confused) orelse .None) != .None) {
|
|
570
|
-
volatiles.confusion -= 1;
|
|
571
|
-
} else {
|
|
572
|
-
volatiles.confusion = 0;
|
|
573
|
-
}
|
|
574
|
-
},
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
575
580
|
.continuing => if (volatiles.confusion > 1) {
|
|
576
581
|
volatiles.confusion -= 1;
|
|
577
582
|
},
|
|
578
|
-
.ended => volatiles.confusion = 0,
|
|
579
583
|
.overwritten => {},
|
|
580
584
|
else => unreachable,
|
|
581
585
|
} else {
|
|
@@ -614,17 +618,10 @@ fn beforeMove(
|
|
|
614
618
|
return .err;
|
|
615
619
|
}
|
|
616
620
|
}
|
|
617
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
618
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
619
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
620
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
621
|
-
player
|
|
622
|
-
else
|
|
623
|
-
player.foe();
|
|
624
621
|
|
|
625
622
|
const uncapped = battle.last_damage;
|
|
626
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
627
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
628
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
629
626
|
if (showdown) battle.last_damage = uncapped;
|
|
630
627
|
|
|
@@ -703,7 +700,7 @@ fn beforeMove(
|
|
|
703
700
|
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
704
701
|
|
|
705
702
|
if (volatiles.attacks == 0) {
|
|
706
|
-
const overwritten =
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
707
704
|
|
|
708
705
|
volatiles.Thrashing = false;
|
|
709
706
|
volatiles.Confusion = true;
|
|
@@ -714,7 +711,11 @@ fn beforeMove(
|
|
|
714
711
|
// can be expected to recognize this pattern and pass the information only to the
|
|
715
712
|
// correct player)
|
|
716
713
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
717
|
-
options.chance.observe(.confusion, player, if (overwritten)
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
718
719
|
}
|
|
719
720
|
|
|
720
721
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -780,9 +781,14 @@ fn canMove(
|
|
|
780
781
|
|
|
781
782
|
if (move.effect == .Thrashing) {
|
|
782
783
|
Effects.thrashing(battle, player, options);
|
|
783
|
-
} else if (
|
|
784
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
785
|
-
|
|
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
|
+
}
|
|
786
792
|
}
|
|
787
793
|
|
|
788
794
|
return true;
|
|
@@ -872,7 +878,7 @@ fn doMove(
|
|
|
872
878
|
var miss = showdown and miss: {
|
|
873
879
|
immune = move.target != .Self and !status and !counter and
|
|
874
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
875
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
876
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
877
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
878
884
|
battle.last_damage = 0;
|
|
@@ -910,7 +916,7 @@ fn doMove(
|
|
|
910
916
|
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
911
917
|
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
912
918
|
if (!showdown) try options.chance.commit(player, .err);
|
|
913
|
-
return
|
|
919
|
+
return .Error;
|
|
914
920
|
}
|
|
915
921
|
if (battle.last_damage == 0) {
|
|
916
922
|
immune = true;
|
|
@@ -1013,7 +1019,7 @@ fn doMove(
|
|
|
1013
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1014
1020
|
|
|
1015
1021
|
var nullified = false;
|
|
1016
|
-
var hit:
|
|
1022
|
+
var hit: usize = 0;
|
|
1017
1023
|
while (hit < hits) {
|
|
1018
1024
|
if (hit == 0) {
|
|
1019
1025
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -1041,7 +1047,7 @@ fn doMove(
|
|
|
1041
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1042
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1043
1049
|
}
|
|
1044
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1045
1051
|
} else if (showdown and move.effect == .Binding) {
|
|
1046
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1047
1053
|
try Effects.binding(battle, player, rewrap, options);
|
|
@@ -1136,7 +1142,7 @@ fn calcDamage(
|
|
|
1136
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1137
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1138
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1139
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1140
1146
|
}
|
|
1141
1147
|
|
|
1142
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1157,7 +1163,7 @@ fn calcDamage(
|
|
|
1157
1163
|
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1158
1164
|
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1159
1165
|
// that results in counter intuitive probabilities in the common case
|
|
1160
|
-
if (pkmn.options.chance and !
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1161
1167
|
atk = attack(side, !crit, special);
|
|
1162
1168
|
def = defense(target, !crit, special, false);
|
|
1163
1169
|
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
@@ -1242,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1242
1248
|
.Psywave => power: {
|
|
1243
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1244
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1245
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1246
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1247
1253
|
},
|
|
1248
1254
|
else => unreachable,
|
|
1249
1255
|
};
|
|
1250
1256
|
|
|
1251
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1252
1258
|
|
|
1253
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1254
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1268,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1268
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1269
1275
|
const foe_last_selected_move =
|
|
1270
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1274
1280
|
|
|
1275
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1276
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1277
1283
|
foe.last_selected_move != .Counter and
|
|
1278
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1279
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1280
1286
|
|
|
1281
1287
|
if (!used and !selected) {
|
|
1282
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1285,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1285
1291
|
|
|
1286
1292
|
if (!used or !selected) {
|
|
1287
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1288
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1289
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1290
1296
|
return null;
|
|
1291
1297
|
}
|
|
1292
1298
|
|
|
1293
|
-
|
|
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;
|
|
1294
1304
|
|
|
1295
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1296
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1436,7 +1446,16 @@ fn moveHit(
|
|
|
1436
1446
|
break :miss true;
|
|
1437
1447
|
}
|
|
1438
1448
|
if (!showdown) {
|
|
1439
|
-
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
|
+
}
|
|
1440
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1441
1460
|
}
|
|
1442
1461
|
|
|
@@ -1509,13 +1528,13 @@ fn checkFaint(
|
|
|
1509
1528
|
|
|
1510
1529
|
if (player_out and foe_out) {
|
|
1511
1530
|
try options.log.tie(.{});
|
|
1512
|
-
return
|
|
1531
|
+
return .Tie;
|
|
1513
1532
|
} else if (player_out) {
|
|
1514
1533
|
try options.log.win(.{player.foe()});
|
|
1515
|
-
return if (player == .P1)
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1516
1535
|
} else if (foe_out) {
|
|
1517
1536
|
try options.log.win(.{player});
|
|
1518
|
-
return if (player == .P1)
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1519
1538
|
}
|
|
1520
1539
|
|
|
1521
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1532,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1532
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1533
1552
|
if (foe_volatiles.Bide) {
|
|
1534
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1535
|
-
foe_volatiles.state = if (
|
|
1536
|
-
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;
|
|
1537
1556
|
}
|
|
1538
1557
|
|
|
1539
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1623,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1623
1642
|
|
|
1624
1643
|
battle.turn += 1;
|
|
1625
1644
|
|
|
1626
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1627
1646
|
try options.log.tie(.{});
|
|
1628
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1629
1648
|
}
|
|
1630
1649
|
|
|
1631
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1632
1651
|
try options.log.tie(.{});
|
|
1633
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1634
1653
|
} else if (battle.turn >= 65535) {
|
|
1635
|
-
return
|
|
1654
|
+
return .Error;
|
|
1636
1655
|
}
|
|
1637
1656
|
|
|
1638
1657
|
try options.log.turn(.{battle.turn});
|
|
1639
1658
|
|
|
1640
|
-
return
|
|
1659
|
+
return .Default;
|
|
1641
1660
|
}
|
|
1642
1661
|
|
|
1643
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1877,6 +1896,7 @@ pub const Effects = struct {
|
|
|
1877
1896
|
|
|
1878
1897
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1879
1898
|
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1880
1900
|
|
|
1881
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1882
1902
|
}
|
|
@@ -1922,13 +1942,12 @@ pub const Effects = struct {
|
|
|
1922
1942
|
|
|
1923
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1924
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1925
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1926
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1927
1947
|
return null;
|
|
1928
1948
|
} else if (err) {
|
|
1929
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1930
|
-
|
|
1931
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1932
1951
|
}
|
|
1933
1952
|
|
|
1934
1953
|
volatiles.disable_move =
|
|
@@ -2005,7 +2024,7 @@ pub const Effects = struct {
|
|
|
2005
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
2006
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2007
2026
|
// Freeze Clause Mod
|
|
2008
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2009
2028
|
|
|
2010
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
2011
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -2346,7 +2365,7 @@ pub const Effects = struct {
|
|
|
2346
2365
|
}
|
|
2347
2366
|
|
|
2348
2367
|
// Sleep Clause Mod
|
|
2349
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2350
2369
|
for (foe.pokemon) |p| {
|
|
2351
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2352
2371
|
}
|
|
@@ -2452,7 +2471,7 @@ pub const Effects = struct {
|
|
|
2452
2471
|
if (side.active.volatiles.Binding) return;
|
|
2453
2472
|
side.active.volatiles.Binding = true;
|
|
2454
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2455
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2456
2475
|
if (!showdown) {
|
|
2457
2476
|
foe.active.volatiles.Recharging = false;
|
|
2458
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
@@ -3126,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3126
3145
|
},
|
|
3127
3146
|
.Switch => {
|
|
3128
3147
|
const side = battle.side(player);
|
|
3129
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3130
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3131
3150
|
const id = side.order[slot - 1];
|
|
3132
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3133
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3134
3153
|
n += 1;
|
|
3135
3154
|
}
|
|
3136
3155
|
if (n == 0) {
|
|
@@ -3161,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3161
3180
|
return n;
|
|
3162
3181
|
}
|
|
3163
3182
|
|
|
3164
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3165
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3166
3185
|
const id = side.order[slot - 1];
|
|
3167
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3168
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3169
3188
|
n += 1;
|
|
3170
3189
|
}
|
|
3171
3190
|
|
|
@@ -3198,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3198
3217
|
const struggle =
|
|
3199
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3200
3219
|
const s = if (struggle) 0 else slot;
|
|
3201
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3202
3221
|
n += 1;
|
|
3203
3222
|
return n;
|
|
3204
3223
|
}
|
|
@@ -3212,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3212
3231
|
if (m.id == .None) break;
|
|
3213
3232
|
if (m.pp == 0) continue;
|
|
3214
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3215
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3216
3235
|
n += 1;
|
|
3217
3236
|
}
|
|
3218
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3219
3238
|
if (n == before) {
|
|
3220
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3221
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3222
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3223
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3224
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
|
}
|
package/src/lib/gen2/chance.zig
CHANGED
|
@@ -27,10 +27,6 @@ const Type = data.Type;
|
|
|
27
27
|
const Effectiveness = data.Effectiveness;
|
|
28
28
|
const TriAttack = data.TriAttack;
|
|
29
29
|
|
|
30
|
-
const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
|
|
31
|
-
const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
|
|
32
|
-
const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
|
|
33
|
-
|
|
34
30
|
/// Actions taken by a hypothetical "chance player" that convey information about which RNG events
|
|
35
31
|
/// were observed during a Generation II battle `update`. This can additionally be provided as input
|
|
36
32
|
/// to the `update` call to override the normal behavior of the RNG in order to force specific
|
|
@@ -57,15 +53,15 @@ pub const Actions = extern struct {
|
|
|
57
53
|
/// same shape if they have the same fields set (though those fields need not necessarily be
|
|
58
54
|
/// set to the same value).
|
|
59
55
|
pub fn matches(a: Actions, b: Actions) bool {
|
|
60
|
-
inline for (@
|
|
61
|
-
inline for (@
|
|
56
|
+
inline for (@typeInfo(Actions).@"struct".fields) |player| {
|
|
57
|
+
inline for (@typeInfo(Action).@"struct".fields) |field| {
|
|
62
58
|
const a_val = @field(@field(a, player.name), field.name);
|
|
63
59
|
const b_val = @field(@field(b, player.name), field.name);
|
|
64
60
|
|
|
65
61
|
switch (@typeInfo(@TypeOf(a_val))) {
|
|
66
|
-
|
|
62
|
+
.@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
|
|
67
63
|
return false,
|
|
68
|
-
|
|
64
|
+
.int => if ((a_val > 0) != (b_val > 0)) return false,
|
|
69
65
|
else => unreachable,
|
|
70
66
|
}
|
|
71
67
|
}
|
|
@@ -73,7 +69,7 @@ pub const Actions = extern struct {
|
|
|
73
69
|
return true;
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
pub fn fmt(self: Actions, writer:
|
|
72
|
+
pub fn fmt(self: Actions, writer: *std.Io.Writer, shape: bool) !void {
|
|
77
73
|
try writer.writeAll("<P1 = ");
|
|
78
74
|
try self.p1.fmt(writer, shape);
|
|
79
75
|
try writer.writeAll(", P2 = ");
|
|
@@ -81,8 +77,7 @@ pub const Actions = extern struct {
|
|
|
81
77
|
try writer.writeAll(">");
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
pub fn format(a: Actions,
|
|
85
|
-
_ = .{ f, o };
|
|
80
|
+
pub fn format(a: Actions, w: *std.Io.Writer) !void {
|
|
86
81
|
try fmt(a, w, false);
|
|
87
82
|
}
|
|
88
83
|
};
|
|
@@ -186,18 +181,17 @@ pub const Action = packed struct(u128) {
|
|
|
186
181
|
|
|
187
182
|
pub const Field = std.meta.FieldEnum(Action);
|
|
188
183
|
|
|
189
|
-
pub fn format(a: Action,
|
|
190
|
-
_ = .{ f, o };
|
|
184
|
+
pub fn format(a: Action, w: *std.Io.Writer) !void {
|
|
191
185
|
try fmt(a, w, false);
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
pub fn fmt(self: Action, writer:
|
|
188
|
+
pub fn fmt(self: Action, writer: *std.Io.Writer, shape: bool) !void {
|
|
195
189
|
try writer.writeByte('(');
|
|
196
190
|
var printed = false;
|
|
197
|
-
inline for (@
|
|
191
|
+
inline for (@typeInfo(Action).@"struct".fields) |field| {
|
|
198
192
|
const val = @field(self, field.name);
|
|
199
193
|
switch (@typeInfo(@TypeOf(val))) {
|
|
200
|
-
|
|
194
|
+
.@"enum" => if (val != .None) {
|
|
201
195
|
if (printed) try writer.writeAll(", ");
|
|
202
196
|
if (shape) {
|
|
203
197
|
try writer.print("{s}:?", .{field.name});
|
|
@@ -211,7 +205,7 @@ pub const Action = packed struct(u128) {
|
|
|
211
205
|
}
|
|
212
206
|
printed = true;
|
|
213
207
|
},
|
|
214
|
-
|
|
208
|
+
.int => if (val != 0) {
|
|
215
209
|
if (printed) try writer.writeAll(", ");
|
|
216
210
|
if (shape) {
|
|
217
211
|
try writer.print("{s}:?", .{field.name});
|
|
@@ -786,7 +780,7 @@ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
|
|
|
786
780
|
|
|
787
781
|
r.reduce();
|
|
788
782
|
if (r.p != p or r.q != q) {
|
|
789
|
-
print("expected {d}/{d}, found {}\n", .{ p, q, r });
|
|
783
|
+
print("expected {d}/{d}, found {f}\n", .{ p, q, r });
|
|
790
784
|
return error.TestExpectedEqual;
|
|
791
785
|
}
|
|
792
786
|
}
|