@pkmn/engine 0.1.0-dev.ce4c0b2a → 0.1.0-dev.d127cb34
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 +23 -15
- 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 +56 -13
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +19 -81
- 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 +27 -16
- package/build/tools/display/ui.jsx.map +1 -1
- package/build/tools/display/util.js +141 -2
- package/build/tools/display/util.js.map +1 -1
- package/build.zig +330 -245
- package/package.json +36 -128
- 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 -34
- 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 +278 -285
- package/src/lib/common/rng.zig +1 -3
- package/src/lib/common/util.zig +34 -31
- package/src/lib/gen1/calc.zig +220 -195
- package/src/lib/gen1/chance.zig +184 -108
- 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 +35 -40
- package/src/lib/gen1/mechanics.zig +147 -127
- 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 +12 -9
|
@@ -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,23 +1047,21 @@ 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 });
|
|
1045
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1046
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
try buildRage(battle, player.foe(), options);
|
|
1058
|
-
}
|
|
1059
|
-
return null;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1060
1063
|
}
|
|
1064
|
+
return null;
|
|
1061
1065
|
}
|
|
1062
1066
|
}
|
|
1063
1067
|
|
|
@@ -1138,7 +1142,7 @@ fn calcDamage(
|
|
|
1138
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1139
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1140
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1141
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1142
1146
|
}
|
|
1143
1147
|
|
|
1144
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1159,7 +1163,7 @@ fn calcDamage(
|
|
|
1159
1163
|
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1160
1164
|
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1161
1165
|
// that results in counter intuitive probabilities in the common case
|
|
1162
|
-
if (pkmn.options.chance and !
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1163
1167
|
atk = attack(side, !crit, special);
|
|
1164
1168
|
def = defense(target, !crit, special, false);
|
|
1165
1169
|
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
@@ -1244,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1244
1248
|
.Psywave => power: {
|
|
1245
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1246
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1247
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return .Error;
|
|
1248
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1249
1253
|
},
|
|
1250
1254
|
else => unreachable,
|
|
1251
1255
|
};
|
|
1252
1256
|
|
|
1253
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
|
|
1254
1258
|
|
|
1255
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1256
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1270,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1270
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1271
1275
|
const foe_last_selected_move =
|
|
1272
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1276
1280
|
|
|
1277
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1278
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1279
1283
|
foe.last_selected_move != .Counter and
|
|
1280
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1281
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1282
1286
|
|
|
1283
1287
|
if (!used and !selected) {
|
|
1284
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1287,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1287
1291
|
|
|
1288
1292
|
if (!used or !selected) {
|
|
1289
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1290
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return .Error;
|
|
1291
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1292
1296
|
return null;
|
|
1293
1297
|
}
|
|
1294
1298
|
|
|
1295
|
-
|
|
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;
|
|
1296
1304
|
|
|
1297
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1298
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1324,7 +1332,7 @@ fn applyDamage(
|
|
|
1324
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1325
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1326
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1327
|
-
options.calc.capped(sub_player);
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1328
1336
|
return true;
|
|
1329
1337
|
} else {
|
|
1330
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1438,7 +1446,16 @@ fn moveHit(
|
|
|
1438
1446
|
break :miss true;
|
|
1439
1447
|
}
|
|
1440
1448
|
if (!showdown) {
|
|
1441
|
-
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
|
+
}
|
|
1442
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1443
1460
|
}
|
|
1444
1461
|
|
|
@@ -1511,13 +1528,13 @@ fn checkFaint(
|
|
|
1511
1528
|
|
|
1512
1529
|
if (player_out and foe_out) {
|
|
1513
1530
|
try options.log.tie(.{});
|
|
1514
|
-
return
|
|
1531
|
+
return .Tie;
|
|
1515
1532
|
} else if (player_out) {
|
|
1516
1533
|
try options.log.win(.{player.foe()});
|
|
1517
|
-
return if (player == .P1)
|
|
1534
|
+
return if (player == .P1) .Lose else .Win;
|
|
1518
1535
|
} else if (foe_out) {
|
|
1519
1536
|
try options.log.win(.{player});
|
|
1520
|
-
return if (player == .P1)
|
|
1537
|
+
return if (player == .P1) .Win else .Lose;
|
|
1521
1538
|
}
|
|
1522
1539
|
|
|
1523
1540
|
const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
|
|
@@ -1534,8 +1551,8 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1534
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1535
1552
|
if (foe_volatiles.Bide) {
|
|
1536
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1537
|
-
foe_volatiles.state = if (
|
|
1538
|
-
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;
|
|
1539
1556
|
}
|
|
1540
1557
|
|
|
1541
1558
|
// Clearing these is not strictly necessary as provided the battle hasn't ended the side that
|
|
@@ -1625,21 +1642,21 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1625
1642
|
|
|
1626
1643
|
battle.turn += 1;
|
|
1627
1644
|
|
|
1628
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1629
1646
|
try options.log.tie(.{});
|
|
1630
|
-
return
|
|
1647
|
+
return .Tie;
|
|
1631
1648
|
}
|
|
1632
1649
|
|
|
1633
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1634
1651
|
try options.log.tie(.{});
|
|
1635
|
-
return
|
|
1652
|
+
return .Tie;
|
|
1636
1653
|
} else if (battle.turn >= 65535) {
|
|
1637
|
-
return
|
|
1654
|
+
return .Error;
|
|
1638
1655
|
}
|
|
1639
1656
|
|
|
1640
1657
|
try options.log.turn(.{battle.turn});
|
|
1641
1658
|
|
|
1642
|
-
return
|
|
1659
|
+
return .Default;
|
|
1643
1660
|
}
|
|
1644
1661
|
|
|
1645
1662
|
fn checkEBC(battle: anytype) bool {
|
|
@@ -1879,6 +1896,7 @@ pub const Effects = struct {
|
|
|
1879
1896
|
|
|
1880
1897
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1881
1898
|
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1882
1900
|
|
|
1883
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1884
1902
|
}
|
|
@@ -1924,13 +1942,12 @@ pub const Effects = struct {
|
|
|
1924
1942
|
|
|
1925
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1926
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1927
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1928
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1929
1947
|
return null;
|
|
1930
1948
|
} else if (err) {
|
|
1931
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1932
|
-
|
|
1933
|
-
return Result.Error;
|
|
1950
|
+
return .Error;
|
|
1934
1951
|
}
|
|
1935
1952
|
|
|
1936
1953
|
volatiles.disable_move =
|
|
@@ -2007,7 +2024,7 @@ pub const Effects = struct {
|
|
|
2007
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
2008
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2009
2026
|
// Freeze Clause Mod
|
|
2010
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2011
2028
|
|
|
2012
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
2013
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -2348,7 +2365,7 @@ pub const Effects = struct {
|
|
|
2348
2365
|
}
|
|
2349
2366
|
|
|
2350
2367
|
// Sleep Clause Mod
|
|
2351
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2352
2369
|
for (foe.pokemon) |p| {
|
|
2353
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2354
2371
|
}
|
|
@@ -2454,7 +2471,7 @@ pub const Effects = struct {
|
|
|
2454
2471
|
if (side.active.volatiles.Binding) return;
|
|
2455
2472
|
side.active.volatiles.Binding = true;
|
|
2456
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2457
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2458
2475
|
if (!showdown) {
|
|
2459
2476
|
foe.active.volatiles.Recharging = false;
|
|
2460
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
@@ -2951,9 +2968,10 @@ pub const Rolls = struct {
|
|
|
2951
2968
|
}
|
|
2952
2969
|
|
|
2953
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2954
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2955
|
-
@intCast(val)
|
|
2956
|
-
else
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2957
2975
|
battle.rng.sleepDuration(player)
|
|
2958
2976
|
else if (showdown)
|
|
2959
2977
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2970,9 +2988,10 @@ pub const Rolls = struct {
|
|
|
2970
2988
|
}
|
|
2971
2989
|
|
|
2972
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2973
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2974
|
-
val
|
|
2975
|
-
else
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2976
2995
|
battle.rng.disableDuration(player)
|
|
2977
2996
|
else if (showdown)
|
|
2978
2997
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2985,9 +3004,10 @@ pub const Rolls = struct {
|
|
|
2985
3004
|
}
|
|
2986
3005
|
|
|
2987
3006
|
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2988
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2989
|
-
@intCast(val)
|
|
2990
|
-
else
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2991
3011
|
battle.rng.confusionDuration(player)
|
|
2992
3012
|
else if (showdown)
|
|
2993
3013
|
battle.rng.range(u3, 2, 6)
|
|
@@ -3000,9 +3020,10 @@ pub const Rolls = struct {
|
|
|
3000
3020
|
}
|
|
3001
3021
|
|
|
3002
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3003
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
3004
|
-
@intCast(val)
|
|
3005
|
-
else
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3006
3027
|
battle.rng.attackingDuration(player)
|
|
3007
3028
|
else if (showdown)
|
|
3008
3029
|
battle.rng.range(u3, 2, 4)
|
|
@@ -3023,9 +3044,10 @@ pub const Rolls = struct {
|
|
|
3023
3044
|
options: anytype,
|
|
3024
3045
|
) !u3 {
|
|
3025
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
3026
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
3027
|
-
@intCast(val)
|
|
3028
|
-
else
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3029
3051
|
battle.rng.distribution(player)
|
|
3030
3052
|
else if (showdown)
|
|
3031
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -3051,15 +3073,13 @@ pub const Rolls = struct {
|
|
|
3051
3073
|
options: anytype,
|
|
3052
3074
|
) !u4 {
|
|
3053
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
3054
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3055
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3056
3078
|
val
|
|
3057
3079
|
else
|
|
3058
3080
|
null
|
|
3059
3081
|
else
|
|
3060
|
-
null
|
|
3061
|
-
|
|
3062
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3063
3083
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
3064
3084
|
else slot: {
|
|
3065
3085
|
if (showdown) {
|
|
@@ -3125,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3125
3145
|
},
|
|
3126
3146
|
.Switch => {
|
|
3127
3147
|
const side = battle.side(player);
|
|
3128
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3129
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3130
3150
|
const id = side.order[slot - 1];
|
|
3131
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3132
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3133
3153
|
n += 1;
|
|
3134
3154
|
}
|
|
3135
3155
|
if (n == 0) {
|
|
@@ -3160,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3160
3180
|
return n;
|
|
3161
3181
|
}
|
|
3162
3182
|
|
|
3163
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3164
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3165
3185
|
const id = side.order[slot - 1];
|
|
3166
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3167
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3168
3188
|
n += 1;
|
|
3169
3189
|
}
|
|
3170
3190
|
|
|
@@ -3197,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3197
3217
|
const struggle =
|
|
3198
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3199
3219
|
const s = if (struggle) 0 else slot;
|
|
3200
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3201
3221
|
n += 1;
|
|
3202
3222
|
return n;
|
|
3203
3223
|
}
|
|
@@ -3211,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3211
3231
|
if (m.id == .None) break;
|
|
3212
3232
|
if (m.pp == 0) continue;
|
|
3213
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3214
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3215
3235
|
n += 1;
|
|
3216
3236
|
}
|
|
3217
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3218
3238
|
if (n == before) {
|
|
3219
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3220
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3221
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3222
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3223
3243
|
}
|