@pkmn/engine 0.1.0-dev.3de821b9 → 0.1.0-dev.3f3af84c
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/README.md +9 -6
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +100 -0
- package/build/pkg/addon/node.js.map +1 -0
- 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 -3
- package/build/pkg/addon.js +34 -65
- 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.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +11 -12
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -90
- package/build/pkg/index.js +6 -63
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +35 -69
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +142 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +330 -85
- package/package.json +53 -134
- package/src/bin/install-pkmn-engine +10 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- package/src/lib/bindings/node.zig +121 -0
- package/src/lib/bindings/wasm.zig +59 -0
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +4 -0
- package/src/lib/common/protocol.zig +44 -43
- package/src/lib/common/rational.zig +53 -51
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +250 -111
- package/src/lib/gen1/chance.zig +130 -121
- 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 +1 -2
- package/src/lib/gen1/data.zig +35 -38
- package/src/lib/gen1/helpers.zig +15 -27
- package/src/lib/gen1/mechanics.zig +118 -95
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +86 -66
- 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 +1 -2
- package/src/lib/gen2/data.zig +26 -29
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +9 -18
- package/src/lib/gen2/mechanics.zig +81 -81
- package/src/lib/node.zig +3 -116
- package/src/lib/pkmn.zig +14 -7
- package/src/lib/wasm.zig +13 -9
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -405
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,39 +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
|
|
7
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
8
8
|
const protocol = @import("../common/protocol.zig");
|
|
9
9
|
const rng = @import("../common/rng.zig");
|
|
10
|
-
|
|
11
|
-
const data = @import("data.zig");
|
|
12
|
-
const chance = @import("chance.zig");
|
|
10
|
+
const std = @import("std");
|
|
13
11
|
|
|
14
12
|
const assert = std.debug.assert;
|
|
15
|
-
|
|
16
|
-
const expectEqual = std.testing.expectEqual;
|
|
17
|
-
|
|
18
|
-
const showdown = pkmn.options.showdown;
|
|
19
|
-
|
|
20
|
-
const Choice = common.Choice;
|
|
21
|
-
const ID = common.ID;
|
|
22
|
-
const Player = common.Player;
|
|
23
|
-
const Result = common.Result;
|
|
24
|
-
|
|
25
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
26
15
|
const Damage = protocol.Damage;
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
27
18
|
const Fail = protocol.Fail;
|
|
28
|
-
const Heal = protocol.Heal;
|
|
29
|
-
|
|
30
19
|
const Gen12 = rng.Gen12;
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
34
22
|
const Move = data.Move;
|
|
35
23
|
const MoveSlot = data.MoveSlot;
|
|
36
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
37
27
|
const Side = data.Side;
|
|
38
28
|
const Species = data.Species;
|
|
39
29
|
const Stats = data.Stats;
|
|
@@ -176,7 +166,10 @@ fn selectMove(
|
|
|
176
166
|
if (foe_choice.type == .Switch) {
|
|
177
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
178
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
179
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
180
173
|
}
|
|
181
174
|
}
|
|
182
175
|
return null;
|
|
@@ -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);
|
|
@@ -460,6 +453,25 @@ fn executeMove(
|
|
|
460
453
|
.err => return @as(?Result, Result.Error),
|
|
461
454
|
}
|
|
462
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;
|
|
473
|
+
}
|
|
474
|
+
|
|
463
475
|
const can = skip_can or
|
|
464
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
465
477
|
if (!can) return null;
|
|
@@ -557,18 +569,17 @@ fn beforeMove(
|
|
|
557
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
558
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
559
571
|
if (side.last_selected_move == .None) {
|
|
560
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
561
573
|
return .err;
|
|
562
574
|
}
|
|
563
575
|
|
|
564
576
|
if (volatiles.Confusion) {
|
|
565
577
|
assert(volatiles.confusion > 0);
|
|
566
578
|
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
-
.started => volatiles.confusion
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
568
580
|
.continuing => if (volatiles.confusion > 1) {
|
|
569
581
|
volatiles.confusion -= 1;
|
|
570
582
|
},
|
|
571
|
-
.ended => volatiles.confusion = 0,
|
|
572
583
|
.overwritten => {},
|
|
573
584
|
else => unreachable,
|
|
574
585
|
} else {
|
|
@@ -594,8 +605,7 @@ fn beforeMove(
|
|
|
594
605
|
volatiles.Binding = false;
|
|
595
606
|
volatiles.Invulnerable = false;
|
|
596
607
|
|
|
597
|
-
options.chance.observe(.
|
|
598
|
-
options.chance.observe(.thrashing, player, .None);
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
599
609
|
options.chance.observe(.binding, player, .None);
|
|
600
610
|
{
|
|
601
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
@@ -636,8 +646,7 @@ fn beforeMove(
|
|
|
636
646
|
volatiles.Charging = false;
|
|
637
647
|
volatiles.Binding = false;
|
|
638
648
|
|
|
639
|
-
options.chance.observe(.
|
|
640
|
-
options.chance.observe(.thrashing, player, .None);
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
641
650
|
options.chance.observe(.binding, player, .None);
|
|
642
651
|
|
|
643
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
@@ -657,8 +666,8 @@ fn beforeMove(
|
|
|
657
666
|
volatiles.state +%= battle.last_damage;
|
|
658
667
|
}
|
|
659
668
|
|
|
660
|
-
volatiles.attacks = decrement(.
|
|
661
|
-
try options.chance.
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
662
671
|
|
|
663
672
|
if (volatiles.attacks != 0) {
|
|
664
673
|
try log.activate(.{ ident, .Bide });
|
|
@@ -694,8 +703,8 @@ fn beforeMove(
|
|
|
694
703
|
if (volatiles.Thrashing) {
|
|
695
704
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
696
705
|
|
|
697
|
-
volatiles.attacks = decrement(.
|
|
698
|
-
try options.chance.
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
699
708
|
|
|
700
709
|
if (volatiles.attacks == 0) {
|
|
701
710
|
const overwritten = volatiles.Confusion;
|
|
@@ -709,7 +718,11 @@ fn beforeMove(
|
|
|
709
718
|
// can be expected to recognize this pattern and pass the information only to the
|
|
710
719
|
// correct player)
|
|
711
720
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
712
|
-
options.chance.observe(.confusion, player, if (overwritten)
|
|
721
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
722
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
723
|
+
else
|
|
724
|
+
.started);
|
|
725
|
+
if (!overwritten) options.calc.confusion(player);
|
|
713
726
|
}
|
|
714
727
|
|
|
715
728
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -775,9 +788,14 @@ fn canMove(
|
|
|
775
788
|
|
|
776
789
|
if (move.effect == .Thrashing) {
|
|
777
790
|
Effects.thrashing(battle, player, options);
|
|
778
|
-
} else if (
|
|
779
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
780
|
-
|
|
791
|
+
} else if (move.effect == .Binding) {
|
|
792
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
793
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
794
|
+
if (showdown) {
|
|
795
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
796
|
+
} else {
|
|
797
|
+
try Effects.binding(battle, player, false, options);
|
|
798
|
+
}
|
|
781
799
|
}
|
|
782
800
|
|
|
783
801
|
return true;
|
|
@@ -823,7 +841,7 @@ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
|
823
841
|
if (showdown and volatiles.Transform) return;
|
|
824
842
|
|
|
825
843
|
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
826
|
-
if (volatiles.Transform and !options.chance.
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
827
845
|
|
|
828
846
|
assert(mslot > 0 and mslot <= 4);
|
|
829
847
|
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
@@ -880,7 +898,7 @@ fn doMove(
|
|
|
880
898
|
|
|
881
899
|
var late = showdown and move.effect != .Explode;
|
|
882
900
|
const skip = status or immune;
|
|
883
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
884
902
|
if (showdown and move.effect.isMulti()) {
|
|
885
903
|
try Effects.multiHit(battle, player, move, options);
|
|
886
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -902,7 +920,7 @@ fn doMove(
|
|
|
902
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
903
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
904
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
905
|
-
if (showdown) break :
|
|
923
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
906
924
|
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
907
925
|
if (!showdown) try options.chance.commit(player, .err);
|
|
908
926
|
return @as(?Result, Result.Error);
|
|
@@ -1037,22 +1055,20 @@ fn doMove(
|
|
|
1037
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1038
1056
|
}
|
|
1039
1057
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1040
|
-
} else if (showdown) {
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1041
1059
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
try buildRage(battle, player.foe(), options);
|
|
1053
|
-
}
|
|
1054
|
-
return null;
|
|
1060
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1061
|
+
if (immune) {
|
|
1062
|
+
battle.last_damage = 0;
|
|
1063
|
+
assert(foe.stored().hp > 0);
|
|
1064
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1065
|
+
if (sub) {
|
|
1066
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1067
|
+
} else {
|
|
1068
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1069
|
+
try buildRage(battle, player.foe(), options);
|
|
1055
1070
|
}
|
|
1071
|
+
return null;
|
|
1056
1072
|
}
|
|
1057
1073
|
}
|
|
1058
1074
|
|
|
@@ -1133,7 +1149,7 @@ fn calcDamage(
|
|
|
1133
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1134
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1135
1151
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1136
|
-
def = @max((def / 4) & 255, if (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1137
1153
|
}
|
|
1138
1154
|
|
|
1139
1155
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1154,7 +1170,7 @@ fn calcDamage(
|
|
|
1154
1170
|
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1155
1171
|
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1156
1172
|
// that results in counter intuitive probabilities in the common case
|
|
1157
|
-
if (pkmn.options.chance and !
|
|
1173
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1158
1174
|
atk = attack(side, !crit, special);
|
|
1159
1175
|
def = defense(target, !crit, special, false);
|
|
1160
1176
|
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
@@ -1239,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1239
1255
|
.Psywave => power: {
|
|
1240
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1241
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1242
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1243
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1244
1260
|
},
|
|
1245
1261
|
else => unreachable,
|
|
1246
1262
|
};
|
|
1247
1263
|
|
|
1248
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1249
1265
|
|
|
1250
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1251
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1282,12 +1298,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1282
1298
|
|
|
1283
1299
|
if (!used or !selected) {
|
|
1284
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1285
|
-
if (!
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1286
1302
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1287
1303
|
return null;
|
|
1288
1304
|
}
|
|
1289
1305
|
|
|
1290
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1291
1311
|
|
|
1292
1312
|
// Pokémon Showdown calls moveHit before Counter
|
|
1293
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1319,7 +1339,7 @@ fn applyDamage(
|
|
|
1319
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1320
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1321
1341
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1322
|
-
options.calc.capped(sub_player);
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1323
1343
|
return true;
|
|
1324
1344
|
} else {
|
|
1325
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1529,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1529
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1530
1550
|
if (foe_volatiles.Bide) {
|
|
1531
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1532
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1533
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1534
1554
|
}
|
|
1535
1555
|
|
|
@@ -1620,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1620
1640
|
|
|
1621
1641
|
battle.turn += 1;
|
|
1622
1642
|
|
|
1623
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1624
1644
|
try options.log.tie(.{});
|
|
1625
1645
|
return Result.Tie;
|
|
1626
1646
|
}
|
|
1627
1647
|
|
|
1628
|
-
if (
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1629
1649
|
try options.log.tie(.{});
|
|
1630
1650
|
return Result.Tie;
|
|
1631
1651
|
} else if (battle.turn >= 65535) {
|
|
@@ -1805,7 +1825,7 @@ pub const Effects = struct {
|
|
|
1805
1825
|
|
|
1806
1826
|
side.active.volatiles.state = 0;
|
|
1807
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1808
|
-
options.chance.observe(.
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1809
1829
|
|
|
1810
1830
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1811
1831
|
}
|
|
@@ -1874,6 +1894,7 @@ pub const Effects = struct {
|
|
|
1874
1894
|
|
|
1875
1895
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1876
1896
|
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1877
1898
|
|
|
1878
1899
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1879
1900
|
}
|
|
@@ -1919,12 +1940,11 @@ pub const Effects = struct {
|
|
|
1919
1940
|
|
|
1920
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1921
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1922
|
-
if (n == 0) {
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1923
1944
|
try options.log.fail(.{ foe_ident, .None });
|
|
1924
1945
|
return null;
|
|
1925
1946
|
} else if (err) {
|
|
1926
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1927
|
-
assert(!showdown);
|
|
1928
1948
|
return Result.Error;
|
|
1929
1949
|
}
|
|
1930
1950
|
|
|
@@ -2002,7 +2022,7 @@ pub const Effects = struct {
|
|
|
2002
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
2003
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2004
2024
|
// Freeze Clause Mod
|
|
2005
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2006
2026
|
|
|
2007
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
2008
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -2343,7 +2363,7 @@ pub const Effects = struct {
|
|
|
2343
2363
|
}
|
|
2344
2364
|
|
|
2345
2365
|
// Sleep Clause Mod
|
|
2346
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2347
2367
|
for (foe.pokemon) |p| {
|
|
2348
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2349
2369
|
}
|
|
@@ -2410,7 +2430,7 @@ pub const Effects = struct {
|
|
|
2410
2430
|
volatiles.Thrashing = true;
|
|
2411
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2412
2432
|
|
|
2413
|
-
options.chance.observe(.
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2414
2434
|
}
|
|
2415
2435
|
|
|
2416
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2449,7 +2469,7 @@ pub const Effects = struct {
|
|
|
2449
2469
|
if (side.active.volatiles.Binding) return;
|
|
2450
2470
|
side.active.volatiles.Binding = true;
|
|
2451
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2452
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2453
2473
|
if (!showdown) {
|
|
2454
2474
|
foe.active.volatiles.Recharging = false;
|
|
2455
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
@@ -2946,9 +2966,10 @@ pub const Rolls = struct {
|
|
|
2946
2966
|
}
|
|
2947
2967
|
|
|
2948
2968
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2949
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2950
|
-
@intCast(val)
|
|
2951
|
-
else
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2952
2973
|
battle.rng.sleepDuration(player)
|
|
2953
2974
|
else if (showdown)
|
|
2954
2975
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2965,9 +2986,10 @@ pub const Rolls = struct {
|
|
|
2965
2986
|
}
|
|
2966
2987
|
|
|
2967
2988
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2968
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2969
|
-
val
|
|
2970
|
-
else
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2971
2993
|
battle.rng.disableDuration(player)
|
|
2972
2994
|
else if (showdown)
|
|
2973
2995
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2980,9 +3002,10 @@ pub const Rolls = struct {
|
|
|
2980
3002
|
}
|
|
2981
3003
|
|
|
2982
3004
|
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2983
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2984
|
-
@intCast(val)
|
|
2985
|
-
else
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2986
3009
|
battle.rng.confusionDuration(player)
|
|
2987
3010
|
else if (showdown)
|
|
2988
3011
|
battle.rng.range(u3, 2, 6)
|
|
@@ -2995,9 +3018,10 @@ pub const Rolls = struct {
|
|
|
2995
3018
|
}
|
|
2996
3019
|
|
|
2997
3020
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2998
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
-
@intCast(val)
|
|
3000
|
-
else
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3001
3025
|
battle.rng.attackingDuration(player)
|
|
3002
3026
|
else if (showdown)
|
|
3003
3027
|
battle.rng.range(u3, 2, 4)
|
|
@@ -3018,9 +3042,10 @@ pub const Rolls = struct {
|
|
|
3018
3042
|
options: anytype,
|
|
3019
3043
|
) !u3 {
|
|
3020
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
3021
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
3022
|
-
@intCast(val)
|
|
3023
|
-
else
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3024
3049
|
battle.rng.distribution(player)
|
|
3025
3050
|
else if (showdown)
|
|
3026
3051
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -3046,15 +3071,13 @@ pub const Rolls = struct {
|
|
|
3046
3071
|
options: anytype,
|
|
3047
3072
|
) !u4 {
|
|
3048
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
3049
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3050
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3051
3076
|
val
|
|
3052
3077
|
else
|
|
3053
3078
|
null
|
|
3054
3079
|
else
|
|
3055
|
-
null
|
|
3056
|
-
|
|
3057
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3058
3081
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
3059
3082
|
else slot: {
|
|
3060
3083
|
if (showdown) {
|
|
@@ -3212,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3212
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3213
3236
|
if (n == before) {
|
|
3214
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3215
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3216
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3217
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3218
3241
|
}
|
package/src/lib/gen2/calc.zig
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
4
|
-
|
|
1
|
+
const chance = @import("chance.zig");
|
|
5
2
|
const common = @import("../common/data.zig");
|
|
3
|
+
const Optional = @import("../common/optional.zig").Optional;
|
|
4
|
+
const pkmn = @import("../pkmn.zig");
|
|
5
|
+
const std = @import("std");
|
|
6
6
|
const util = @import("../common/util.zig");
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const Action = chance.Action;
|
|
9
|
+
const Actions = chance.Actions;
|
|
10
10
|
const assert = std.debug.assert;
|
|
11
|
-
|
|
11
|
+
const Criticals = chance.Criticals;
|
|
12
|
+
const Damages = chance.Damages;
|
|
12
13
|
const enabled = pkmn.options.calc;
|
|
13
|
-
|
|
14
|
+
const isPointerTo = util.isPointerTo;
|
|
14
15
|
const Player = common.Player;
|
|
15
|
-
|
|
16
16
|
const PointerType = util.PointerType;
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
|
|
19
|
+
const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
|
|
20
20
|
|
|
21
21
|
/// Information relevant to damage calculation that occured during a Generation II battle `update`.
|
|
22
22
|
pub const Summaries = extern struct {
|
|
@@ -31,7 +31,7 @@ pub const Summaries = extern struct {
|
|
|
31
31
|
|
|
32
32
|
/// Returns the `Summary` for the given `player`.
|
|
33
33
|
pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
|
|
34
|
-
assert(
|
|
34
|
+
assert(isPointerTo(self, Summaries));
|
|
35
35
|
return if (player == .P1) &self.p1 else &self.p2;
|
|
36
36
|
}
|
|
37
37
|
};
|
|
@@ -65,33 +65,34 @@ pub const Summary = extern struct {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
/// TODO
|
|
69
|
-
pub const Overrides = extern struct {
|
|
70
|
-
/// TODO
|
|
71
|
-
actions: Actions = .{},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
68
|
/// Allows for forcing the value of specific RNG events during a Generation II battle `update` via
|
|
75
69
|
/// `overrides` and tracks `summaries` of information relevant to damage calculation.
|
|
76
70
|
pub const Calc = struct {
|
|
77
71
|
/// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
|
|
78
|
-
overrides:
|
|
72
|
+
overrides: Actions = .{},
|
|
79
73
|
/// Information relevant to damage calculation.
|
|
80
74
|
summaries: Summaries = .{},
|
|
81
75
|
|
|
82
76
|
pub fn overridden(
|
|
83
77
|
self: Calc,
|
|
84
78
|
player: Player,
|
|
79
|
+
hit: u3,
|
|
85
80
|
comptime field: Action.Field,
|
|
86
|
-
)
|
|
81
|
+
) ReturnType(field) {
|
|
87
82
|
if (!enabled) return null;
|
|
88
83
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const val = switch (field) {
|
|
85
|
+
.damages => Damages.get(self.overrides.get(player).damages, hit),
|
|
86
|
+
.critical_hits => Criticals.get(self.overrides.get(player).critical_hits, hit),
|
|
87
|
+
else => val: {
|
|
88
|
+
assert(hit == 0);
|
|
89
|
+
break :val @field(self.overrides.get(player), @tagName(field));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
92
93
|
return if (switch (@typeInfo(@TypeOf(val))) {
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
Enum => val != .None,
|
|
95
|
+
Int => val != 0,
|
|
95
96
|
else => unreachable,
|
|
96
97
|
}) val else null;
|
|
97
98
|
}
|
|
@@ -123,9 +124,10 @@ const Null = struct {
|
|
|
123
124
|
pub fn overridden(
|
|
124
125
|
self: Null,
|
|
125
126
|
player: Player,
|
|
127
|
+
hit: u3,
|
|
126
128
|
comptime field: Action.Field,
|
|
127
|
-
)
|
|
128
|
-
_ = .{ self, player };
|
|
129
|
+
) ReturnType(field) {
|
|
130
|
+
_ = .{ self, player, hit };
|
|
129
131
|
return null;
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -141,3 +143,11 @@ const Null = struct {
|
|
|
141
143
|
_ = .{ self, player };
|
|
142
144
|
}
|
|
143
145
|
};
|
|
146
|
+
|
|
147
|
+
fn ReturnType(comptime field: Action.Field) type {
|
|
148
|
+
return ?(switch (field) {
|
|
149
|
+
.damages => u8,
|
|
150
|
+
.critical_hits => Optional(bool),
|
|
151
|
+
else => std.meta.FieldType(Action, field),
|
|
152
|
+
});
|
|
153
|
+
}
|