@pkmn/engine 0.1.0-dev.15f64810 → 0.1.0-dev.1cea12bf
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 +34 -67
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +128 -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 +35 -22
- 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/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 +113 -22
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +428 -153
- package/src/lib/gen1/chance.zig +332 -224
- package/src/lib/gen1/data.zig +28 -27
- package/src/lib/gen1/helpers.zig +12 -542
- package/src/lib/gen1/mechanics.zig +189 -120
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +90 -503
- 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 +351 -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);
|
|
@@ -347,13 +346,13 @@ fn doTurn(
|
|
|
347
346
|
)) |r| return r;
|
|
348
347
|
if (!replace) {
|
|
349
348
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
349
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
350
|
}
|
|
352
351
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
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;
|
|
@@ -369,10 +368,10 @@ fn doTurn(
|
|
|
369
368
|
)) |r| return r;
|
|
370
369
|
if (!replace) {
|
|
371
370
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
371
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
372
|
}
|
|
374
373
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
374
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -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,19 @@ 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 => volatiles.confusion -= 1,
|
|
565
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
566
|
+
volatiles.confusion -= 1;
|
|
567
|
+
},
|
|
568
|
+
.ended => volatiles.confusion = 0,
|
|
569
|
+
.overwritten => {},
|
|
570
|
+
else => unreachable,
|
|
562
571
|
} else {
|
|
563
572
|
volatiles.confusion -= 1;
|
|
564
573
|
}
|
|
565
|
-
|
|
574
|
+
|
|
575
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
576
|
|
|
567
577
|
if (volatiles.confusion == 0) {
|
|
568
578
|
volatiles.Confusion = false;
|
|
@@ -580,7 +590,9 @@ fn beforeMove(
|
|
|
580
590
|
volatiles.Charging = false;
|
|
581
591
|
volatiles.Binding = false;
|
|
582
592
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
593
|
+
|
|
594
|
+
options.chance.observe(.attacking, player, .None);
|
|
595
|
+
options.chance.observe(.binding, player, .None);
|
|
584
596
|
{
|
|
585
597
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
598
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +600,9 @@ fn beforeMove(
|
|
|
588
600
|
const def = foe.active.stats.def;
|
|
589
601
|
foe.active.stats.def = active.stats.def;
|
|
590
602
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
603
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
604
|
+
return .err;
|
|
605
|
+
}
|
|
592
606
|
}
|
|
593
607
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
608
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +631,10 @@ fn beforeMove(
|
|
|
617
631
|
volatiles.Thrashing = false;
|
|
618
632
|
volatiles.Charging = false;
|
|
619
633
|
volatiles.Binding = false;
|
|
620
|
-
|
|
634
|
+
|
|
635
|
+
options.chance.observe(.attacking, player, .None);
|
|
636
|
+
options.chance.observe(.binding, player, .None);
|
|
637
|
+
|
|
621
638
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
639
|
try log.cant(.{ ident, .Paralysis });
|
|
623
640
|
return .done;
|
|
@@ -635,12 +652,8 @@ fn beforeMove(
|
|
|
635
652
|
volatiles.state +%= battle.last_damage;
|
|
636
653
|
}
|
|
637
654
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
655
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
656
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
657
|
|
|
645
658
|
if (volatiles.attacks != 0) {
|
|
646
659
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +687,36 @@ fn beforeMove(
|
|
|
674
687
|
}
|
|
675
688
|
|
|
676
689
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
690
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
691
|
+
|
|
692
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
693
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
694
|
+
|
|
695
|
+
if (volatiles.attacks == 0) {
|
|
696
|
+
const overwritten = volatiles.Confusion;
|
|
683
697
|
|
|
684
|
-
|
|
698
|
+
volatiles.Thrashing = false;
|
|
699
|
+
volatiles.Confusion = true;
|
|
700
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
701
|
+
|
|
702
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
703
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
704
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
705
|
+
// correct player)
|
|
685
706
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
707
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
686
708
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
709
|
+
|
|
710
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
711
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
712
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
713
|
+
|
|
696
714
|
return .skip_can;
|
|
697
715
|
}
|
|
698
716
|
|
|
699
717
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
718
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
719
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
720
|
|
|
707
721
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
722
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -795,7 +809,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
809
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
810
|
}
|
|
797
811
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
812
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
813
|
var active = &side.active;
|
|
800
814
|
const volatiles = &active.volatiles;
|
|
801
815
|
|
|
@@ -803,8 +817,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
817
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
818
|
if (showdown and volatiles.Transform) return;
|
|
805
819
|
|
|
820
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
821
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
822
|
+
|
|
806
823
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
824
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
825
|
}
|
|
809
826
|
|
|
810
827
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,7 +875,7 @@ fn doMove(
|
|
|
858
875
|
|
|
859
876
|
var late = showdown and move.effect != .Explode;
|
|
860
877
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
878
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
879
|
if (showdown and move.effect.isMulti()) {
|
|
863
880
|
try Effects.multiHit(battle, player, move, options);
|
|
864
881
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +897,9 @@ fn doMove(
|
|
|
880
897
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
898
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
899
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
900
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
901
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
902
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
903
|
return @as(?Result, Result.Error);
|
|
886
904
|
}
|
|
887
905
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +913,12 @@ fn doMove(
|
|
|
895
913
|
}
|
|
896
914
|
}
|
|
897
915
|
|
|
916
|
+
var missed: bool = undefined;
|
|
898
917
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
918
|
+
if (!showdown and !skip) {
|
|
919
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
920
|
+
miss = missed or zero;
|
|
921
|
+
}
|
|
903
922
|
|
|
904
923
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
924
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +945,32 @@ fn doMove(
|
|
|
926
945
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
946
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
947
|
try log.immune(.{ foe_ident, .None });
|
|
948
|
+
if (!showdown) {
|
|
949
|
+
if (move.effect == .Binding) {
|
|
950
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
951
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
952
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
953
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
954
|
+
// (considered missing)
|
|
955
|
+
assert(zero);
|
|
956
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
957
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
958
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
959
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
960
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
961
|
+
// roll)
|
|
962
|
+
try options.chance.commit(player, .binding);
|
|
963
|
+
} else {
|
|
964
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
965
|
+
// division-by-zero freeze alternative timeline scenario
|
|
966
|
+
try options.chance.commit(player, .glitch);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
929
969
|
} else if (mist) {
|
|
930
970
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
971
|
try log.fail(.{ foe_ident, .None });
|
|
932
972
|
} else {
|
|
933
|
-
if (!showdown
|
|
973
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
974
|
try log.lastmiss(.{});
|
|
935
975
|
try log.miss(.{battle.active(player)});
|
|
936
976
|
}
|
|
@@ -945,8 +985,6 @@ fn doMove(
|
|
|
945
985
|
try buildRage(battle, player.foe(), options);
|
|
946
986
|
} else if (showdown and move.effect == .Disable) {
|
|
947
987
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
988
|
}
|
|
951
989
|
return null;
|
|
952
990
|
} else if (!showdown) {
|
|
@@ -1069,6 +1107,7 @@ fn calcDamage(
|
|
|
1069
1107
|
target_player: Player,
|
|
1070
1108
|
m: ?Move.Data,
|
|
1071
1109
|
crit: bool,
|
|
1110
|
+
options: anytype,
|
|
1072
1111
|
) bool {
|
|
1073
1112
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1113
|
const cfz = m == null;
|
|
@@ -1080,28 +1119,8 @@ fn calcDamage(
|
|
|
1080
1119
|
|
|
1081
1120
|
const special = move.type.special();
|
|
1082
1121
|
|
|
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
|
|
1122
|
+
var atk = attack(side, crit, special);
|
|
1123
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1124
|
|
|
1106
1125
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1126
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1126,11 +1145,41 @@ fn calcDamage(
|
|
|
1126
1145
|
d = @min(997, d);
|
|
1127
1146
|
d += 2;
|
|
1128
1147
|
|
|
1148
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1149
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1150
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1151
|
+
// that results in counter intuitive probabilities in the common case
|
|
1152
|
+
if (pkmn.options.chance and !showdown) {
|
|
1153
|
+
atk = attack(side, !crit, special);
|
|
1154
|
+
def = defense(target, !crit, special, false);
|
|
1155
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1129
1158
|
battle.last_damage = @intCast(d);
|
|
1130
1159
|
|
|
1131
1160
|
return true;
|
|
1132
1161
|
}
|
|
1133
1162
|
|
|
1163
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1164
|
+
return if (crit)
|
|
1165
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1166
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1170
|
+
return if (crit)
|
|
1171
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1172
|
+
else
|
|
1173
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1174
|
+
if (special)
|
|
1175
|
+
target.active.stats.spc *
|
|
1176
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1177
|
+
else
|
|
1178
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1179
|
+
target.active.stats.def *
|
|
1180
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1134
1183
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1184
|
const side = battle.side(player);
|
|
1136
1185
|
const foe = battle.foe(player);
|
|
@@ -1265,7 +1314,7 @@ fn applyDamage(
|
|
|
1265
1314
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1315
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1316
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1317
|
+
options.calc.capped(sub_player);
|
|
1269
1318
|
return true;
|
|
1270
1319
|
} else {
|
|
1271
1320
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1348,7 @@ fn mirrorMove(
|
|
|
1299
1348
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1349
|
return null;
|
|
1301
1350
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1351
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1352
|
}
|
|
1304
1353
|
|
|
1305
1354
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1369,7 @@ fn metronome(
|
|
|
1320
1369
|
var side = battle.side(player);
|
|
1321
1370
|
|
|
1322
1371
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1372
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1373
|
|
|
1325
1374
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1375
|
return null;
|
|
@@ -1421,13 +1470,17 @@ fn moveHit(
|
|
|
1421
1470
|
|
|
1422
1471
|
if (!miss) return true;
|
|
1423
1472
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1473
|
+
|
|
1424
1474
|
side.active.volatiles.Binding = false;
|
|
1475
|
+
options.chance.observe(.binding, player, .None);
|
|
1476
|
+
|
|
1425
1477
|
return false;
|
|
1426
1478
|
}
|
|
1427
1479
|
|
|
1428
1480
|
fn checkFaint(
|
|
1429
1481
|
battle: anytype,
|
|
1430
1482
|
player: Player,
|
|
1483
|
+
cap: bool,
|
|
1431
1484
|
options: anytype,
|
|
1432
1485
|
) @TypeOf(options.log).Error!?Result {
|
|
1433
1486
|
const side = battle.side(player);
|
|
@@ -1440,8 +1493,8 @@ fn checkFaint(
|
|
|
1440
1493
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1441
1494
|
const more = player_out or foe_out;
|
|
1442
1495
|
|
|
1443
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1444
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1496
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1497
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1445
1498
|
|
|
1446
1499
|
assert(!side.active.volatiles.MultiHit);
|
|
1447
1500
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1462,7 +1515,7 @@ fn checkFaint(
|
|
|
1462
1515
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1463
1516
|
}
|
|
1464
1517
|
|
|
1465
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1518
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1466
1519
|
var side = battle.side(player);
|
|
1467
1520
|
var foe = battle.foe(player);
|
|
1468
1521
|
assert(side.stored().hp == 0);
|
|
@@ -1496,7 +1549,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1496
1549
|
}
|
|
1497
1550
|
|
|
1498
1551
|
try options.log.faint(.{ battle.active(player), done });
|
|
1499
|
-
|
|
1552
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1553
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1554
|
+
if (cap) options.calc.capped(player);
|
|
1500
1555
|
return null;
|
|
1501
1556
|
}
|
|
1502
1557
|
|
|
@@ -1656,17 +1711,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1656
1711
|
}
|
|
1657
1712
|
}
|
|
1658
1713
|
|
|
1659
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1660
|
-
var volatiles = &active.volatiles;
|
|
1661
|
-
assert(volatiles.Thrashing);
|
|
1662
|
-
if (volatiles.attacks > 0) return false;
|
|
1663
|
-
|
|
1664
|
-
volatiles.Thrashing = false;
|
|
1665
|
-
volatiles.Confusion = true;
|
|
1666
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1667
|
-
return true;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1714
|
fn onBegin(
|
|
1671
1715
|
battle: anytype,
|
|
1672
1716
|
player: Player,
|
|
@@ -1753,8 +1797,10 @@ pub const Effects = struct {
|
|
|
1753
1797
|
|
|
1754
1798
|
side.active.volatiles.Bide = true;
|
|
1755
1799
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1800
|
+
|
|
1756
1801
|
side.active.volatiles.state = 0;
|
|
1757
1802
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1803
|
+
options.chance.observe(.attacking, player, .started);
|
|
1758
1804
|
|
|
1759
1805
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1760
1806
|
}
|
|
@@ -1820,7 +1866,9 @@ pub const Effects = struct {
|
|
|
1820
1866
|
|
|
1821
1867
|
if (foe.active.volatiles.Confusion) return;
|
|
1822
1868
|
foe.active.volatiles.Confusion = true;
|
|
1823
|
-
|
|
1869
|
+
|
|
1870
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1871
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1824
1872
|
|
|
1825
1873
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1826
1874
|
}
|
|
@@ -1878,6 +1926,7 @@ pub const Effects = struct {
|
|
|
1878
1926
|
volatiles.disable_move =
|
|
1879
1927
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1880
1928
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1929
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1881
1930
|
|
|
1882
1931
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1883
1932
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1984,22 +2033,24 @@ pub const Effects = struct {
|
|
|
1984
2033
|
if (p != player and Status.any(s.stored().status)) {
|
|
1985
2034
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1986
2035
|
s.stored().status = 0;
|
|
2036
|
+
options.chance.observe(.sleep, p, .None);
|
|
1987
2037
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1988
2038
|
s.stored().status = Status.init(.PSN);
|
|
1989
2039
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1990
2040
|
}
|
|
1991
|
-
try clearVolatiles(battle, p,
|
|
2041
|
+
try clearVolatiles(battle, p, options);
|
|
1992
2042
|
}
|
|
1993
2043
|
} else {
|
|
1994
2044
|
if (Status.any(foe_stored.status)) {
|
|
1995
2045
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1996
2046
|
foe.last_selected_move = .SKIP_TURN;
|
|
2047
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1997
2048
|
}
|
|
1998
2049
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1999
2050
|
foe_stored.status = 0;
|
|
2000
2051
|
}
|
|
2001
|
-
try clearVolatiles(battle, player,
|
|
2002
|
-
try clearVolatiles(battle, player.foe(),
|
|
2052
|
+
try clearVolatiles(battle, player, options);
|
|
2053
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2003
2054
|
}
|
|
2004
2055
|
}
|
|
2005
2056
|
|
|
@@ -2295,6 +2346,8 @@ pub const Effects = struct {
|
|
|
2295
2346
|
foe.active.volatiles.Recharging = false;
|
|
2296
2347
|
|
|
2297
2348
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2349
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2350
|
+
|
|
2298
2351
|
const last = battle.side(player).last_selected_move;
|
|
2299
2352
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2300
2353
|
}
|
|
@@ -2351,6 +2404,8 @@ pub const Effects = struct {
|
|
|
2351
2404
|
|
|
2352
2405
|
volatiles.Thrashing = true;
|
|
2353
2406
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2407
|
+
|
|
2408
|
+
options.chance.observe(.attacking, player, .started);
|
|
2354
2409
|
}
|
|
2355
2410
|
|
|
2356
2411
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2396,6 +2451,7 @@ pub const Effects = struct {
|
|
|
2396
2451
|
|
|
2397
2452
|
side.active.volatiles.attacks =
|
|
2398
2453
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2454
|
+
options.chance.observe(.binding, player, .started);
|
|
2399
2455
|
}
|
|
2400
2456
|
|
|
2401
2457
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2625,22 +2681,22 @@ fn isForced(active: anytype) bool {
|
|
|
2625
2681
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2626
2682
|
}
|
|
2627
2683
|
|
|
2628
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2684
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2629
2685
|
var log = options.log;
|
|
2630
2686
|
var side = battle.side(who);
|
|
2631
2687
|
var volatiles = &side.active.volatiles;
|
|
2632
2688
|
const ident = battle.active(who);
|
|
2633
2689
|
|
|
2634
|
-
options.chance.clearDurations(who, clear);
|
|
2635
|
-
|
|
2636
2690
|
if (volatiles.disable_move != 0) {
|
|
2637
2691
|
volatiles.disable_move = 0;
|
|
2638
2692
|
volatiles.disable_duration = 0;
|
|
2693
|
+
options.chance.observe(.disable, who, .None);
|
|
2639
2694
|
try log.end(.{ ident, .DisableSilent });
|
|
2640
2695
|
}
|
|
2641
2696
|
if (volatiles.Confusion) {
|
|
2642
2697
|
// volatiles.confusion is left unchanged
|
|
2643
2698
|
volatiles.Confusion = false;
|
|
2699
|
+
options.chance.observe(.confusion, who, .None);
|
|
2644
2700
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2645
2701
|
}
|
|
2646
2702
|
if (volatiles.Mist) {
|
|
@@ -2675,6 +2731,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2675
2731
|
}
|
|
2676
2732
|
}
|
|
2677
2733
|
|
|
2734
|
+
fn decrement(
|
|
2735
|
+
comptime field: chance.Action.Field,
|
|
2736
|
+
player: Player,
|
|
2737
|
+
options: anytype,
|
|
2738
|
+
n: anytype,
|
|
2739
|
+
) @TypeOf(n) {
|
|
2740
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2741
|
+
.started, .ended => 0,
|
|
2742
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2743
|
+
else => unreachable,
|
|
2744
|
+
} else n - 1;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2678
2747
|
pub const Rolls = struct {
|
|
2679
2748
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2680
2749
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2886,7 +2955,7 @@ pub const Rolls = struct {
|
|
|
2886
2955
|
};
|
|
2887
2956
|
|
|
2888
2957
|
assert(duration >= 1 and duration <= 7);
|
|
2889
|
-
options.chance.duration(
|
|
2958
|
+
options.chance.duration(player, duration);
|
|
2890
2959
|
return duration;
|
|
2891
2960
|
}
|
|
2892
2961
|
|
|
@@ -2901,22 +2970,22 @@ pub const Rolls = struct {
|
|
|
2901
2970
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2902
2971
|
|
|
2903
2972
|
assert(duration >= 1 and duration <= 8);
|
|
2904
|
-
options.chance.duration(
|
|
2973
|
+
options.chance.duration(player, duration);
|
|
2905
2974
|
return duration;
|
|
2906
2975
|
}
|
|
2907
2976
|
|
|
2908
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2977
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2909
2978
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2910
2979
|
@intCast(val)
|
|
2911
2980
|
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2912
|
-
battle.rng.confusionDuration(player
|
|
2981
|
+
battle.rng.confusionDuration(player)
|
|
2913
2982
|
else if (showdown)
|
|
2914
2983
|
battle.rng.range(u3, 2, 6)
|
|
2915
2984
|
else
|
|
2916
2985
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2917
2986
|
|
|
2918
2987
|
assert(duration >= 2 and duration <= 5);
|
|
2919
|
-
options.chance.duration(
|
|
2988
|
+
options.chance.duration(player, duration);
|
|
2920
2989
|
return duration;
|
|
2921
2990
|
}
|
|
2922
2991
|
|
|
@@ -2931,7 +3000,7 @@ pub const Rolls = struct {
|
|
|
2931
3000
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2932
3001
|
|
|
2933
3002
|
assert(duration >= 2 and duration <= 3);
|
|
2934
|
-
options.chance.duration(
|
|
3003
|
+
options.chance.duration(player, duration);
|
|
2935
3004
|
return duration;
|
|
2936
3005
|
}
|
|
2937
3006
|
|
|
@@ -2957,7 +3026,7 @@ pub const Rolls = struct {
|
|
|
2957
3026
|
|
|
2958
3027
|
assert(n >= 2 and n <= 5);
|
|
2959
3028
|
if (effect == .Binding) {
|
|
2960
|
-
options.chance.duration(
|
|
3029
|
+
options.chance.duration(player, n);
|
|
2961
3030
|
} else {
|
|
2962
3031
|
try options.chance.multiHit(player, n);
|
|
2963
3032
|
}
|