@pkmn/engine 0.1.0-dev.3de821b9 → 0.1.0-dev.3ed876db
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 +4 -2
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -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 +97 -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 +21 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +22 -0
- package/build/pkg/addon.js +29 -51
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +23 -9
- package/build/pkg/index.js +17 -11
- 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 -68
- 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 +141 -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 +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +249 -77
- package/package.json +51 -31
- package/src/bin/install-pkmn-engine +7 -5
- package/src/bin/pkmn-debug +1 -1
- 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 +247 -108
- 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 +112 -94
- 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 +62 -73
- package/src/lib/node.zig +2 -4
- package/src/lib/pkmn.zig +11 -5
- package/src/lib/wasm.zig +1 -2
- 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,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const chance = @import("chance.zig");
|
|
5
4
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const data = @import("data.zig");
|
|
6
|
+
const pkmn = @import("../pkmn.zig");
|
|
8
7
|
const protocol = @import("../common/protocol.zig");
|
|
9
8
|
const rng = @import("../common/rng.zig");
|
|
10
|
-
|
|
11
|
-
const data = @import("data.zig");
|
|
12
|
-
const chance = @import("chance.zig");
|
|
9
|
+
const std = @import("std");
|
|
13
10
|
|
|
14
11
|
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
12
|
const Boost = protocol.Boost;
|
|
13
|
+
const Choice = common.Choice;
|
|
26
14
|
const Damage = protocol.Damage;
|
|
15
|
+
const Effectiveness = data.Effectiveness;
|
|
16
|
+
const expectEqual = std.testing.expectEqual;
|
|
27
17
|
const Fail = protocol.Fail;
|
|
28
|
-
const Heal = protocol.Heal;
|
|
29
|
-
|
|
30
18
|
const Gen12 = rng.Gen12;
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const Effectiveness = data.Effectiveness;
|
|
19
|
+
const Heal = protocol.Heal;
|
|
20
|
+
const ID = common.ID;
|
|
34
21
|
const Move = data.Move;
|
|
35
22
|
const MoveSlot = data.MoveSlot;
|
|
36
|
-
const
|
|
23
|
+
const Player = common.Player;
|
|
24
|
+
const Result = common.Result;
|
|
25
|
+
const showdown = pkmn.options.showdown;
|
|
37
26
|
const Side = data.Side;
|
|
38
27
|
const Species = data.Species;
|
|
39
28
|
const Stats = data.Stats;
|
|
@@ -176,7 +165,10 @@ fn selectMove(
|
|
|
176
165
|
if (foe_choice.type == .Switch) {
|
|
177
166
|
const last = side.active.move(battle.lastMove(player).index);
|
|
178
167
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
179
|
-
if (last.id == .MirrorMove)
|
|
168
|
+
if (last.id == .MirrorMove) {
|
|
169
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
170
|
+
side.last_selected_move = last.id;
|
|
171
|
+
}
|
|
180
172
|
}
|
|
181
173
|
}
|
|
182
174
|
return null;
|
|
@@ -427,7 +419,7 @@ fn executeMove(
|
|
|
427
419
|
mslot = @intCast(battle.lastMove(player).index);
|
|
428
420
|
const stored = side.stored();
|
|
429
421
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
430
|
-
auto = isForced(&side.active) or
|
|
422
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
431
423
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
432
424
|
side.last_selected_move == .HyperBeam or
|
|
433
425
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
@@ -460,6 +452,25 @@ fn executeMove(
|
|
|
460
452
|
.err => return @as(?Result, Result.Error),
|
|
461
453
|
}
|
|
462
454
|
|
|
455
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
456
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
457
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
458
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
459
|
+
side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
|
|
460
|
+
{
|
|
461
|
+
assert(mslot == 1);
|
|
462
|
+
const has_beam = has_beam: {
|
|
463
|
+
for (side.active.moves, 0..) |m, i| {
|
|
464
|
+
if (m.id == .HyperBeam) {
|
|
465
|
+
mslot = @intCast(i + 1);
|
|
466
|
+
break :has_beam true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break :has_beam false;
|
|
470
|
+
};
|
|
471
|
+
if (!has_beam) skip_pp = true;
|
|
472
|
+
}
|
|
473
|
+
|
|
463
474
|
const can = skip_can or
|
|
464
475
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
465
476
|
if (!can) return null;
|
|
@@ -557,18 +568,17 @@ fn beforeMove(
|
|
|
557
568
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
558
569
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
559
570
|
if (side.last_selected_move == .None) {
|
|
560
|
-
assert(!
|
|
571
|
+
assert(!pkmn.options.mod);
|
|
561
572
|
return .err;
|
|
562
573
|
}
|
|
563
574
|
|
|
564
575
|
if (volatiles.Confusion) {
|
|
565
576
|
assert(volatiles.confusion > 0);
|
|
566
577
|
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
-
.started => volatiles.confusion
|
|
578
|
+
.started, .ended => volatiles.confusion = 0,
|
|
568
579
|
.continuing => if (volatiles.confusion > 1) {
|
|
569
580
|
volatiles.confusion -= 1;
|
|
570
581
|
},
|
|
571
|
-
.ended => volatiles.confusion = 0,
|
|
572
582
|
.overwritten => {},
|
|
573
583
|
else => unreachable,
|
|
574
584
|
} else {
|
|
@@ -594,8 +604,7 @@ fn beforeMove(
|
|
|
594
604
|
volatiles.Binding = false;
|
|
595
605
|
volatiles.Invulnerable = false;
|
|
596
606
|
|
|
597
|
-
options.chance.observe(.
|
|
598
|
-
options.chance.observe(.thrashing, player, .None);
|
|
607
|
+
options.chance.observe(.attacking, player, .None);
|
|
599
608
|
options.chance.observe(.binding, player, .None);
|
|
600
609
|
{
|
|
601
610
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
@@ -636,8 +645,7 @@ fn beforeMove(
|
|
|
636
645
|
volatiles.Charging = false;
|
|
637
646
|
volatiles.Binding = false;
|
|
638
647
|
|
|
639
|
-
options.chance.observe(.
|
|
640
|
-
options.chance.observe(.thrashing, player, .None);
|
|
648
|
+
options.chance.observe(.attacking, player, .None);
|
|
641
649
|
options.chance.observe(.binding, player, .None);
|
|
642
650
|
|
|
643
651
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
@@ -657,8 +665,8 @@ fn beforeMove(
|
|
|
657
665
|
volatiles.state +%= battle.last_damage;
|
|
658
666
|
}
|
|
659
667
|
|
|
660
|
-
volatiles.attacks = decrement(.
|
|
661
|
-
try options.chance.
|
|
668
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
669
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
662
670
|
|
|
663
671
|
if (volatiles.attacks != 0) {
|
|
664
672
|
try log.activate(.{ ident, .Bide });
|
|
@@ -694,8 +702,8 @@ fn beforeMove(
|
|
|
694
702
|
if (volatiles.Thrashing) {
|
|
695
703
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
696
704
|
|
|
697
|
-
volatiles.attacks = decrement(.
|
|
698
|
-
try options.chance.
|
|
705
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
706
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
699
707
|
|
|
700
708
|
if (volatiles.attacks == 0) {
|
|
701
709
|
const overwritten = volatiles.Confusion;
|
|
@@ -709,7 +717,11 @@ fn beforeMove(
|
|
|
709
717
|
// can be expected to recognize this pattern and pass the information only to the
|
|
710
718
|
// correct player)
|
|
711
719
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
712
|
-
options.chance.observe(.confusion, player, if (overwritten)
|
|
720
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
721
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
722
|
+
else
|
|
723
|
+
.started);
|
|
724
|
+
if (!overwritten) options.calc.confusion(player);
|
|
713
725
|
}
|
|
714
726
|
|
|
715
727
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -775,9 +787,14 @@ fn canMove(
|
|
|
775
787
|
|
|
776
788
|
if (move.effect == .Thrashing) {
|
|
777
789
|
Effects.thrashing(battle, player, options);
|
|
778
|
-
} else if (
|
|
779
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
780
|
-
|
|
790
|
+
} else if (move.effect == .Binding) {
|
|
791
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
792
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
793
|
+
if (showdown) {
|
|
794
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
795
|
+
} else {
|
|
796
|
+
try Effects.binding(battle, player, false, options);
|
|
797
|
+
}
|
|
781
798
|
}
|
|
782
799
|
|
|
783
800
|
return true;
|
|
@@ -823,7 +840,7 @@ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
|
823
840
|
if (showdown and volatiles.Transform) return;
|
|
824
841
|
|
|
825
842
|
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
826
|
-
if (volatiles.Transform and !options.chance.
|
|
843
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
827
844
|
|
|
828
845
|
assert(mslot > 0 and mslot <= 4);
|
|
829
846
|
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
@@ -880,7 +897,7 @@ fn doMove(
|
|
|
880
897
|
|
|
881
898
|
var late = showdown and move.effect != .Explode;
|
|
882
899
|
const skip = status or immune;
|
|
883
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
900
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
884
901
|
if (showdown and move.effect.isMulti()) {
|
|
885
902
|
try Effects.multiHit(battle, player, move, options);
|
|
886
903
|
hits = side.active.volatiles.attacks;
|
|
@@ -902,7 +919,7 @@ fn doMove(
|
|
|
902
919
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
903
920
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
904
921
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
905
|
-
if (showdown) break :
|
|
922
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
906
923
|
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
907
924
|
if (!showdown) try options.chance.commit(player, .err);
|
|
908
925
|
return @as(?Result, Result.Error);
|
|
@@ -1037,22 +1054,20 @@ fn doMove(
|
|
|
1037
1054
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1038
1055
|
}
|
|
1039
1056
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1040
|
-
} else if (showdown) {
|
|
1057
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1041
1058
|
// 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;
|
|
1059
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1060
|
+
if (immune) {
|
|
1061
|
+
battle.last_damage = 0;
|
|
1062
|
+
assert(foe.stored().hp > 0);
|
|
1063
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1064
|
+
if (sub) {
|
|
1065
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1066
|
+
} else {
|
|
1067
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1068
|
+
try buildRage(battle, player.foe(), options);
|
|
1055
1069
|
}
|
|
1070
|
+
return null;
|
|
1056
1071
|
}
|
|
1057
1072
|
}
|
|
1058
1073
|
|
|
@@ -1133,7 +1148,7 @@ fn calcDamage(
|
|
|
1133
1148
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1134
1149
|
atk = @max((atk / 4) & 255, 1);
|
|
1135
1150
|
// 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 (
|
|
1151
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1137
1152
|
}
|
|
1138
1153
|
|
|
1139
1154
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1154,7 +1169,7 @@ fn calcDamage(
|
|
|
1154
1169
|
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1155
1170
|
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1156
1171
|
// that results in counter intuitive probabilities in the common case
|
|
1157
|
-
if (pkmn.options.chance and !
|
|
1172
|
+
if (pkmn.options.chance and !pkmn.options.mod) {
|
|
1158
1173
|
atk = attack(side, !crit, special);
|
|
1159
1174
|
def = defense(target, !crit, special, false);
|
|
1160
1175
|
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
@@ -1239,13 +1254,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1239
1254
|
.Psywave => power: {
|
|
1240
1255
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1241
1256
|
// GLITCH: Psywave infinite glitch loop
|
|
1242
|
-
if (!
|
|
1257
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1243
1258
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1244
1259
|
},
|
|
1245
1260
|
else => unreachable,
|
|
1246
1261
|
};
|
|
1247
1262
|
|
|
1248
|
-
if (battle.last_damage == 0) return if (
|
|
1263
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1249
1264
|
|
|
1250
1265
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1251
1266
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1282,7 +1297,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1282
1297
|
|
|
1283
1298
|
if (!used or !selected) {
|
|
1284
1299
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1285
|
-
if (!
|
|
1300
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1286
1301
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1287
1302
|
return null;
|
|
1288
1303
|
}
|
|
@@ -1319,7 +1334,7 @@ fn applyDamage(
|
|
|
1319
1334
|
subbed.active.volatiles.Substitute = false;
|
|
1320
1335
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1321
1336
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1322
|
-
options.calc.capped(sub_player);
|
|
1337
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1323
1338
|
return true;
|
|
1324
1339
|
} else {
|
|
1325
1340
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1529,7 +1544,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1529
1544
|
assert(!foe_volatiles.MultiHit);
|
|
1530
1545
|
if (foe_volatiles.Bide) {
|
|
1531
1546
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1532
|
-
foe_volatiles.state = if (
|
|
1547
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1533
1548
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1534
1549
|
}
|
|
1535
1550
|
|
|
@@ -1620,12 +1635,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1620
1635
|
|
|
1621
1636
|
battle.turn += 1;
|
|
1622
1637
|
|
|
1623
|
-
if (
|
|
1638
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1624
1639
|
try options.log.tie(.{});
|
|
1625
1640
|
return Result.Tie;
|
|
1626
1641
|
}
|
|
1627
1642
|
|
|
1628
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1629
1644
|
try options.log.tie(.{});
|
|
1630
1645
|
return Result.Tie;
|
|
1631
1646
|
} else if (battle.turn >= 65535) {
|
|
@@ -1805,7 +1820,7 @@ pub const Effects = struct {
|
|
|
1805
1820
|
|
|
1806
1821
|
side.active.volatiles.state = 0;
|
|
1807
1822
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1808
|
-
options.chance.observe(.
|
|
1823
|
+
options.chance.observe(.attacking, player, .started);
|
|
1809
1824
|
|
|
1810
1825
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1811
1826
|
}
|
|
@@ -1874,6 +1889,7 @@ pub const Effects = struct {
|
|
|
1874
1889
|
|
|
1875
1890
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1876
1891
|
options.chance.observe(.confusion, player.foe(), .started);
|
|
1892
|
+
options.calc.confusion(player.foe());
|
|
1877
1893
|
|
|
1878
1894
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1879
1895
|
}
|
|
@@ -1919,12 +1935,11 @@ pub const Effects = struct {
|
|
|
1919
1935
|
|
|
1920
1936
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1921
1937
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1922
|
-
if (n == 0) {
|
|
1938
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1923
1939
|
try options.log.fail(.{ foe_ident, .None });
|
|
1924
1940
|
return null;
|
|
1925
1941
|
} else if (err) {
|
|
1926
1942
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1927
|
-
assert(!showdown);
|
|
1928
1943
|
return Result.Error;
|
|
1929
1944
|
}
|
|
1930
1945
|
|
|
@@ -2002,7 +2017,7 @@ pub const Effects = struct {
|
|
|
2002
2017
|
if (foe.active.types.includes(move.type)) return;
|
|
2003
2018
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
2004
2019
|
// Freeze Clause Mod
|
|
2005
|
-
if (
|
|
2020
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
2006
2021
|
|
|
2007
2022
|
foe_stored.status = Status.init(.FRZ);
|
|
2008
2023
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -2343,7 +2358,7 @@ pub const Effects = struct {
|
|
|
2343
2358
|
}
|
|
2344
2359
|
|
|
2345
2360
|
// Sleep Clause Mod
|
|
2346
|
-
if (
|
|
2361
|
+
if (pkmn.options.mod) {
|
|
2347
2362
|
for (foe.pokemon) |p| {
|
|
2348
2363
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2349
2364
|
}
|
|
@@ -2410,7 +2425,7 @@ pub const Effects = struct {
|
|
|
2410
2425
|
volatiles.Thrashing = true;
|
|
2411
2426
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2412
2427
|
|
|
2413
|
-
options.chance.observe(.
|
|
2428
|
+
options.chance.observe(.attacking, player, .started);
|
|
2414
2429
|
}
|
|
2415
2430
|
|
|
2416
2431
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2449,7 +2464,7 @@ pub const Effects = struct {
|
|
|
2449
2464
|
if (side.active.volatiles.Binding) return;
|
|
2450
2465
|
side.active.volatiles.Binding = true;
|
|
2451
2466
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2452
|
-
// (Pokémon Showdown
|
|
2467
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2453
2468
|
if (!showdown) {
|
|
2454
2469
|
foe.active.volatiles.Recharging = false;
|
|
2455
2470
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
@@ -2946,9 +2961,10 @@ pub const Rolls = struct {
|
|
|
2946
2961
|
}
|
|
2947
2962
|
|
|
2948
2963
|
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
|
|
2964
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2965
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2966
|
+
else
|
|
2967
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2952
2968
|
battle.rng.sleepDuration(player)
|
|
2953
2969
|
else if (showdown)
|
|
2954
2970
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2965,9 +2981,10 @@ pub const Rolls = struct {
|
|
|
2965
2981
|
}
|
|
2966
2982
|
|
|
2967
2983
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2968
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2969
|
-
val
|
|
2970
|
-
else
|
|
2984
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2985
|
+
if (val >= 1 and val <= 8) val else null
|
|
2986
|
+
else
|
|
2987
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2971
2988
|
battle.rng.disableDuration(player)
|
|
2972
2989
|
else if (showdown)
|
|
2973
2990
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2980,9 +2997,10 @@ pub const Rolls = struct {
|
|
|
2980
2997
|
}
|
|
2981
2998
|
|
|
2982
2999
|
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
|
|
3000
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3001
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3002
|
+
else
|
|
3003
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2986
3004
|
battle.rng.confusionDuration(player)
|
|
2987
3005
|
else if (showdown)
|
|
2988
3006
|
battle.rng.range(u3, 2, 6)
|
|
@@ -2995,9 +3013,10 @@ pub const Rolls = struct {
|
|
|
2995
3013
|
}
|
|
2996
3014
|
|
|
2997
3015
|
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
|
|
3016
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3017
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3018
|
+
else
|
|
3019
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3001
3020
|
battle.rng.attackingDuration(player)
|
|
3002
3021
|
else if (showdown)
|
|
3003
3022
|
battle.rng.range(u3, 2, 4)
|
|
@@ -3018,9 +3037,10 @@ pub const Rolls = struct {
|
|
|
3018
3037
|
options: anytype,
|
|
3019
3038
|
) !u3 {
|
|
3020
3039
|
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
|
|
3040
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3041
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3042
|
+
else
|
|
3043
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3024
3044
|
battle.rng.distribution(player)
|
|
3025
3045
|
else if (showdown)
|
|
3026
3046
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -3046,15 +3066,13 @@ pub const Rolls = struct {
|
|
|
3046
3066
|
options: anytype,
|
|
3047
3067
|
) !u4 {
|
|
3048
3068
|
// TODO: consider throwing error instead of rerolling?
|
|
3049
|
-
const
|
|
3069
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3050
3070
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3051
3071
|
val
|
|
3052
3072
|
else
|
|
3053
3073
|
null
|
|
3054
3074
|
else
|
|
3055
|
-
null
|
|
3056
|
-
|
|
3057
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3075
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3058
3076
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
3059
3077
|
else slot: {
|
|
3060
3078
|
if (showdown) {
|
|
@@ -3212,7 +3230,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3212
3230
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3213
3231
|
if (n == before) {
|
|
3214
3232
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3215
|
-
if (!
|
|
3233
|
+
if (!pkmn.options.mod) {
|
|
3216
3234
|
while (slot <= 4) : (slot += 1) {
|
|
3217
3235
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3218
3236
|
}
|
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
|
+
}
|