@pkmn/engine 0.1.0-dev.a0167a9d → 0.1.0-dev.a0447192
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 +43 -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 +96 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon.d.ts +2 -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 +22 -9
- package/build/pkg/index.js +15 -10
- 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 +132 -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 +199 -66
- package/package.json +46 -29
- 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 +2 -0
- package/src/lib/common/protocol.zig +41 -36
- package/src/lib/common/rational.zig +53 -51
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +508 -127
- package/src/lib/gen1/chance.zig +316 -173
- package/src/lib/gen1/data.zig +28 -27
- package/src/lib/gen1/helpers.zig +15 -450
- package/src/lib/gen1/mechanics.zig +216 -145
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +90 -592
- package/src/lib/gen2/data.zig +15 -14
- package/src/lib/gen2/helpers.zig +4 -6
- package/src/lib/gen2/mechanics.zig +47 -77
- package/src/lib/pkmn.zig +7 -5
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +359 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
|
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
9
|
|
|
10
10
|
const data = @import("data.zig");
|
|
11
|
+
const chance = @import("chance.zig");
|
|
11
12
|
|
|
12
13
|
const assert = std.debug.assert;
|
|
13
14
|
|
|
@@ -27,11 +28,9 @@ const Heal = protocol.Heal;
|
|
|
27
28
|
|
|
28
29
|
const Gen12 = rng.Gen12;
|
|
29
30
|
|
|
30
|
-
const ActivePokemon = data.ActivePokemon;
|
|
31
31
|
const Effectiveness = data.Effectiveness;
|
|
32
32
|
const Move = data.Move;
|
|
33
33
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const Pokemon = data.Pokemon;
|
|
35
34
|
const Side = data.Side;
|
|
36
35
|
const Species = data.Species;
|
|
37
36
|
const Stats = data.Stats;
|
|
@@ -253,11 +252,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
252
|
active.moves = incoming.moves;
|
|
254
253
|
|
|
255
254
|
statusModify(incoming.status, &active.stats);
|
|
255
|
+
options.chance.switched(player, slot);
|
|
256
256
|
|
|
257
257
|
foe.active.volatiles.Binding = false;
|
|
258
258
|
|
|
259
259
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
260
|
|
|
262
261
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
262
|
incoming.status = Status.init(.PSN);
|
|
@@ -353,7 +352,7 @@ fn doTurn(
|
|
|
353
352
|
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
353
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
354
|
|
|
356
|
-
options.chance.clearPending();
|
|
355
|
+
if (!showdown) options.chance.clearPending();
|
|
357
356
|
|
|
358
357
|
residual = true;
|
|
359
358
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -484,17 +483,22 @@ fn beforeMove(
|
|
|
484
483
|
|
|
485
484
|
if (Status.is(stored.status, .SLP)) {
|
|
486
485
|
const before = stored.status;
|
|
486
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
487
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
488
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
489
|
+
.started, .ended => stored.status = 0,
|
|
490
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
491
|
+
stored.status -= 1;
|
|
492
|
+
},
|
|
493
|
+
else => unreachable,
|
|
490
494
|
} else {
|
|
491
495
|
stored.status -= 1;
|
|
492
496
|
}
|
|
493
497
|
|
|
494
498
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
499
|
+
try options.chance.sleep(player, if (slf)
|
|
500
|
+
.None
|
|
501
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
502
|
|
|
499
503
|
if (duration == 0) {
|
|
500
504
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +536,12 @@ fn beforeMove(
|
|
|
532
536
|
}
|
|
533
537
|
|
|
534
538
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
volatiles.disable_duration =
|
|
540
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
541
|
+
try options.chance.disable(
|
|
542
|
+
player,
|
|
543
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
544
|
+
);
|
|
541
545
|
|
|
542
546
|
if (volatiles.disable_duration == 0) {
|
|
543
547
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +560,18 @@ fn beforeMove(
|
|
|
556
560
|
|
|
557
561
|
if (volatiles.Confusion) {
|
|
558
562
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
563
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
564
|
+
.started, .ended => volatiles.confusion = 0,
|
|
565
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
566
|
+
volatiles.confusion -= 1;
|
|
567
|
+
},
|
|
568
|
+
.overwritten => {},
|
|
569
|
+
else => unreachable,
|
|
562
570
|
} else {
|
|
563
571
|
volatiles.confusion -= 1;
|
|
564
572
|
}
|
|
565
|
-
|
|
573
|
+
|
|
574
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
575
|
|
|
567
576
|
if (volatiles.confusion == 0) {
|
|
568
577
|
volatiles.Confusion = false;
|
|
@@ -580,7 +589,9 @@ fn beforeMove(
|
|
|
580
589
|
volatiles.Charging = false;
|
|
581
590
|
volatiles.Binding = false;
|
|
582
591
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
592
|
+
|
|
593
|
+
options.chance.observe(.attacking, player, .None);
|
|
594
|
+
options.chance.observe(.binding, player, .None);
|
|
584
595
|
{
|
|
585
596
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
597
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +599,9 @@ fn beforeMove(
|
|
|
588
599
|
const def = foe.active.stats.def;
|
|
589
600
|
foe.active.stats.def = active.stats.def;
|
|
590
601
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
602
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
603
|
+
return .err;
|
|
604
|
+
}
|
|
592
605
|
}
|
|
593
606
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
607
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +630,10 @@ fn beforeMove(
|
|
|
617
630
|
volatiles.Thrashing = false;
|
|
618
631
|
volatiles.Charging = false;
|
|
619
632
|
volatiles.Binding = false;
|
|
620
|
-
|
|
633
|
+
|
|
634
|
+
options.chance.observe(.attacking, player, .None);
|
|
635
|
+
options.chance.observe(.binding, player, .None);
|
|
636
|
+
|
|
621
637
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
638
|
try log.cant(.{ ident, .Paralysis });
|
|
623
639
|
return .done;
|
|
@@ -635,12 +651,8 @@ fn beforeMove(
|
|
|
635
651
|
volatiles.state +%= battle.last_damage;
|
|
636
652
|
}
|
|
637
653
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
654
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
655
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
656
|
|
|
645
657
|
if (volatiles.attacks != 0) {
|
|
646
658
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +686,40 @@ fn beforeMove(
|
|
|
674
686
|
}
|
|
675
687
|
|
|
676
688
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
689
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
690
|
+
|
|
691
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
692
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
693
|
+
|
|
694
|
+
if (volatiles.attacks == 0) {
|
|
695
|
+
const overwritten = volatiles.Confusion;
|
|
683
696
|
|
|
684
|
-
|
|
697
|
+
volatiles.Thrashing = false;
|
|
698
|
+
volatiles.Confusion = true;
|
|
699
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
700
|
+
|
|
701
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
702
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
703
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
704
|
+
// correct player)
|
|
685
705
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
706
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
707
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
708
|
+
else
|
|
709
|
+
.started);
|
|
710
|
+
if (!overwritten) options.calc.confusion(player);
|
|
686
711
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
712
|
+
|
|
713
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
714
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
715
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
716
|
+
|
|
696
717
|
return .skip_can;
|
|
697
718
|
}
|
|
698
719
|
|
|
699
720
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
721
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
722
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
723
|
|
|
707
724
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
725
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -795,7 +812,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
812
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
813
|
}
|
|
797
814
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
815
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
816
|
var active = &side.active;
|
|
800
817
|
const volatiles = &active.volatiles;
|
|
801
818
|
|
|
@@ -803,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
820
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
821
|
if (showdown and volatiles.Transform) return;
|
|
805
822
|
|
|
823
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
824
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
825
|
+
|
|
806
826
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
827
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
828
|
}
|
|
809
829
|
|
|
810
830
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,7 +878,7 @@ fn doMove(
|
|
|
858
878
|
|
|
859
879
|
var late = showdown and move.effect != .Explode;
|
|
860
880
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
881
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
882
|
if (showdown and move.effect.isMulti()) {
|
|
863
883
|
try Effects.multiHit(battle, player, move, options);
|
|
864
884
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +900,9 @@ fn doMove(
|
|
|
880
900
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
901
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
902
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
903
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
904
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
905
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
906
|
return @as(?Result, Result.Error);
|
|
886
907
|
}
|
|
887
908
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +916,12 @@ fn doMove(
|
|
|
895
916
|
}
|
|
896
917
|
}
|
|
897
918
|
|
|
919
|
+
var missed: bool = undefined;
|
|
898
920
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
921
|
+
if (!showdown and !skip) {
|
|
922
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
923
|
+
miss = missed or zero;
|
|
924
|
+
}
|
|
903
925
|
|
|
904
926
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
927
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +948,32 @@ fn doMove(
|
|
|
926
948
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
949
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
950
|
try log.immune(.{ foe_ident, .None });
|
|
951
|
+
if (!showdown) {
|
|
952
|
+
if (move.effect == .Binding) {
|
|
953
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
954
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
955
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
956
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
957
|
+
// (considered missing)
|
|
958
|
+
assert(zero);
|
|
959
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
960
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
961
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
962
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
963
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
964
|
+
// roll)
|
|
965
|
+
try options.chance.commit(player, .binding);
|
|
966
|
+
} else {
|
|
967
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
968
|
+
// division-by-zero freeze alternative timeline scenario
|
|
969
|
+
try options.chance.commit(player, .glitch);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
929
972
|
} else if (mist) {
|
|
930
973
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
974
|
try log.fail(.{ foe_ident, .None });
|
|
932
975
|
} else {
|
|
933
|
-
if (!showdown
|
|
976
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
977
|
try log.lastmiss(.{});
|
|
935
978
|
try log.miss(.{battle.active(player)});
|
|
936
979
|
}
|
|
@@ -945,8 +988,6 @@ fn doMove(
|
|
|
945
988
|
try buildRage(battle, player.foe(), options);
|
|
946
989
|
} else if (showdown and move.effect == .Disable) {
|
|
947
990
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
991
|
}
|
|
951
992
|
return null;
|
|
952
993
|
} else if (!showdown) {
|
|
@@ -994,22 +1035,20 @@ fn doMove(
|
|
|
994
1035
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1036
|
}
|
|
996
1037
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1038
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1039
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try buildRage(battle, player.foe(), options);
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1040
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1041
|
+
if (immune) {
|
|
1042
|
+
battle.last_damage = 0;
|
|
1043
|
+
assert(foe.stored().hp > 0);
|
|
1044
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1045
|
+
if (sub) {
|
|
1046
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1047
|
+
} else {
|
|
1048
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1049
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1050
|
}
|
|
1051
|
+
return null;
|
|
1013
1052
|
}
|
|
1014
1053
|
}
|
|
1015
1054
|
|
|
@@ -1069,6 +1108,7 @@ fn calcDamage(
|
|
|
1069
1108
|
target_player: Player,
|
|
1070
1109
|
m: ?Move.Data,
|
|
1071
1110
|
crit: bool,
|
|
1111
|
+
options: anytype,
|
|
1072
1112
|
) bool {
|
|
1073
1113
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1114
|
const cfz = m == null;
|
|
@@ -1080,28 +1120,8 @@ fn calcDamage(
|
|
|
1080
1120
|
|
|
1081
1121
|
const special = move.type.special();
|
|
1082
1122
|
|
|
1083
|
-
|
|
1084
|
-
var
|
|
1085
|
-
if (crit)
|
|
1086
|
-
if (special) side.stored().stats.spc
|
|
1087
|
-
else side.stored().stats.atk
|
|
1088
|
-
else
|
|
1089
|
-
if (special) side.active.stats.spc
|
|
1090
|
-
else side.active.stats.atk;
|
|
1091
|
-
var def: u32 =
|
|
1092
|
-
if (crit)
|
|
1093
|
-
if (special) target.stored().stats.spc
|
|
1094
|
-
else target.stored().stats.def
|
|
1095
|
-
else
|
|
1096
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1097
|
-
if (special)
|
|
1098
|
-
target.active.stats.spc *
|
|
1099
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1100
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1101
|
-
else
|
|
1102
|
-
target.active.stats.def *
|
|
1103
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1104
|
-
// zig fmt: on
|
|
1123
|
+
var atk = attack(side, crit, special);
|
|
1124
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1125
|
|
|
1106
1126
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1127
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1126,11 +1146,41 @@ fn calcDamage(
|
|
|
1126
1146
|
d = @min(997, d);
|
|
1127
1147
|
d += 2;
|
|
1128
1148
|
|
|
1149
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1150
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1151
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1152
|
+
// that results in counter intuitive probabilities in the common case
|
|
1153
|
+
if (pkmn.options.chance and !showdown) {
|
|
1154
|
+
atk = attack(side, !crit, special);
|
|
1155
|
+
def = defense(target, !crit, special, false);
|
|
1156
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1129
1159
|
battle.last_damage = @intCast(d);
|
|
1130
1160
|
|
|
1131
1161
|
return true;
|
|
1132
1162
|
}
|
|
1133
1163
|
|
|
1164
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1165
|
+
return if (crit)
|
|
1166
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1167
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1171
|
+
return if (crit)
|
|
1172
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1173
|
+
else
|
|
1174
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1175
|
+
if (special)
|
|
1176
|
+
target.active.stats.spc *
|
|
1177
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1178
|
+
else
|
|
1179
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1180
|
+
target.active.stats.def *
|
|
1181
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1134
1184
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1185
|
const side = battle.side(player);
|
|
1136
1186
|
const foe = battle.foe(player);
|
|
@@ -1265,7 +1315,7 @@ fn applyDamage(
|
|
|
1265
1315
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1316
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1317
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(sub_player);
|
|
1318
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1319
|
return true;
|
|
1270
1320
|
} else {
|
|
1271
1321
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1349,7 @@ fn mirrorMove(
|
|
|
1299
1349
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1350
|
return null;
|
|
1301
1351
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1352
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1353
|
}
|
|
1304
1354
|
|
|
1305
1355
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1370,7 @@ fn metronome(
|
|
|
1320
1370
|
var side = battle.side(player);
|
|
1321
1371
|
|
|
1322
1372
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1373
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1374
|
|
|
1325
1375
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1376
|
return null;
|
|
@@ -1421,7 +1471,10 @@ fn moveHit(
|
|
|
1421
1471
|
|
|
1422
1472
|
if (!miss) return true;
|
|
1423
1473
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1474
|
+
|
|
1424
1475
|
side.active.volatiles.Binding = false;
|
|
1476
|
+
options.chance.observe(.binding, player, .None);
|
|
1477
|
+
|
|
1425
1478
|
return false;
|
|
1426
1479
|
}
|
|
1427
1480
|
|
|
@@ -1659,17 +1712,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1712
|
}
|
|
1660
1713
|
}
|
|
1661
1714
|
|
|
1662
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1663
|
-
var volatiles = &active.volatiles;
|
|
1664
|
-
assert(volatiles.Thrashing);
|
|
1665
|
-
if (volatiles.attacks > 0) return false;
|
|
1666
|
-
|
|
1667
|
-
volatiles.Thrashing = false;
|
|
1668
|
-
volatiles.Confusion = true;
|
|
1669
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1670
|
-
return true;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
1715
|
fn onBegin(
|
|
1674
1716
|
battle: anytype,
|
|
1675
1717
|
player: Player,
|
|
@@ -1756,8 +1798,10 @@ pub const Effects = struct {
|
|
|
1756
1798
|
|
|
1757
1799
|
side.active.volatiles.Bide = true;
|
|
1758
1800
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1801
|
+
|
|
1759
1802
|
side.active.volatiles.state = 0;
|
|
1760
1803
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1804
|
+
options.chance.observe(.attacking, player, .started);
|
|
1761
1805
|
|
|
1762
1806
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1763
1807
|
}
|
|
@@ -1823,7 +1867,10 @@ pub const Effects = struct {
|
|
|
1823
1867
|
|
|
1824
1868
|
if (foe.active.volatiles.Confusion) return;
|
|
1825
1869
|
foe.active.volatiles.Confusion = true;
|
|
1826
|
-
|
|
1870
|
+
|
|
1871
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1872
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1873
|
+
options.calc.confusion(player.foe());
|
|
1827
1874
|
|
|
1828
1875
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1829
1876
|
}
|
|
@@ -1881,6 +1928,7 @@ pub const Effects = struct {
|
|
|
1881
1928
|
volatiles.disable_move =
|
|
1882
1929
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1883
1930
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1931
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1884
1932
|
|
|
1885
1933
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1886
1934
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1987,22 +2035,24 @@ pub const Effects = struct {
|
|
|
1987
2035
|
if (p != player and Status.any(s.stored().status)) {
|
|
1988
2036
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1989
2037
|
s.stored().status = 0;
|
|
2038
|
+
options.chance.observe(.sleep, p, .None);
|
|
1990
2039
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1991
2040
|
s.stored().status = Status.init(.PSN);
|
|
1992
2041
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1993
2042
|
}
|
|
1994
|
-
try clearVolatiles(battle, p,
|
|
2043
|
+
try clearVolatiles(battle, p, options);
|
|
1995
2044
|
}
|
|
1996
2045
|
} else {
|
|
1997
2046
|
if (Status.any(foe_stored.status)) {
|
|
1998
2047
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1999
2048
|
foe.last_selected_move = .SKIP_TURN;
|
|
2049
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2000
2050
|
}
|
|
2001
2051
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2002
2052
|
foe_stored.status = 0;
|
|
2003
2053
|
}
|
|
2004
|
-
try clearVolatiles(battle, player,
|
|
2005
|
-
try clearVolatiles(battle, player.foe(),
|
|
2054
|
+
try clearVolatiles(battle, player, options);
|
|
2055
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2006
2056
|
}
|
|
2007
2057
|
}
|
|
2008
2058
|
|
|
@@ -2298,6 +2348,8 @@ pub const Effects = struct {
|
|
|
2298
2348
|
foe.active.volatiles.Recharging = false;
|
|
2299
2349
|
|
|
2300
2350
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2351
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2352
|
+
|
|
2301
2353
|
const last = battle.side(player).last_selected_move;
|
|
2302
2354
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2303
2355
|
}
|
|
@@ -2354,6 +2406,8 @@ pub const Effects = struct {
|
|
|
2354
2406
|
|
|
2355
2407
|
volatiles.Thrashing = true;
|
|
2356
2408
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2409
|
+
|
|
2410
|
+
options.chance.observe(.attacking, player, .started);
|
|
2357
2411
|
}
|
|
2358
2412
|
|
|
2359
2413
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2399,6 +2453,7 @@ pub const Effects = struct {
|
|
|
2399
2453
|
|
|
2400
2454
|
side.active.volatiles.attacks =
|
|
2401
2455
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2456
|
+
options.chance.observe(.binding, player, .started);
|
|
2402
2457
|
}
|
|
2403
2458
|
|
|
2404
2459
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2628,22 +2683,22 @@ fn isForced(active: anytype) bool {
|
|
|
2628
2683
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2629
2684
|
}
|
|
2630
2685
|
|
|
2631
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2686
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2632
2687
|
var log = options.log;
|
|
2633
2688
|
var side = battle.side(who);
|
|
2634
2689
|
var volatiles = &side.active.volatiles;
|
|
2635
2690
|
const ident = battle.active(who);
|
|
2636
2691
|
|
|
2637
|
-
options.chance.clearDurations(who, clear);
|
|
2638
|
-
|
|
2639
2692
|
if (volatiles.disable_move != 0) {
|
|
2640
2693
|
volatiles.disable_move = 0;
|
|
2641
2694
|
volatiles.disable_duration = 0;
|
|
2695
|
+
options.chance.observe(.disable, who, .None);
|
|
2642
2696
|
try log.end(.{ ident, .DisableSilent });
|
|
2643
2697
|
}
|
|
2644
2698
|
if (volatiles.Confusion) {
|
|
2645
2699
|
// volatiles.confusion is left unchanged
|
|
2646
2700
|
volatiles.Confusion = false;
|
|
2701
|
+
options.chance.observe(.confusion, who, .None);
|
|
2647
2702
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2648
2703
|
}
|
|
2649
2704
|
if (volatiles.Mist) {
|
|
@@ -2678,6 +2733,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2678
2733
|
}
|
|
2679
2734
|
}
|
|
2680
2735
|
|
|
2736
|
+
fn decrement(
|
|
2737
|
+
comptime field: chance.Action.Field,
|
|
2738
|
+
player: Player,
|
|
2739
|
+
options: anytype,
|
|
2740
|
+
n: anytype,
|
|
2741
|
+
) @TypeOf(n) {
|
|
2742
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2743
|
+
.started, .ended => 0,
|
|
2744
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2745
|
+
else => unreachable,
|
|
2746
|
+
} else n - 1;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2681
2749
|
pub const Rolls = struct {
|
|
2682
2750
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2683
2751
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2875,9 +2943,10 @@ pub const Rolls = struct {
|
|
|
2875
2943
|
}
|
|
2876
2944
|
|
|
2877
2945
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2878
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2879
|
-
@intCast(val)
|
|
2880
|
-
else
|
|
2946
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2947
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2948
|
+
else
|
|
2949
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2881
2950
|
battle.rng.sleepDuration(player)
|
|
2882
2951
|
else if (showdown)
|
|
2883
2952
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2889,14 +2958,15 @@ pub const Rolls = struct {
|
|
|
2889
2958
|
};
|
|
2890
2959
|
|
|
2891
2960
|
assert(duration >= 1 and duration <= 7);
|
|
2892
|
-
options.chance.duration(
|
|
2961
|
+
options.chance.duration(player, duration);
|
|
2893
2962
|
return duration;
|
|
2894
2963
|
}
|
|
2895
2964
|
|
|
2896
2965
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2897
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2898
|
-
val
|
|
2899
|
-
else
|
|
2966
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2967
|
+
if (val >= 1 and val <= 8) val else null
|
|
2968
|
+
else
|
|
2969
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2900
2970
|
battle.rng.disableDuration(player)
|
|
2901
2971
|
else if (showdown)
|
|
2902
2972
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2904,29 +2974,31 @@ pub const Rolls = struct {
|
|
|
2904
2974
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2905
2975
|
|
|
2906
2976
|
assert(duration >= 1 and duration <= 8);
|
|
2907
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2908
2978
|
return duration;
|
|
2909
2979
|
}
|
|
2910
2980
|
|
|
2911
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2912
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2913
|
-
@intCast(val)
|
|
2914
|
-
else
|
|
2915
|
-
battle.rng
|
|
2981
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2982
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2983
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
2984
|
+
else
|
|
2985
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2986
|
+
battle.rng.confusionDuration(player)
|
|
2916
2987
|
else if (showdown)
|
|
2917
2988
|
battle.rng.range(u3, 2, 6)
|
|
2918
2989
|
else
|
|
2919
2990
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2920
2991
|
|
|
2921
2992
|
assert(duration >= 2 and duration <= 5);
|
|
2922
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2923
2994
|
return duration;
|
|
2924
2995
|
}
|
|
2925
2996
|
|
|
2926
2997
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2927
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2928
|
-
@intCast(val)
|
|
2929
|
-
else
|
|
2998
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3000
|
+
else
|
|
3001
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2930
3002
|
battle.rng.attackingDuration(player)
|
|
2931
3003
|
else if (showdown)
|
|
2932
3004
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2934,7 +3006,7 @@ pub const Rolls = struct {
|
|
|
2934
3006
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2935
3007
|
|
|
2936
3008
|
assert(duration >= 2 and duration <= 3);
|
|
2937
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2938
3010
|
return duration;
|
|
2939
3011
|
}
|
|
2940
3012
|
|
|
@@ -2947,9 +3019,10 @@ pub const Rolls = struct {
|
|
|
2947
3019
|
options: anytype,
|
|
2948
3020
|
) !u3 {
|
|
2949
3021
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2950
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2951
|
-
@intCast(val)
|
|
2952
|
-
else
|
|
3022
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3023
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3024
|
+
else
|
|
3025
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2953
3026
|
battle.rng.distribution(player)
|
|
2954
3027
|
else if (showdown)
|
|
2955
3028
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2960,7 +3033,7 @@ pub const Rolls = struct {
|
|
|
2960
3033
|
|
|
2961
3034
|
assert(n >= 2 and n <= 5);
|
|
2962
3035
|
if (effect == .Binding) {
|
|
2963
|
-
options.chance.duration(
|
|
3036
|
+
options.chance.duration(player, n);
|
|
2964
3037
|
} else {
|
|
2965
3038
|
try options.chance.multiHit(player, n);
|
|
2966
3039
|
}
|
|
@@ -2975,15 +3048,13 @@ pub const Rolls = struct {
|
|
|
2975
3048
|
options: anytype,
|
|
2976
3049
|
) !u4 {
|
|
2977
3050
|
// TODO: consider throwing error instead of rerolling?
|
|
2978
|
-
const
|
|
3051
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2979
3052
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2980
3053
|
val
|
|
2981
3054
|
else
|
|
2982
3055
|
null
|
|
2983
3056
|
else
|
|
2984
|
-
null
|
|
2985
|
-
|
|
2986
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3057
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2987
3058
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2988
3059
|
else slot: {
|
|
2989
3060
|
if (showdown) {
|