@pkmn/engine 0.1.0-dev.09d1baec → 0.1.0-dev.0cabf16e
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/LICENSE +0 -2
- package/README.md +15 -11
- 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 +35 -58
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +25 -4
- 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 +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -70
- 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 +187 -72
- package/package.json +48 -32
- package/src/bin/install-pkmn-engine +17 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +4 -0
- package/src/lib/common/protocol.zig +51 -44
- package/src/lib/common/rational.zig +114 -24
- package/src/lib/common/rng.zig +10 -10
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +671 -125
- package/src/lib/gen1/chance.zig +384 -275
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data.zig +31 -30
- package/src/lib/gen1/helpers.zig +89 -538
- package/src/lib/gen1/mechanics.zig +297 -189
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +90 -502
- package/src/lib/gen2/data.zig +15 -14
- package/src/lib/gen2/helpers.zig +41 -13
- package/src/lib/gen2/mechanics.zig +158 -103
- package/src/lib/pkmn.zig +10 -16
- 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/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
- 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;
|
|
@@ -166,7 +165,7 @@ fn selectMove(
|
|
|
166
165
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
166
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
167
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
168
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
169
|
saveMove(battle, player, choice);
|
|
171
170
|
} else {
|
|
172
171
|
assert(choice.data == 0);
|
|
@@ -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
|
|
@@ -431,7 +430,7 @@ fn executeMove(
|
|
|
431
430
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
431
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
432
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
434
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
433
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
434
|
if (!auto) {
|
|
436
435
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
436
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -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
|
-
if (!extend) volatiles.attacks = 0;
|
|
679
|
-
} else {
|
|
680
|
-
volatiles.attacks -= 1;
|
|
681
|
-
}
|
|
682
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
689
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
683
690
|
|
|
684
|
-
|
|
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;
|
|
696
|
+
|
|
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
|
|
@@ -852,13 +872,13 @@ fn doMove(
|
|
|
852
872
|
break :miss true;
|
|
853
873
|
}
|
|
854
874
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
875
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
876
|
};
|
|
857
877
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
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,17 +988,15 @@ 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
|
-
} else {
|
|
993
|
+
} else if (!showdown) {
|
|
953
994
|
try options.chance.commit(player, .hit);
|
|
954
995
|
}
|
|
955
996
|
|
|
956
997
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
998
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
999
|
+
// it up above before damage calculation)
|
|
959
1000
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1001
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1002
|
hits = side.active.volatiles.attacks;
|
|
@@ -994,32 +1035,30 @@ 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
|
|
|
1016
1055
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1017
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1056
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1057
|
if (nullified) return null;
|
|
1019
1058
|
|
|
1020
1059
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1060
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1022
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1061
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1062
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1063
|
|
|
1025
1064
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1073,7 @@ fn doMove(
|
|
|
1034
1073
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1074
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1075
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1076
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1077
|
if (move.effect == .Twineedle) {
|
|
1039
1078
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1079
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1097,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1097
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1098
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1099
|
|
|
1061
|
-
|
|
1100
|
+
const spurious =
|
|
1101
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1102
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1103
|
}
|
|
1063
1104
|
|
|
1064
1105
|
fn calcDamage(
|
|
@@ -1067,6 +1108,7 @@ fn calcDamage(
|
|
|
1067
1108
|
target_player: Player,
|
|
1068
1109
|
m: ?Move.Data,
|
|
1069
1110
|
crit: bool,
|
|
1111
|
+
options: anytype,
|
|
1070
1112
|
) bool {
|
|
1071
1113
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1114
|
const cfz = m == null;
|
|
@@ -1078,28 +1120,8 @@ fn calcDamage(
|
|
|
1078
1120
|
|
|
1079
1121
|
const special = move.type.special();
|
|
1080
1122
|
|
|
1081
|
-
|
|
1082
|
-
var
|
|
1083
|
-
if (crit)
|
|
1084
|
-
if (special) side.stored().stats.spc
|
|
1085
|
-
else side.stored().stats.atk
|
|
1086
|
-
else
|
|
1087
|
-
if (special) side.active.stats.spc
|
|
1088
|
-
else side.active.stats.atk;
|
|
1089
|
-
var def: u32 =
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) target.stored().stats.spc
|
|
1092
|
-
else target.stored().stats.def
|
|
1093
|
-
else
|
|
1094
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1095
|
-
if (special)
|
|
1096
|
-
target.active.stats.spc *
|
|
1097
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1098
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1099
|
-
else
|
|
1100
|
-
target.active.stats.def *
|
|
1101
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1102
|
-
// zig fmt: on
|
|
1123
|
+
var atk = attack(side, crit, special);
|
|
1124
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1125
|
|
|
1104
1126
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1127
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1124,11 +1146,41 @@ fn calcDamage(
|
|
|
1124
1146
|
d = @min(997, d);
|
|
1125
1147
|
d += 2;
|
|
1126
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
|
+
|
|
1127
1159
|
battle.last_damage = @intCast(d);
|
|
1128
1160
|
|
|
1129
1161
|
return true;
|
|
1130
1162
|
}
|
|
1131
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
|
+
|
|
1132
1184
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1185
|
const side = battle.side(player);
|
|
1134
1186
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1225,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1225
|
const foe = battle.foe(player);
|
|
1174
1226
|
|
|
1175
1227
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1228
|
|
|
1178
1229
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1230
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1234,9 +1285,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1285
|
|
|
1235
1286
|
battle.last_damage *|= 2;
|
|
1236
1287
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1288
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1289
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1290
|
|
|
1241
1291
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1292
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -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(
|
|
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;
|
|
@@ -1332,7 +1382,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1382
|
var immune = false;
|
|
1333
1383
|
var mist = false;
|
|
1334
1384
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1385
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1386
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1336
1389
|
|
|
1337
1390
|
assert(!immune);
|
|
1338
1391
|
if (mist) {
|
|
@@ -1341,11 +1394,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1394
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1395
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1396
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1397
|
try options.log.lastmiss(.{});
|
|
1346
1398
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1399
|
}
|
|
1348
1400
|
|
|
1401
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1402
|
return false;
|
|
1350
1403
|
}
|
|
1351
1404
|
|
|
@@ -1356,7 +1409,7 @@ fn moveHit(
|
|
|
1356
1409
|
immune: *bool,
|
|
1357
1410
|
mist: *bool,
|
|
1358
1411
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1412
|
+
) !bool {
|
|
1360
1413
|
var side = battle.side(player);
|
|
1361
1414
|
const foe = battle.foe(player);
|
|
1362
1415
|
|
|
@@ -1407,7 +1460,7 @@ fn moveHit(
|
|
|
1407
1460
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1461
|
|
|
1409
1462
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1463
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1464
|
};
|
|
1412
1465
|
|
|
1413
1466
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1471,17 @@ fn moveHit(
|
|
|
1418
1471
|
|
|
1419
1472
|
if (!miss) return true;
|
|
1420
1473
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1474
|
+
|
|
1421
1475
|
side.active.volatiles.Binding = false;
|
|
1476
|
+
options.chance.observe(.binding, player, .None);
|
|
1477
|
+
|
|
1422
1478
|
return false;
|
|
1423
1479
|
}
|
|
1424
1480
|
|
|
1425
1481
|
fn checkFaint(
|
|
1426
1482
|
battle: anytype,
|
|
1427
1483
|
player: Player,
|
|
1484
|
+
cap: bool,
|
|
1428
1485
|
options: anytype,
|
|
1429
1486
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1487
|
const side = battle.side(player);
|
|
@@ -1437,8 +1494,8 @@ fn checkFaint(
|
|
|
1437
1494
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1495
|
const more = player_out or foe_out;
|
|
1439
1496
|
|
|
1440
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1441
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1497
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1498
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1499
|
|
|
1443
1500
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1501
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1516,7 @@ fn checkFaint(
|
|
|
1459
1516
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1517
|
}
|
|
1461
1518
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1519
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1520
|
var side = battle.side(player);
|
|
1464
1521
|
var foe = battle.foe(player);
|
|
1465
1522
|
assert(side.stored().hp == 0);
|
|
@@ -1493,7 +1550,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1550
|
}
|
|
1494
1551
|
|
|
1495
1552
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1553
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1554
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1555
|
+
if (cap) options.calc.capped(player);
|
|
1497
1556
|
return null;
|
|
1498
1557
|
}
|
|
1499
1558
|
|
|
@@ -1653,17 +1712,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1712
|
}
|
|
1654
1713
|
}
|
|
1655
1714
|
|
|
1656
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1657
|
-
var volatiles = &active.volatiles;
|
|
1658
|
-
assert(volatiles.Thrashing);
|
|
1659
|
-
if (volatiles.attacks > 0) return false;
|
|
1660
|
-
|
|
1661
|
-
volatiles.Thrashing = false;
|
|
1662
|
-
volatiles.Confusion = true;
|
|
1663
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1664
|
-
return true;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1715
|
fn onBegin(
|
|
1668
1716
|
battle: anytype,
|
|
1669
1717
|
player: Player,
|
|
@@ -1750,8 +1798,10 @@ pub const Effects = struct {
|
|
|
1750
1798
|
|
|
1751
1799
|
side.active.volatiles.Bide = true;
|
|
1752
1800
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1801
|
+
|
|
1753
1802
|
side.active.volatiles.state = 0;
|
|
1754
1803
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1804
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1805
|
|
|
1756
1806
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1807
|
}
|
|
@@ -1813,12 +1863,14 @@ pub const Effects = struct {
|
|
|
1813
1863
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1864
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1865
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1866
|
}
|
|
1818
1867
|
|
|
1819
1868
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1869
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
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());
|
|
1822
1874
|
|
|
1823
1875
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1876
|
}
|
|
@@ -1846,10 +1898,7 @@ pub const Effects = struct {
|
|
|
1846
1898
|
const foe_ident = battle.active(player.foe());
|
|
1847
1899
|
|
|
1848
1900
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1849
|
-
if (!showdown)
|
|
1850
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1851
|
-
try options.chance.commit(player, .hit);
|
|
1852
|
-
}
|
|
1901
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1902
|
|
|
1854
1903
|
if (volatiles.disable_move != 0) {
|
|
1855
1904
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1879,6 +1928,7 @@ pub const Effects = struct {
|
|
|
1879
1928
|
volatiles.disable_move =
|
|
1880
1929
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1930
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1931
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1932
|
|
|
1883
1933
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1934
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1985,22 +2035,24 @@ pub const Effects = struct {
|
|
|
1985
2035
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2036
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2037
|
s.stored().status = 0;
|
|
2038
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2039
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2040
|
s.stored().status = Status.init(.PSN);
|
|
1990
2041
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2042
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2043
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2044
|
}
|
|
1994
2045
|
} else {
|
|
1995
2046
|
if (Status.any(foe_stored.status)) {
|
|
1996
2047
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2048
|
foe.last_selected_move = .SKIP_TURN;
|
|
2049
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2050
|
}
|
|
1999
2051
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2052
|
foe_stored.status = 0;
|
|
2001
2053
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2054
|
+
try clearVolatiles(battle, player, options);
|
|
2055
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2056
|
}
|
|
2005
2057
|
}
|
|
2006
2058
|
|
|
@@ -2051,7 +2103,6 @@ pub const Effects = struct {
|
|
|
2051
2103
|
}
|
|
2052
2104
|
}
|
|
2053
2105
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2106
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2107
|
|
|
2057
2108
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2143,10 @@ pub const Effects = struct {
|
|
|
2092
2143
|
if (!has_mimic) {
|
|
2093
2144
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2145
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2146
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2147
|
}
|
|
2098
2148
|
}
|
|
2099
2149
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2150
|
|
|
2102
2151
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2152
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2200,6 @@ pub const Effects = struct {
|
|
|
2151
2200
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2201
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2202
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2203
|
|
|
2156
2204
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2205
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2267,6 @@ pub const Effects = struct {
|
|
|
2219
2267
|
|
|
2220
2268
|
if (move.effect == .Poison) {
|
|
2221
2269
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2270
|
} else {
|
|
2224
2271
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2272
|
return;
|
|
@@ -2288,9 +2335,8 @@ pub const Effects = struct {
|
|
|
2288
2335
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2336
|
});
|
|
2290
2337
|
}
|
|
2291
|
-
// If
|
|
2338
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2339
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2340
|
}
|
|
2295
2341
|
|
|
2296
2342
|
// Sleep Clause Mod
|
|
@@ -2302,6 +2348,8 @@ pub const Effects = struct {
|
|
|
2302
2348
|
foe.active.volatiles.Recharging = false;
|
|
2303
2349
|
|
|
2304
2350
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2351
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2352
|
+
|
|
2305
2353
|
const last = battle.side(player).last_selected_move;
|
|
2306
2354
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2355
|
}
|
|
@@ -2343,9 +2391,7 @@ pub const Effects = struct {
|
|
|
2343
2391
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2392
|
if (showdown) {
|
|
2345
2393
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2394
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2395
|
battle.last_damage = 0;
|
|
2350
2396
|
} else {
|
|
2351
2397
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2406,8 @@ pub const Effects = struct {
|
|
|
2360
2406
|
|
|
2361
2407
|
volatiles.Thrashing = true;
|
|
2362
2408
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2409
|
+
|
|
2410
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2411
|
}
|
|
2364
2412
|
|
|
2365
2413
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2405,6 +2453,7 @@ pub const Effects = struct {
|
|
|
2405
2453
|
|
|
2406
2454
|
side.active.volatiles.attacks =
|
|
2407
2455
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2456
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2457
|
}
|
|
2409
2458
|
|
|
2410
2459
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2570,6 @@ pub const Effects = struct {
|
|
|
2521
2570
|
} else {
|
|
2522
2571
|
// checkHit already checks for Invulnerable
|
|
2523
2572
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2573
|
}
|
|
2526
2574
|
|
|
2527
2575
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2683,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2683
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2684
|
}
|
|
2637
2685
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2686
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2687
|
var log = options.log;
|
|
2640
2688
|
var side = battle.side(who);
|
|
2641
2689
|
var volatiles = &side.active.volatiles;
|
|
2642
2690
|
const ident = battle.active(who);
|
|
2643
2691
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2692
|
if (volatiles.disable_move != 0) {
|
|
2647
2693
|
volatiles.disable_move = 0;
|
|
2648
2694
|
volatiles.disable_duration = 0;
|
|
2695
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2696
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2697
|
}
|
|
2651
2698
|
if (volatiles.Confusion) {
|
|
2652
2699
|
// volatiles.confusion is left unchanged
|
|
2653
2700
|
volatiles.Confusion = false;
|
|
2701
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2702
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2703
|
}
|
|
2656
2704
|
if (volatiles.Mist) {
|
|
@@ -2685,10 +2733,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2733
|
}
|
|
2686
2734
|
}
|
|
2687
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
|
+
|
|
2688
2749
|
pub const Rolls = struct {
|
|
2689
2750
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2751
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2691
2752
|
player == .P1
|
|
2753
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2754
|
+
battle.rng.speedTie()
|
|
2692
2755
|
else if (showdown)
|
|
2693
2756
|
battle.rng.range(u8, 0, 2) == 0
|
|
2694
2757
|
else
|
|
@@ -2698,21 +2761,31 @@ pub const Rolls = struct {
|
|
|
2698
2761
|
return p1;
|
|
2699
2762
|
}
|
|
2700
2763
|
|
|
2701
|
-
fn criticalHit(
|
|
2764
|
+
fn criticalHit(
|
|
2765
|
+
battle: anytype,
|
|
2766
|
+
player: Player,
|
|
2767
|
+
rate: u8,
|
|
2768
|
+
spurious: bool,
|
|
2769
|
+
options: anytype,
|
|
2770
|
+
) !bool {
|
|
2702
2771
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2703
2772
|
val == .true
|
|
2773
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2774
|
+
battle.rng.criticalHit(player, rate)
|
|
2704
2775
|
else if (showdown)
|
|
2705
2776
|
battle.rng.chance(u8, rate, 256)
|
|
2706
2777
|
else
|
|
2707
2778
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2708
2779
|
|
|
2709
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2780
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2710
2781
|
return crit;
|
|
2711
2782
|
}
|
|
2712
2783
|
|
|
2713
2784
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2714
2785
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2715
2786
|
val
|
|
2787
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2788
|
+
battle.rng.damage(player)
|
|
2716
2789
|
else roll: {
|
|
2717
2790
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2718
2791
|
while (true) {
|
|
@@ -2726,21 +2799,27 @@ pub const Rolls = struct {
|
|
|
2726
2799
|
return roll;
|
|
2727
2800
|
}
|
|
2728
2801
|
|
|
2729
|
-
|
|
2802
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2803
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2804
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2730
2805
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2731
2806
|
val == .true
|
|
2807
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2808
|
+
battle.rng.hit(player, acc)
|
|
2732
2809
|
else if (showdown)
|
|
2733
|
-
battle.rng.chance(
|
|
2810
|
+
battle.rng.chance(uN, acc, 256)
|
|
2734
2811
|
else
|
|
2735
|
-
battle.rng.next() <
|
|
2812
|
+
battle.rng.next() < acc;
|
|
2736
2813
|
|
|
2737
|
-
options.chance.hit(ok,
|
|
2814
|
+
try options.chance.hit(player, ok, acc);
|
|
2738
2815
|
return ok;
|
|
2739
2816
|
}
|
|
2740
2817
|
|
|
2741
2818
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2742
2819
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2743
2820
|
val == .true
|
|
2821
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2822
|
+
battle.rng.confused(player)
|
|
2744
2823
|
else if (showdown)
|
|
2745
2824
|
!battle.rng.chance(u8, 128, 256)
|
|
2746
2825
|
else
|
|
@@ -2753,6 +2832,8 @@ pub const Rolls = struct {
|
|
|
2753
2832
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2754
2833
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2755
2834
|
val == .true
|
|
2835
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2836
|
+
battle.rng.paralyzed(player)
|
|
2756
2837
|
else if (showdown)
|
|
2757
2838
|
battle.rng.chance(u8, 63, 256)
|
|
2758
2839
|
else
|
|
@@ -2765,6 +2846,8 @@ pub const Rolls = struct {
|
|
|
2765
2846
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2766
2847
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2767
2848
|
val == .true
|
|
2849
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2850
|
+
battle.rng.secondaryChance(player, 25)
|
|
2768
2851
|
else if (showdown)
|
|
2769
2852
|
battle.rng.chance(u8, 25, 256)
|
|
2770
2853
|
else
|
|
@@ -2779,6 +2862,8 @@ pub const Rolls = struct {
|
|
|
2779
2862
|
|
|
2780
2863
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2781
2864
|
val == .true
|
|
2865
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2866
|
+
battle.rng.secondaryChance(player, rate)
|
|
2782
2867
|
else if (showdown)
|
|
2783
2868
|
battle.rng.chance(u8, rate, 256)
|
|
2784
2869
|
else
|
|
@@ -2793,6 +2878,8 @@ pub const Rolls = struct {
|
|
|
2793
2878
|
|
|
2794
2879
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2795
2880
|
val == .true
|
|
2881
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2882
|
+
battle.rng.secondaryChance(player, rate)
|
|
2796
2883
|
else if (showdown)
|
|
2797
2884
|
battle.rng.chance(u8, rate, 256)
|
|
2798
2885
|
else
|
|
@@ -2805,6 +2892,8 @@ pub const Rolls = struct {
|
|
|
2805
2892
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2806
2893
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2807
2894
|
val == .true
|
|
2895
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2896
|
+
battle.rng.secondaryChance(player, 85)
|
|
2808
2897
|
else if (showdown)
|
|
2809
2898
|
battle.rng.chance(u8, 85, 256)
|
|
2810
2899
|
else
|
|
@@ -2817,6 +2906,8 @@ pub const Rolls = struct {
|
|
|
2817
2906
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2818
2907
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2819
2908
|
val
|
|
2909
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2910
|
+
battle.rng.metronome(player)
|
|
2820
2911
|
else if (showdown)
|
|
2821
2912
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2822
2913
|
else move: {
|
|
@@ -2835,6 +2926,8 @@ pub const Rolls = struct {
|
|
|
2835
2926
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2836
2927
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2837
2928
|
val - 1
|
|
2929
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2930
|
+
battle.rng.psywave(player)
|
|
2838
2931
|
else if (showdown)
|
|
2839
2932
|
battle.rng.range(u8, 0, max)
|
|
2840
2933
|
else power: {
|
|
@@ -2850,8 +2943,11 @@ pub const Rolls = struct {
|
|
|
2850
2943
|
}
|
|
2851
2944
|
|
|
2852
2945
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2853
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2854
|
-
@intCast(val)
|
|
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"))
|
|
2950
|
+
battle.rng.sleepDuration(player)
|
|
2855
2951
|
else if (showdown)
|
|
2856
2952
|
battle.rng.range(u3, 1, 8)
|
|
2857
2953
|
else duration: {
|
|
@@ -2862,46 +2958,55 @@ pub const Rolls = struct {
|
|
|
2862
2958
|
};
|
|
2863
2959
|
|
|
2864
2960
|
assert(duration >= 1 and duration <= 7);
|
|
2865
|
-
options.chance.duration(
|
|
2961
|
+
options.chance.duration(player, duration);
|
|
2866
2962
|
return duration;
|
|
2867
2963
|
}
|
|
2868
2964
|
|
|
2869
2965
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2870
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2871
|
-
val
|
|
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"))
|
|
2970
|
+
battle.rng.disableDuration(player)
|
|
2872
2971
|
else if (showdown)
|
|
2873
2972
|
battle.rng.range(u4, 1, 9)
|
|
2874
2973
|
else
|
|
2875
2974
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2876
2975
|
|
|
2877
2976
|
assert(duration >= 1 and duration <= 8);
|
|
2878
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2879
2978
|
return duration;
|
|
2880
2979
|
}
|
|
2881
2980
|
|
|
2882
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2883
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2884
|
-
@intCast(val)
|
|
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)
|
|
2885
2987
|
else if (showdown)
|
|
2886
2988
|
battle.rng.range(u3, 2, 6)
|
|
2887
2989
|
else
|
|
2888
2990
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2889
2991
|
|
|
2890
2992
|
assert(duration >= 2 and duration <= 5);
|
|
2891
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2892
2994
|
return duration;
|
|
2893
2995
|
}
|
|
2894
2996
|
|
|
2895
2997
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2896
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
@intCast(val)
|
|
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"))
|
|
3002
|
+
battle.rng.attackingDuration(player)
|
|
2898
3003
|
else if (showdown)
|
|
2899
3004
|
battle.rng.range(u3, 2, 4)
|
|
2900
3005
|
else
|
|
2901
3006
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2902
3007
|
|
|
2903
3008
|
assert(duration >= 2 and duration <= 3);
|
|
2904
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2905
3010
|
return duration;
|
|
2906
3011
|
}
|
|
2907
3012
|
|
|
@@ -2914,8 +3019,11 @@ pub const Rolls = struct {
|
|
|
2914
3019
|
options: anytype,
|
|
2915
3020
|
) !u3 {
|
|
2916
3021
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2917
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2918
|
-
@intCast(val)
|
|
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"))
|
|
3026
|
+
battle.rng.distribution(player)
|
|
2919
3027
|
else if (showdown)
|
|
2920
3028
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2921
3029
|
else n: {
|
|
@@ -2925,7 +3033,7 @@ pub const Rolls = struct {
|
|
|
2925
3033
|
|
|
2926
3034
|
assert(n >= 2 and n <= 5);
|
|
2927
3035
|
if (effect == .Binding) {
|
|
2928
|
-
options.chance.duration(
|
|
3036
|
+
options.chance.duration(player, n);
|
|
2929
3037
|
} else {
|
|
2930
3038
|
try options.chance.multiHit(player, n);
|
|
2931
3039
|
}
|
|
@@ -2940,15 +3048,15 @@ pub const Rolls = struct {
|
|
|
2940
3048
|
options: anytype,
|
|
2941
3049
|
) !u4 {
|
|
2942
3050
|
// TODO: consider throwing error instead of rerolling?
|
|
2943
|
-
const
|
|
3051
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2944
3052
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2945
3053
|
val
|
|
2946
3054
|
else
|
|
2947
3055
|
null
|
|
2948
3056
|
else
|
|
2949
|
-
null
|
|
2950
|
-
|
|
2951
|
-
|
|
3057
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3058
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3059
|
+
else slot: {
|
|
2952
3060
|
if (showdown) {
|
|
2953
3061
|
if (check_pp == 0) {
|
|
2954
3062
|
var i: usize = moves.len;
|
|
@@ -3032,7 +3140,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3032
3140
|
const stored = side.stored();
|
|
3033
3141
|
|
|
3034
3142
|
// While players are not given any input options on the cartridge in these cases,
|
|
3035
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3143
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3036
3144
|
//
|
|
3037
3145
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3038
3146
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3040,7 +3148,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3040
3148
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3041
3149
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3042
3150
|
// strictly correct as the player would not have been presented the option to move or
|
|
3043
|
-
// switch at all
|
|
3151
|
+
// switch at all
|
|
3044
3152
|
if (isForced(active)) {
|
|
3045
3153
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3046
3154
|
n += 1;
|
|
@@ -3059,7 +3167,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3059
3167
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3060
3168
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3061
3169
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3062
|
-
// move normally if sleeping/frozen/bound
|
|
3170
|
+
// move normally if sleeping/frozen/bound
|
|
3063
3171
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3064
3172
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3065
3173
|
{
|
|
@@ -3072,7 +3180,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3072
3180
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3073
3181
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3074
3182
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3075
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3183
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3076
3184
|
if (limited) {
|
|
3077
3185
|
assert(showdown);
|
|
3078
3186
|
assert(side.last_selected_move != .None);
|