@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.6be9a775
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/index.d.ts +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/test/display.js +6 -1
- package/build/test/display.js.map +1 -1
- package/build.zig +199 -66
- package/package.json +4 -1
- package/src/bin/install-pkmn-engine +7 -5
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/protocol.zig +41 -36
- package/src/lib/common/rational.zig +46 -46
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +329 -119
- package/src/lib/gen1/chance.zig +332 -224
- package/src/lib/gen1/data.zig +28 -27
- package/src/lib/gen1/helpers.zig +13 -534
- package/src/lib/gen1/mechanics.zig +178 -109
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +91 -501
- package/src/lib/gen2/data.zig +15 -14
- package/src/lib/gen2/helpers.zig +4 -2
- package/src/lib/gen2/mechanics.zig +47 -71
- package/src/lib/pkmn.zig +7 -5
|
@@ -4,10 +4,12 @@ const pkmn = @import("../pkmn.zig");
|
|
|
4
4
|
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
6
|
const DEBUG = @import("../common/debug.zig").print;
|
|
7
|
+
const optional = @import("../common/optional.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
10
|
|
|
10
11
|
const data = @import("data.zig");
|
|
12
|
+
const chance = @import("chance.zig");
|
|
11
13
|
|
|
12
14
|
const assert = std.debug.assert;
|
|
13
15
|
|
|
@@ -253,11 +255,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
255
|
active.moves = incoming.moves;
|
|
254
256
|
|
|
255
257
|
statusModify(incoming.status, &active.stats);
|
|
258
|
+
options.chance.switched(player, slot);
|
|
256
259
|
|
|
257
260
|
foe.active.volatiles.Binding = false;
|
|
258
261
|
|
|
259
262
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
263
|
|
|
262
264
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
265
|
incoming.status = Status.init(.PSN);
|
|
@@ -353,7 +355,7 @@ fn doTurn(
|
|
|
353
355
|
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
356
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
357
|
|
|
356
|
-
options.chance.clearPending();
|
|
358
|
+
if (!showdown) options.chance.clearPending();
|
|
357
359
|
|
|
358
360
|
residual = true;
|
|
359
361
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -484,17 +486,22 @@ fn beforeMove(
|
|
|
484
486
|
|
|
485
487
|
if (Status.is(stored.status, .SLP)) {
|
|
486
488
|
const before = stored.status;
|
|
489
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
490
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
491
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
492
|
+
.started, .ended => stored.status = 0,
|
|
493
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
494
|
+
stored.status -= 1;
|
|
495
|
+
},
|
|
496
|
+
else => unreachable,
|
|
490
497
|
} else {
|
|
491
498
|
stored.status -= 1;
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
502
|
+
try options.chance.sleep(player, if (slf)
|
|
503
|
+
.None
|
|
504
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
505
|
|
|
499
506
|
if (duration == 0) {
|
|
500
507
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +539,12 @@ fn beforeMove(
|
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
volatiles.disable_duration =
|
|
543
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
544
|
+
try options.chance.disable(
|
|
545
|
+
player,
|
|
546
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
547
|
+
);
|
|
541
548
|
|
|
542
549
|
if (volatiles.disable_duration == 0) {
|
|
543
550
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +563,19 @@ fn beforeMove(
|
|
|
556
563
|
|
|
557
564
|
if (volatiles.Confusion) {
|
|
558
565
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
566
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
+
.started => volatiles.confusion -= 1,
|
|
568
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
569
|
+
volatiles.confusion -= 1;
|
|
570
|
+
},
|
|
571
|
+
.ended => volatiles.confusion = 0,
|
|
572
|
+
.overwritten => {},
|
|
573
|
+
else => unreachable,
|
|
562
574
|
} else {
|
|
563
575
|
volatiles.confusion -= 1;
|
|
564
576
|
}
|
|
565
|
-
|
|
577
|
+
|
|
578
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
579
|
|
|
567
580
|
if (volatiles.confusion == 0) {
|
|
568
581
|
volatiles.Confusion = false;
|
|
@@ -580,7 +593,9 @@ fn beforeMove(
|
|
|
580
593
|
volatiles.Charging = false;
|
|
581
594
|
volatiles.Binding = false;
|
|
582
595
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
596
|
+
|
|
597
|
+
options.chance.observe(.attacking, player, .None);
|
|
598
|
+
options.chance.observe(.binding, player, .None);
|
|
584
599
|
{
|
|
585
600
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
601
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +603,9 @@ fn beforeMove(
|
|
|
588
603
|
const def = foe.active.stats.def;
|
|
589
604
|
foe.active.stats.def = active.stats.def;
|
|
590
605
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
606
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
607
|
+
return .err;
|
|
608
|
+
}
|
|
592
609
|
}
|
|
593
610
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
611
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +634,10 @@ fn beforeMove(
|
|
|
617
634
|
volatiles.Thrashing = false;
|
|
618
635
|
volatiles.Charging = false;
|
|
619
636
|
volatiles.Binding = false;
|
|
620
|
-
|
|
637
|
+
|
|
638
|
+
options.chance.observe(.attacking, player, .None);
|
|
639
|
+
options.chance.observe(.binding, player, .None);
|
|
640
|
+
|
|
621
641
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
642
|
try log.cant(.{ ident, .Paralysis });
|
|
623
643
|
return .done;
|
|
@@ -635,12 +655,8 @@ fn beforeMove(
|
|
|
635
655
|
volatiles.state +%= battle.last_damage;
|
|
636
656
|
}
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
658
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
659
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
660
|
|
|
645
661
|
if (volatiles.attacks != 0) {
|
|
646
662
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +690,36 @@ fn beforeMove(
|
|
|
674
690
|
}
|
|
675
691
|
|
|
676
692
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
693
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
694
|
+
|
|
695
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
696
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
697
|
+
|
|
698
|
+
if (volatiles.attacks == 0) {
|
|
699
|
+
const overwritten = volatiles.Confusion;
|
|
683
700
|
|
|
684
|
-
|
|
701
|
+
volatiles.Thrashing = false;
|
|
702
|
+
volatiles.Confusion = true;
|
|
703
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
704
|
+
|
|
705
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
706
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
707
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
708
|
+
// correct player)
|
|
685
709
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
710
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
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) {
|
|
@@ -1069,6 +1110,7 @@ fn calcDamage(
|
|
|
1069
1110
|
target_player: Player,
|
|
1070
1111
|
m: ?Move.Data,
|
|
1071
1112
|
crit: bool,
|
|
1113
|
+
options: anytype,
|
|
1072
1114
|
) bool {
|
|
1073
1115
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1116
|
const cfz = m == null;
|
|
@@ -1080,28 +1122,8 @@ fn calcDamage(
|
|
|
1080
1122
|
|
|
1081
1123
|
const special = move.type.special();
|
|
1082
1124
|
|
|
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
|
|
1125
|
+
var atk = attack(side, crit, special);
|
|
1126
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1127
|
|
|
1106
1128
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1129
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1126,11 +1148,41 @@ fn calcDamage(
|
|
|
1126
1148
|
d = @min(997, d);
|
|
1127
1149
|
d += 2;
|
|
1128
1150
|
|
|
1151
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1152
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1153
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1154
|
+
// that results in counter intuitive probabilities in the common case
|
|
1155
|
+
if (pkmn.options.chance and !showdown) {
|
|
1156
|
+
atk = attack(side, !crit, special);
|
|
1157
|
+
def = defense(target, !crit, special, false);
|
|
1158
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1129
1161
|
battle.last_damage = @intCast(d);
|
|
1130
1162
|
|
|
1131
1163
|
return true;
|
|
1132
1164
|
}
|
|
1133
1165
|
|
|
1166
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1167
|
+
return if (crit)
|
|
1168
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1169
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1173
|
+
return if (crit)
|
|
1174
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1175
|
+
else
|
|
1176
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1177
|
+
if (special)
|
|
1178
|
+
target.active.stats.spc *
|
|
1179
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1180
|
+
else
|
|
1181
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1182
|
+
target.active.stats.def *
|
|
1183
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1134
1186
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1187
|
const side = battle.side(player);
|
|
1136
1188
|
const foe = battle.foe(player);
|
|
@@ -1299,7 +1351,7 @@ fn mirrorMove(
|
|
|
1299
1351
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1352
|
return null;
|
|
1301
1353
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1354
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1355
|
}
|
|
1304
1356
|
|
|
1305
1357
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1372,7 @@ fn metronome(
|
|
|
1320
1372
|
var side = battle.side(player);
|
|
1321
1373
|
|
|
1322
1374
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1375
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1376
|
|
|
1325
1377
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1378
|
return null;
|
|
@@ -1421,7 +1473,10 @@ fn moveHit(
|
|
|
1421
1473
|
|
|
1422
1474
|
if (!miss) return true;
|
|
1423
1475
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1476
|
+
|
|
1424
1477
|
side.active.volatiles.Binding = false;
|
|
1478
|
+
options.chance.observe(.binding, player, .None);
|
|
1479
|
+
|
|
1425
1480
|
return false;
|
|
1426
1481
|
}
|
|
1427
1482
|
|
|
@@ -1659,17 +1714,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1714
|
}
|
|
1660
1715
|
}
|
|
1661
1716
|
|
|
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
1717
|
fn onBegin(
|
|
1674
1718
|
battle: anytype,
|
|
1675
1719
|
player: Player,
|
|
@@ -1756,8 +1800,10 @@ pub const Effects = struct {
|
|
|
1756
1800
|
|
|
1757
1801
|
side.active.volatiles.Bide = true;
|
|
1758
1802
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1803
|
+
|
|
1759
1804
|
side.active.volatiles.state = 0;
|
|
1760
1805
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1806
|
+
options.chance.observe(.attacking, player, .started);
|
|
1761
1807
|
|
|
1762
1808
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1763
1809
|
}
|
|
@@ -1823,7 +1869,9 @@ pub const Effects = struct {
|
|
|
1823
1869
|
|
|
1824
1870
|
if (foe.active.volatiles.Confusion) return;
|
|
1825
1871
|
foe.active.volatiles.Confusion = true;
|
|
1826
|
-
|
|
1872
|
+
|
|
1873
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1874
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1827
1875
|
|
|
1828
1876
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1829
1877
|
}
|
|
@@ -1881,6 +1929,7 @@ pub const Effects = struct {
|
|
|
1881
1929
|
volatiles.disable_move =
|
|
1882
1930
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1883
1931
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1932
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1884
1933
|
|
|
1885
1934
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1886
1935
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1987,22 +2036,24 @@ pub const Effects = struct {
|
|
|
1987
2036
|
if (p != player and Status.any(s.stored().status)) {
|
|
1988
2037
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1989
2038
|
s.stored().status = 0;
|
|
2039
|
+
options.chance.observe(.sleep, p, .None);
|
|
1990
2040
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1991
2041
|
s.stored().status = Status.init(.PSN);
|
|
1992
2042
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1993
2043
|
}
|
|
1994
|
-
try clearVolatiles(battle, p,
|
|
2044
|
+
try clearVolatiles(battle, p, options);
|
|
1995
2045
|
}
|
|
1996
2046
|
} else {
|
|
1997
2047
|
if (Status.any(foe_stored.status)) {
|
|
1998
2048
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1999
2049
|
foe.last_selected_move = .SKIP_TURN;
|
|
2050
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2000
2051
|
}
|
|
2001
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2002
2053
|
foe_stored.status = 0;
|
|
2003
2054
|
}
|
|
2004
|
-
try clearVolatiles(battle, player,
|
|
2005
|
-
try clearVolatiles(battle, player.foe(),
|
|
2055
|
+
try clearVolatiles(battle, player, options);
|
|
2056
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2006
2057
|
}
|
|
2007
2058
|
}
|
|
2008
2059
|
|
|
@@ -2298,6 +2349,8 @@ pub const Effects = struct {
|
|
|
2298
2349
|
foe.active.volatiles.Recharging = false;
|
|
2299
2350
|
|
|
2300
2351
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2352
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2353
|
+
|
|
2301
2354
|
const last = battle.side(player).last_selected_move;
|
|
2302
2355
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2303
2356
|
}
|
|
@@ -2354,6 +2407,8 @@ pub const Effects = struct {
|
|
|
2354
2407
|
|
|
2355
2408
|
volatiles.Thrashing = true;
|
|
2356
2409
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2410
|
+
|
|
2411
|
+
options.chance.observe(.attacking, player, .started);
|
|
2357
2412
|
}
|
|
2358
2413
|
|
|
2359
2414
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2399,6 +2454,7 @@ pub const Effects = struct {
|
|
|
2399
2454
|
|
|
2400
2455
|
side.active.volatiles.attacks =
|
|
2401
2456
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2457
|
+
options.chance.observe(.binding, player, .started);
|
|
2402
2458
|
}
|
|
2403
2459
|
|
|
2404
2460
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2628,22 +2684,22 @@ fn isForced(active: anytype) bool {
|
|
|
2628
2684
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2629
2685
|
}
|
|
2630
2686
|
|
|
2631
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2687
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2632
2688
|
var log = options.log;
|
|
2633
2689
|
var side = battle.side(who);
|
|
2634
2690
|
var volatiles = &side.active.volatiles;
|
|
2635
2691
|
const ident = battle.active(who);
|
|
2636
2692
|
|
|
2637
|
-
options.chance.clearDurations(who, clear);
|
|
2638
|
-
|
|
2639
2693
|
if (volatiles.disable_move != 0) {
|
|
2640
2694
|
volatiles.disable_move = 0;
|
|
2641
2695
|
volatiles.disable_duration = 0;
|
|
2696
|
+
options.chance.observe(.disable, who, .None);
|
|
2642
2697
|
try log.end(.{ ident, .DisableSilent });
|
|
2643
2698
|
}
|
|
2644
2699
|
if (volatiles.Confusion) {
|
|
2645
2700
|
// volatiles.confusion is left unchanged
|
|
2646
2701
|
volatiles.Confusion = false;
|
|
2702
|
+
options.chance.observe(.confusion, who, .None);
|
|
2647
2703
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2648
2704
|
}
|
|
2649
2705
|
if (volatiles.Mist) {
|
|
@@ -2678,6 +2734,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2678
2734
|
}
|
|
2679
2735
|
}
|
|
2680
2736
|
|
|
2737
|
+
fn decrement(
|
|
2738
|
+
comptime field: chance.Action.Field,
|
|
2739
|
+
player: Player,
|
|
2740
|
+
options: anytype,
|
|
2741
|
+
n: anytype,
|
|
2742
|
+
) @TypeOf(n) {
|
|
2743
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2744
|
+
.started, .ended => 0,
|
|
2745
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2746
|
+
else => unreachable,
|
|
2747
|
+
} else n - 1;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2681
2750
|
pub const Rolls = struct {
|
|
2682
2751
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2683
2752
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2889,7 +2958,7 @@ 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
|
|
|
@@ -2904,22 +2973,22 @@ pub const Rolls = struct {
|
|
|
2904
2973
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2905
2974
|
|
|
2906
2975
|
assert(duration >= 1 and duration <= 8);
|
|
2907
|
-
options.chance.duration(
|
|
2976
|
+
options.chance.duration(player, duration);
|
|
2908
2977
|
return duration;
|
|
2909
2978
|
}
|
|
2910
2979
|
|
|
2911
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2980
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2912
2981
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2913
2982
|
@intCast(val)
|
|
2914
2983
|
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2915
|
-
battle.rng.confusionDuration(player
|
|
2984
|
+
battle.rng.confusionDuration(player)
|
|
2916
2985
|
else if (showdown)
|
|
2917
2986
|
battle.rng.range(u3, 2, 6)
|
|
2918
2987
|
else
|
|
2919
2988
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2920
2989
|
|
|
2921
2990
|
assert(duration >= 2 and duration <= 5);
|
|
2922
|
-
options.chance.duration(
|
|
2991
|
+
options.chance.duration(player, duration);
|
|
2923
2992
|
return duration;
|
|
2924
2993
|
}
|
|
2925
2994
|
|
|
@@ -2934,7 +3003,7 @@ pub const Rolls = struct {
|
|
|
2934
3003
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2935
3004
|
|
|
2936
3005
|
assert(duration >= 2 and duration <= 3);
|
|
2937
|
-
options.chance.duration(
|
|
3006
|
+
options.chance.duration(player, duration);
|
|
2938
3007
|
return duration;
|
|
2939
3008
|
}
|
|
2940
3009
|
|
|
@@ -2960,7 +3029,7 @@ pub const Rolls = struct {
|
|
|
2960
3029
|
|
|
2961
3030
|
assert(n >= 2 and n <= 5);
|
|
2962
3031
|
if (effect == .Binding) {
|
|
2963
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, n);
|
|
2964
3033
|
} else {
|
|
2965
3034
|
try options.chance.multiHit(player, n);
|
|
2966
3035
|
}
|