@pkmn/engine 0.1.0-dev.6c5fdce2 → 0.1.0-dev.6d566713
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 +1 -3
- package/README.md +41 -14
- package/build/pkg/addon.js +6 -7
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +4 -5
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +4 -4
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +8 -3
- package/build/test/display.js.map +1 -1
- package/build/tools/debug.js +2 -3
- package/build/tools/debug.js.map +1 -1
- package/build.zig +203 -89
- package/package.json +32 -22
- package/src/bin/install-pkmn-engine +30 -20
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +2 -2
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +14 -10
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +57 -57
- package/src/lib/common/rational.zig +114 -24
- package/src/lib/common/rng.zig +7 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +506 -128
- package/src/lib/gen1/chance.zig +374 -267
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data.zig +17 -16
- package/src/lib/gen1/helpers.zig +95 -538
- package/src/lib/gen1/mechanics.zig +268 -174
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +84 -496
- package/src/lib/gen2/data.zig +7 -6
- package/src/lib/gen2/generated.zig +0 -2
- package/src/lib/gen2/helpers.zig +41 -13
- package/src/lib/gen2/mechanics.zig +158 -108
- package/src/lib/{binding/node.zig → node.zig} +3 -6
- package/src/lib/pkmn.zig +32 -15
- package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
- package/src/test/display/pkmn.html.tmpl +5 -5
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
|
@@ -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,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,14 +652,8 @@ fn beforeMove(
|
|
|
635
652
|
volatiles.state +%= battle.last_damage;
|
|
636
653
|
}
|
|
637
654
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
641
|
-
if (!extend) volatiles.attacks = 0;
|
|
642
|
-
} else {
|
|
643
|
-
volatiles.attacks -= 1;
|
|
644
|
-
}
|
|
645
|
-
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);
|
|
646
657
|
|
|
647
658
|
if (volatiles.attacks != 0) {
|
|
648
659
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +687,36 @@ fn beforeMove(
|
|
|
676
687
|
}
|
|
677
688
|
|
|
678
689
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
690
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
691
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
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;
|
|
687
697
|
|
|
688
|
-
|
|
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)
|
|
689
706
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
707
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
690
708
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
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
|
+
|
|
700
714
|
return .skip_can;
|
|
701
715
|
}
|
|
702
716
|
|
|
703
717
|
if (volatiles.Binding) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (options.calc.modify(player, .binding)) |extend| {
|
|
707
|
-
if (!extend) volatiles.attacks = 0;
|
|
708
|
-
} else {
|
|
709
|
-
volatiles.attacks -= 1;
|
|
710
|
-
}
|
|
711
|
-
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);
|
|
712
720
|
|
|
713
721
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
722
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -801,7 +809,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
801
809
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
802
810
|
}
|
|
803
811
|
|
|
804
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
812
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
805
813
|
var active = &side.active;
|
|
806
814
|
const volatiles = &active.volatiles;
|
|
807
815
|
|
|
@@ -809,8 +817,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
809
817
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
810
818
|
if (showdown and volatiles.Transform) return;
|
|
811
819
|
|
|
820
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
821
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
822
|
+
|
|
812
823
|
assert(mslot > 0 and mslot <= 4);
|
|
813
|
-
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;
|
|
814
825
|
}
|
|
815
826
|
|
|
816
827
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,13 +869,13 @@ fn doMove(
|
|
|
858
869
|
break :miss true;
|
|
859
870
|
}
|
|
860
871
|
break :miss (move.target != .Self and
|
|
861
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
872
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
862
873
|
};
|
|
863
874
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
864
875
|
|
|
865
876
|
var late = showdown and move.effect != .Explode;
|
|
866
877
|
const skip = status or immune;
|
|
867
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
878
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
868
879
|
if (showdown and move.effect.isMulti()) {
|
|
869
880
|
try Effects.multiHit(battle, player, move, options);
|
|
870
881
|
hits = side.active.volatiles.attacks;
|
|
@@ -886,8 +897,9 @@ fn doMove(
|
|
|
886
897
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
887
898
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
888
899
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
889
|
-
if (showdown) break :
|
|
890
|
-
} 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);
|
|
891
903
|
return @as(?Result, Result.Error);
|
|
892
904
|
}
|
|
893
905
|
if (battle.last_damage == 0) {
|
|
@@ -901,11 +913,12 @@ fn doMove(
|
|
|
901
913
|
}
|
|
902
914
|
}
|
|
903
915
|
|
|
916
|
+
var missed: bool = undefined;
|
|
904
917
|
const zero = battle.last_damage == 0;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
918
|
+
if (!showdown and !skip) {
|
|
919
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
920
|
+
miss = missed or zero;
|
|
921
|
+
}
|
|
909
922
|
|
|
910
923
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
911
924
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -932,11 +945,32 @@ fn doMove(
|
|
|
932
945
|
try log.immune(.{ foe_ident, .OHKO });
|
|
933
946
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
934
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
|
+
}
|
|
935
969
|
} else if (mist) {
|
|
936
970
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
937
971
|
try log.fail(.{ foe_ident, .None });
|
|
938
972
|
} else {
|
|
939
|
-
if (showdown
|
|
973
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
940
974
|
try log.lastmiss(.{});
|
|
941
975
|
try log.miss(.{battle.active(player)});
|
|
942
976
|
}
|
|
@@ -951,17 +985,15 @@ fn doMove(
|
|
|
951
985
|
try buildRage(battle, player.foe(), options);
|
|
952
986
|
} else if (showdown and move.effect == .Disable) {
|
|
953
987
|
try buildRage(battle, player.foe(), options);
|
|
954
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
955
|
-
try options.chance.commit(player, .binding);
|
|
956
988
|
}
|
|
957
989
|
return null;
|
|
958
|
-
} else {
|
|
990
|
+
} else if (!showdown) {
|
|
959
991
|
try options.chance.commit(player, .hit);
|
|
960
992
|
}
|
|
961
993
|
|
|
962
994
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
963
995
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
964
|
-
// it up above before damage calculation)
|
|
996
|
+
// it up above before damage calculation)
|
|
965
997
|
if (!showdown and move.effect.isMulti()) {
|
|
966
998
|
try Effects.multiHit(battle, player, move, options);
|
|
967
999
|
hits = side.active.volatiles.attacks;
|
|
@@ -1020,12 +1052,12 @@ fn doMove(
|
|
|
1020
1052
|
}
|
|
1021
1053
|
|
|
1022
1054
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1023
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1055
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1024
1056
|
if (nullified) return null;
|
|
1025
1057
|
|
|
1026
1058
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1027
1059
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1028
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1060
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1029
1061
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1030
1062
|
|
|
1031
1063
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1040,7 +1072,7 @@ fn doMove(
|
|
|
1040
1072
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1041
1073
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1042
1074
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1043
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1075
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1044
1076
|
if (move.effect == .Twineedle) {
|
|
1045
1077
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1046
1078
|
} else if (move.effect == .Disable) {
|
|
@@ -1059,12 +1091,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1059
1091
|
const side = battle.side(player);
|
|
1060
1092
|
|
|
1061
1093
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1062
|
-
var rate =
|
|
1094
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1063
1095
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1064
1096
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1065
1097
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1066
1098
|
|
|
1067
|
-
|
|
1099
|
+
const spurious =
|
|
1100
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1101
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1068
1102
|
}
|
|
1069
1103
|
|
|
1070
1104
|
fn calcDamage(
|
|
@@ -1073,6 +1107,7 @@ fn calcDamage(
|
|
|
1073
1107
|
target_player: Player,
|
|
1074
1108
|
m: ?Move.Data,
|
|
1075
1109
|
crit: bool,
|
|
1110
|
+
options: anytype,
|
|
1076
1111
|
) bool {
|
|
1077
1112
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1078
1113
|
const cfz = m == null;
|
|
@@ -1084,28 +1119,8 @@ fn calcDamage(
|
|
|
1084
1119
|
|
|
1085
1120
|
const special = move.type.special();
|
|
1086
1121
|
|
|
1087
|
-
|
|
1088
|
-
var
|
|
1089
|
-
if (crit)
|
|
1090
|
-
if (special) side.stored().stats.spc
|
|
1091
|
-
else side.stored().stats.atk
|
|
1092
|
-
else
|
|
1093
|
-
if (special) side.active.stats.spc
|
|
1094
|
-
else side.active.stats.atk;
|
|
1095
|
-
var def: u32 =
|
|
1096
|
-
if (crit)
|
|
1097
|
-
if (special) target.stored().stats.spc
|
|
1098
|
-
else target.stored().stats.def
|
|
1099
|
-
else
|
|
1100
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1101
|
-
if (special)
|
|
1102
|
-
target.active.stats.spc *
|
|
1103
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1104
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1105
|
-
else
|
|
1106
|
-
target.active.stats.def *
|
|
1107
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1108
|
-
// zig fmt: on
|
|
1122
|
+
var atk = attack(side, crit, special);
|
|
1123
|
+
var def = defense(target, crit, special, cfz);
|
|
1109
1124
|
|
|
1110
1125
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1111
1126
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1116,9 +1131,9 @@ fn calcDamage(
|
|
|
1116
1131
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1117
1132
|
}
|
|
1118
1133
|
|
|
1119
|
-
const lvl =
|
|
1134
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1120
1135
|
|
|
1121
|
-
def =
|
|
1136
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1122
1137
|
|
|
1123
1138
|
if (def == 0) return false;
|
|
1124
1139
|
|
|
@@ -1130,11 +1145,41 @@ fn calcDamage(
|
|
|
1130
1145
|
d = @min(997, d);
|
|
1131
1146
|
d += 2;
|
|
1132
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
|
+
|
|
1133
1158
|
battle.last_damage = @intCast(d);
|
|
1134
1159
|
|
|
1135
1160
|
return true;
|
|
1136
1161
|
}
|
|
1137
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
|
+
|
|
1138
1183
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1139
1184
|
const side = battle.side(player);
|
|
1140
1185
|
const foe = battle.foe(player);
|
|
@@ -1179,7 +1224,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1179
1224
|
const foe = battle.foe(player);
|
|
1180
1225
|
|
|
1181
1226
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1182
|
-
try options.chance.commit(player, .hit);
|
|
1183
1227
|
|
|
1184
1228
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1185
1229
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1240,9 +1284,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1240
1284
|
|
|
1241
1285
|
battle.last_damage *|= 2;
|
|
1242
1286
|
|
|
1243
|
-
// Pokémon Showdown calls
|
|
1287
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1244
1288
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1245
|
-
try options.chance.commit(player, .hit);
|
|
1246
1289
|
|
|
1247
1290
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1248
1291
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1271,7 +1314,7 @@ fn applyDamage(
|
|
|
1271
1314
|
subbed.active.volatiles.Substitute = false;
|
|
1272
1315
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1273
1316
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1274
|
-
options.calc.capped(
|
|
1317
|
+
options.calc.capped(sub_player);
|
|
1275
1318
|
return true;
|
|
1276
1319
|
} else {
|
|
1277
1320
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1305,7 +1348,7 @@ fn mirrorMove(
|
|
|
1305
1348
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1306
1349
|
return null;
|
|
1307
1350
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1308
|
-
incrementPP(side, mslot);
|
|
1351
|
+
incrementPP(side, player, mslot, options);
|
|
1309
1352
|
}
|
|
1310
1353
|
|
|
1311
1354
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1326,7 +1369,7 @@ fn metronome(
|
|
|
1326
1369
|
var side = battle.side(player);
|
|
1327
1370
|
|
|
1328
1371
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1329
|
-
incrementPP(side, mslot);
|
|
1372
|
+
incrementPP(side, player, mslot, options);
|
|
1330
1373
|
|
|
1331
1374
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1332
1375
|
return null;
|
|
@@ -1338,7 +1381,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1338
1381
|
var immune = false;
|
|
1339
1382
|
var mist = false;
|
|
1340
1383
|
|
|
1341
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1384
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1385
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1342
1388
|
|
|
1343
1389
|
assert(!immune);
|
|
1344
1390
|
if (mist) {
|
|
@@ -1347,11 +1393,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1347
1393
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1348
1394
|
try options.log.fail(.{ foe_ident, .None });
|
|
1349
1395
|
} else {
|
|
1350
|
-
try options.chance.commit(player, .miss);
|
|
1351
1396
|
try options.log.lastmiss(.{});
|
|
1352
1397
|
try options.log.miss(.{battle.active(player)});
|
|
1353
1398
|
}
|
|
1354
1399
|
|
|
1400
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1355
1401
|
return false;
|
|
1356
1402
|
}
|
|
1357
1403
|
|
|
@@ -1362,7 +1408,7 @@ fn moveHit(
|
|
|
1362
1408
|
immune: *bool,
|
|
1363
1409
|
mist: *bool,
|
|
1364
1410
|
options: anytype,
|
|
1365
|
-
) bool {
|
|
1411
|
+
) !bool {
|
|
1366
1412
|
var side = battle.side(player);
|
|
1367
1413
|
const foe = battle.foe(player);
|
|
1368
1414
|
|
|
@@ -1401,7 +1447,7 @@ fn moveHit(
|
|
|
1401
1447
|
const overwritten = overwrite and state > 0;
|
|
1402
1448
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1403
1449
|
|
|
1404
|
-
var accuracy = if (overwritten) state else
|
|
1450
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1405
1451
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1406
1452
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1407
1453
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1413,7 +1459,7 @@ fn moveHit(
|
|
|
1413
1459
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1414
1460
|
|
|
1415
1461
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1416
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1462
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1417
1463
|
};
|
|
1418
1464
|
|
|
1419
1465
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1424,13 +1470,17 @@ fn moveHit(
|
|
|
1424
1470
|
|
|
1425
1471
|
if (!miss) return true;
|
|
1426
1472
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1473
|
+
|
|
1427
1474
|
side.active.volatiles.Binding = false;
|
|
1475
|
+
options.chance.observe(.binding, player, .None);
|
|
1476
|
+
|
|
1428
1477
|
return false;
|
|
1429
1478
|
}
|
|
1430
1479
|
|
|
1431
1480
|
fn checkFaint(
|
|
1432
1481
|
battle: anytype,
|
|
1433
1482
|
player: Player,
|
|
1483
|
+
cap: bool,
|
|
1434
1484
|
options: anytype,
|
|
1435
1485
|
) @TypeOf(options.log).Error!?Result {
|
|
1436
1486
|
const side = battle.side(player);
|
|
@@ -1443,8 +1493,8 @@ fn checkFaint(
|
|
|
1443
1493
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1444
1494
|
const more = player_out or foe_out;
|
|
1445
1495
|
|
|
1446
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1447
|
-
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;
|
|
1448
1498
|
|
|
1449
1499
|
assert(!side.active.volatiles.MultiHit);
|
|
1450
1500
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1465,7 +1515,7 @@ fn checkFaint(
|
|
|
1465
1515
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1466
1516
|
}
|
|
1467
1517
|
|
|
1468
|
-
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 {
|
|
1469
1519
|
var side = battle.side(player);
|
|
1470
1520
|
var foe = battle.foe(player);
|
|
1471
1521
|
assert(side.stored().hp == 0);
|
|
@@ -1499,7 +1549,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1499
1549
|
}
|
|
1500
1550
|
|
|
1501
1551
|
try options.log.faint(.{ battle.active(player), done });
|
|
1502
|
-
|
|
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);
|
|
1503
1555
|
return null;
|
|
1504
1556
|
}
|
|
1505
1557
|
|
|
@@ -1659,17 +1711,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1711
|
}
|
|
1660
1712
|
}
|
|
1661
1713
|
|
|
1662
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1663
|
-
var volatiles = &active.volatiles;
|
|
1664
|
-
assert(volatiles.Thrashing);
|
|
1665
|
-
if (volatiles.attacks > 0) return false;
|
|
1666
|
-
|
|
1667
|
-
volatiles.Thrashing = false;
|
|
1668
|
-
volatiles.Confusion = true;
|
|
1669
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1670
|
-
return true;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
1714
|
fn onBegin(
|
|
1674
1715
|
battle: anytype,
|
|
1675
1716
|
player: Player,
|
|
@@ -1678,7 +1719,6 @@ fn onBegin(
|
|
|
1678
1719
|
residual: *bool,
|
|
1679
1720
|
options: anytype,
|
|
1680
1721
|
) !void {
|
|
1681
|
-
assert(move.effect.onBegin());
|
|
1682
1722
|
return switch (move.effect) {
|
|
1683
1723
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1684
1724
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1701,7 +1741,6 @@ fn onBegin(
|
|
|
1701
1741
|
}
|
|
1702
1742
|
|
|
1703
1743
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1704
|
-
assert(move.effect.onEnd());
|
|
1705
1744
|
return switch (move.effect) {
|
|
1706
1745
|
.Bide => Effects.bide(battle, player, options),
|
|
1707
1746
|
// zig fmt: off
|
|
@@ -1723,7 +1762,6 @@ fn alwaysHappens(
|
|
|
1723
1762
|
residual: *bool,
|
|
1724
1763
|
options: anytype,
|
|
1725
1764
|
) !void {
|
|
1726
|
-
assert(move.effect.alwaysHappens());
|
|
1727
1765
|
return switch (move.effect) {
|
|
1728
1766
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1729
1767
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1759,8 +1797,10 @@ pub const Effects = struct {
|
|
|
1759
1797
|
|
|
1760
1798
|
side.active.volatiles.Bide = true;
|
|
1761
1799
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1800
|
+
|
|
1762
1801
|
side.active.volatiles.state = 0;
|
|
1763
1802
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1803
|
+
options.chance.observe(.attacking, player, .started);
|
|
1764
1804
|
|
|
1765
1805
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1766
1806
|
}
|
|
@@ -1822,12 +1862,13 @@ pub const Effects = struct {
|
|
|
1822
1862
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1823
1863
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1824
1864
|
}
|
|
1825
|
-
try options.chance.commit(player, .hit);
|
|
1826
1865
|
}
|
|
1827
1866
|
|
|
1828
1867
|
if (foe.active.volatiles.Confusion) return;
|
|
1829
1868
|
foe.active.volatiles.Confusion = true;
|
|
1830
|
-
|
|
1869
|
+
|
|
1870
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1871
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1831
1872
|
|
|
1832
1873
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1833
1874
|
}
|
|
@@ -1855,10 +1896,7 @@ pub const Effects = struct {
|
|
|
1855
1896
|
const foe_ident = battle.active(player.foe());
|
|
1856
1897
|
|
|
1857
1898
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1858
|
-
if (!showdown)
|
|
1859
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1860
|
-
try options.chance.commit(player, .hit);
|
|
1861
|
-
}
|
|
1899
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1862
1900
|
|
|
1863
1901
|
if (volatiles.disable_move != 0) {
|
|
1864
1902
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1888,6 +1926,7 @@ pub const Effects = struct {
|
|
|
1888
1926
|
volatiles.disable_move =
|
|
1889
1927
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1890
1928
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1929
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1891
1930
|
|
|
1892
1931
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1893
1932
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1994,22 +2033,24 @@ pub const Effects = struct {
|
|
|
1994
2033
|
if (p != player and Status.any(s.stored().status)) {
|
|
1995
2034
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1996
2035
|
s.stored().status = 0;
|
|
2036
|
+
options.chance.observe(.sleep, p, .None);
|
|
1997
2037
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1998
2038
|
s.stored().status = Status.init(.PSN);
|
|
1999
2039
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2000
2040
|
}
|
|
2001
|
-
try clearVolatiles(battle, p,
|
|
2041
|
+
try clearVolatiles(battle, p, options);
|
|
2002
2042
|
}
|
|
2003
2043
|
} else {
|
|
2004
2044
|
if (Status.any(foe_stored.status)) {
|
|
2005
2045
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2006
2046
|
foe.last_selected_move = .SKIP_TURN;
|
|
2047
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2007
2048
|
}
|
|
2008
2049
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2009
2050
|
foe_stored.status = 0;
|
|
2010
2051
|
}
|
|
2011
|
-
try clearVolatiles(battle, player,
|
|
2012
|
-
try clearVolatiles(battle, player.foe(),
|
|
2052
|
+
try clearVolatiles(battle, player, options);
|
|
2053
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2013
2054
|
}
|
|
2014
2055
|
}
|
|
2015
2056
|
|
|
@@ -2060,7 +2101,6 @@ pub const Effects = struct {
|
|
|
2060
2101
|
}
|
|
2061
2102
|
}
|
|
2062
2103
|
|
|
2063
|
-
try options.chance.commit(player, .hit);
|
|
2064
2104
|
foe.active.volatiles.LeechSeed = true;
|
|
2065
2105
|
|
|
2066
2106
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2101,12 +2141,10 @@ pub const Effects = struct {
|
|
|
2101
2141
|
if (!has_mimic) {
|
|
2102
2142
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2103
2143
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2104
|
-
try options.chance.commit(player, .hit);
|
|
2105
2144
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2106
2145
|
}
|
|
2107
2146
|
}
|
|
2108
2147
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2109
|
-
try options.chance.commit(player, .hit);
|
|
2110
2148
|
|
|
2111
2149
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2112
2150
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2160,7 +2198,6 @@ pub const Effects = struct {
|
|
|
2160
2198
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2161
2199
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2162
2200
|
}
|
|
2163
|
-
try options.chance.commit(player, .hit);
|
|
2164
2201
|
|
|
2165
2202
|
foe_stored.status = Status.init(.PAR);
|
|
2166
2203
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2228,7 +2265,6 @@ pub const Effects = struct {
|
|
|
2228
2265
|
|
|
2229
2266
|
if (move.effect == .Poison) {
|
|
2230
2267
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2231
|
-
try options.chance.commit(player, .hit);
|
|
2232
2268
|
} else {
|
|
2233
2269
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2234
2270
|
return;
|
|
@@ -2297,9 +2333,8 @@ pub const Effects = struct {
|
|
|
2297
2333
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2298
2334
|
});
|
|
2299
2335
|
}
|
|
2300
|
-
// If
|
|
2336
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2301
2337
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2302
|
-
try options.chance.commit(player, .hit);
|
|
2303
2338
|
}
|
|
2304
2339
|
|
|
2305
2340
|
// Sleep Clause Mod
|
|
@@ -2311,6 +2346,8 @@ pub const Effects = struct {
|
|
|
2311
2346
|
foe.active.volatiles.Recharging = false;
|
|
2312
2347
|
|
|
2313
2348
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2349
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2350
|
+
|
|
2314
2351
|
const last = battle.side(player).last_selected_move;
|
|
2315
2352
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2316
2353
|
}
|
|
@@ -2352,9 +2389,7 @@ pub const Effects = struct {
|
|
|
2352
2389
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2353
2390
|
if (showdown) {
|
|
2354
2391
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2355
|
-
|
|
2356
|
-
try options.chance.commit(player, .hit);
|
|
2357
|
-
}
|
|
2392
|
+
_ = try checkHit(battle, player, move, options);
|
|
2358
2393
|
battle.last_damage = 0;
|
|
2359
2394
|
} else {
|
|
2360
2395
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2369,6 +2404,8 @@ pub const Effects = struct {
|
|
|
2369
2404
|
|
|
2370
2405
|
volatiles.Thrashing = true;
|
|
2371
2406
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2407
|
+
|
|
2408
|
+
options.chance.observe(.attacking, player, .started);
|
|
2372
2409
|
}
|
|
2373
2410
|
|
|
2374
2411
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2414,6 +2451,7 @@ pub const Effects = struct {
|
|
|
2414
2451
|
|
|
2415
2452
|
side.active.volatiles.attacks =
|
|
2416
2453
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2454
|
+
options.chance.observe(.binding, player, .started);
|
|
2417
2455
|
}
|
|
2418
2456
|
|
|
2419
2457
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2530,7 +2568,6 @@ pub const Effects = struct {
|
|
|
2530
2568
|
} else {
|
|
2531
2569
|
// checkHit already checks for Invulnerable
|
|
2532
2570
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2533
|
-
try options.chance.commit(player, .hit);
|
|
2534
2571
|
}
|
|
2535
2572
|
|
|
2536
2573
|
var stats = &foe.active.stats;
|
|
@@ -2644,22 +2681,22 @@ fn isForced(active: anytype) bool {
|
|
|
2644
2681
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2645
2682
|
}
|
|
2646
2683
|
|
|
2647
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2684
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2648
2685
|
var log = options.log;
|
|
2649
2686
|
var side = battle.side(who);
|
|
2650
2687
|
var volatiles = &side.active.volatiles;
|
|
2651
2688
|
const ident = battle.active(who);
|
|
2652
2689
|
|
|
2653
|
-
options.chance.clearDurations(who, clear);
|
|
2654
|
-
|
|
2655
2690
|
if (volatiles.disable_move != 0) {
|
|
2656
2691
|
volatiles.disable_move = 0;
|
|
2657
2692
|
volatiles.disable_duration = 0;
|
|
2693
|
+
options.chance.observe(.disable, who, .None);
|
|
2658
2694
|
try log.end(.{ ident, .DisableSilent });
|
|
2659
2695
|
}
|
|
2660
2696
|
if (volatiles.Confusion) {
|
|
2661
2697
|
// volatiles.confusion is left unchanged
|
|
2662
2698
|
volatiles.Confusion = false;
|
|
2699
|
+
options.chance.observe(.confusion, who, .None);
|
|
2663
2700
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2664
2701
|
}
|
|
2665
2702
|
if (volatiles.Mist) {
|
|
@@ -2694,10 +2731,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2694
2731
|
}
|
|
2695
2732
|
}
|
|
2696
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
|
+
|
|
2697
2747
|
pub const Rolls = struct {
|
|
2698
2748
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2699
2749
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2700
2750
|
player == .P1
|
|
2751
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2752
|
+
battle.rng.speedTie()
|
|
2701
2753
|
else if (showdown)
|
|
2702
2754
|
battle.rng.range(u8, 0, 2) == 0
|
|
2703
2755
|
else
|
|
@@ -2707,21 +2759,31 @@ pub const Rolls = struct {
|
|
|
2707
2759
|
return p1;
|
|
2708
2760
|
}
|
|
2709
2761
|
|
|
2710
|
-
fn criticalHit(
|
|
2762
|
+
fn criticalHit(
|
|
2763
|
+
battle: anytype,
|
|
2764
|
+
player: Player,
|
|
2765
|
+
rate: u8,
|
|
2766
|
+
spurious: bool,
|
|
2767
|
+
options: anytype,
|
|
2768
|
+
) !bool {
|
|
2711
2769
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2712
2770
|
val == .true
|
|
2771
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2772
|
+
battle.rng.criticalHit(player, rate)
|
|
2713
2773
|
else if (showdown)
|
|
2714
2774
|
battle.rng.chance(u8, rate, 256)
|
|
2715
2775
|
else
|
|
2716
2776
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2717
2777
|
|
|
2718
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2778
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2719
2779
|
return crit;
|
|
2720
2780
|
}
|
|
2721
2781
|
|
|
2722
2782
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2723
2783
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2724
2784
|
val
|
|
2785
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2786
|
+
battle.rng.damage(player)
|
|
2725
2787
|
else roll: {
|
|
2726
2788
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2727
2789
|
while (true) {
|
|
@@ -2735,21 +2797,27 @@ pub const Rolls = struct {
|
|
|
2735
2797
|
return roll;
|
|
2736
2798
|
}
|
|
2737
2799
|
|
|
2738
|
-
|
|
2800
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2801
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2802
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2739
2803
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2740
2804
|
val == .true
|
|
2805
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2806
|
+
battle.rng.hit(player, acc)
|
|
2741
2807
|
else if (showdown)
|
|
2742
|
-
battle.rng.chance(
|
|
2808
|
+
battle.rng.chance(uN, acc, 256)
|
|
2743
2809
|
else
|
|
2744
|
-
battle.rng.next() <
|
|
2810
|
+
battle.rng.next() < acc;
|
|
2745
2811
|
|
|
2746
|
-
options.chance.hit(ok,
|
|
2812
|
+
try options.chance.hit(player, ok, acc);
|
|
2747
2813
|
return ok;
|
|
2748
2814
|
}
|
|
2749
2815
|
|
|
2750
2816
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2751
2817
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2752
2818
|
val == .true
|
|
2819
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2820
|
+
battle.rng.confused(player)
|
|
2753
2821
|
else if (showdown)
|
|
2754
2822
|
!battle.rng.chance(u8, 128, 256)
|
|
2755
2823
|
else
|
|
@@ -2762,6 +2830,8 @@ pub const Rolls = struct {
|
|
|
2762
2830
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2763
2831
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2764
2832
|
val == .true
|
|
2833
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2834
|
+
battle.rng.paralyzed(player)
|
|
2765
2835
|
else if (showdown)
|
|
2766
2836
|
battle.rng.chance(u8, 63, 256)
|
|
2767
2837
|
else
|
|
@@ -2774,6 +2844,8 @@ pub const Rolls = struct {
|
|
|
2774
2844
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2775
2845
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2776
2846
|
val == .true
|
|
2847
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2848
|
+
battle.rng.secondaryChance(player, 25)
|
|
2777
2849
|
else if (showdown)
|
|
2778
2850
|
battle.rng.chance(u8, 25, 256)
|
|
2779
2851
|
else
|
|
@@ -2788,6 +2860,8 @@ pub const Rolls = struct {
|
|
|
2788
2860
|
|
|
2789
2861
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2790
2862
|
val == .true
|
|
2863
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2864
|
+
battle.rng.secondaryChance(player, rate)
|
|
2791
2865
|
else if (showdown)
|
|
2792
2866
|
battle.rng.chance(u8, rate, 256)
|
|
2793
2867
|
else
|
|
@@ -2802,6 +2876,8 @@ pub const Rolls = struct {
|
|
|
2802
2876
|
|
|
2803
2877
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2804
2878
|
val == .true
|
|
2879
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2880
|
+
battle.rng.secondaryChance(player, rate)
|
|
2805
2881
|
else if (showdown)
|
|
2806
2882
|
battle.rng.chance(u8, rate, 256)
|
|
2807
2883
|
else
|
|
@@ -2814,6 +2890,8 @@ pub const Rolls = struct {
|
|
|
2814
2890
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2815
2891
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2816
2892
|
val == .true
|
|
2893
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2894
|
+
battle.rng.secondaryChance(player, 85)
|
|
2817
2895
|
else if (showdown)
|
|
2818
2896
|
battle.rng.chance(u8, 85, 256)
|
|
2819
2897
|
else
|
|
@@ -2826,6 +2904,8 @@ pub const Rolls = struct {
|
|
|
2826
2904
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2827
2905
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2828
2906
|
val
|
|
2907
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2908
|
+
battle.rng.metronome(player)
|
|
2829
2909
|
else if (showdown)
|
|
2830
2910
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2831
2911
|
else move: {
|
|
@@ -2844,6 +2924,8 @@ pub const Rolls = struct {
|
|
|
2844
2924
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2845
2925
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2846
2926
|
val - 1
|
|
2927
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2928
|
+
battle.rng.psywave(player)
|
|
2847
2929
|
else if (showdown)
|
|
2848
2930
|
battle.rng.range(u8, 0, max)
|
|
2849
2931
|
else power: {
|
|
@@ -2861,6 +2943,8 @@ pub const Rolls = struct {
|
|
|
2861
2943
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2862
2944
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2863
2945
|
@intCast(val)
|
|
2946
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2947
|
+
battle.rng.sleepDuration(player)
|
|
2864
2948
|
else if (showdown)
|
|
2865
2949
|
battle.rng.range(u3, 1, 8)
|
|
2866
2950
|
else duration: {
|
|
@@ -2871,46 +2955,52 @@ pub const Rolls = struct {
|
|
|
2871
2955
|
};
|
|
2872
2956
|
|
|
2873
2957
|
assert(duration >= 1 and duration <= 7);
|
|
2874
|
-
options.chance.duration(
|
|
2958
|
+
options.chance.duration(player, duration);
|
|
2875
2959
|
return duration;
|
|
2876
2960
|
}
|
|
2877
2961
|
|
|
2878
2962
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2879
2963
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2880
2964
|
val
|
|
2965
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2966
|
+
battle.rng.disableDuration(player)
|
|
2881
2967
|
else if (showdown)
|
|
2882
2968
|
battle.rng.range(u4, 1, 9)
|
|
2883
2969
|
else
|
|
2884
2970
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2885
2971
|
|
|
2886
2972
|
assert(duration >= 1 and duration <= 8);
|
|
2887
|
-
options.chance.duration(
|
|
2973
|
+
options.chance.duration(player, duration);
|
|
2888
2974
|
return duration;
|
|
2889
2975
|
}
|
|
2890
2976
|
|
|
2891
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2977
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2892
2978
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2893
2979
|
@intCast(val)
|
|
2980
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2981
|
+
battle.rng.confusionDuration(player)
|
|
2894
2982
|
else if (showdown)
|
|
2895
2983
|
battle.rng.range(u3, 2, 6)
|
|
2896
2984
|
else
|
|
2897
2985
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2898
2986
|
|
|
2899
2987
|
assert(duration >= 2 and duration <= 5);
|
|
2900
|
-
options.chance.duration(
|
|
2988
|
+
options.chance.duration(player, duration);
|
|
2901
2989
|
return duration;
|
|
2902
2990
|
}
|
|
2903
2991
|
|
|
2904
2992
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2905
2993
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2906
2994
|
@intCast(val)
|
|
2995
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2996
|
+
battle.rng.attackingDuration(player)
|
|
2907
2997
|
else if (showdown)
|
|
2908
2998
|
battle.rng.range(u3, 2, 4)
|
|
2909
2999
|
else
|
|
2910
3000
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2911
3001
|
|
|
2912
3002
|
assert(duration >= 2 and duration <= 3);
|
|
2913
|
-
options.chance.duration(
|
|
3003
|
+
options.chance.duration(player, duration);
|
|
2914
3004
|
return duration;
|
|
2915
3005
|
}
|
|
2916
3006
|
|
|
@@ -2925,6 +3015,8 @@ pub const Rolls = struct {
|
|
|
2925
3015
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2926
3016
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2927
3017
|
@intCast(val)
|
|
3018
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3019
|
+
battle.rng.distribution(player)
|
|
2928
3020
|
else if (showdown)
|
|
2929
3021
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2930
3022
|
else n: {
|
|
@@ -2934,7 +3026,7 @@ pub const Rolls = struct {
|
|
|
2934
3026
|
|
|
2935
3027
|
assert(n >= 2 and n <= 5);
|
|
2936
3028
|
if (effect == .Binding) {
|
|
2937
|
-
options.chance.duration(
|
|
3029
|
+
options.chance.duration(player, n);
|
|
2938
3030
|
} else {
|
|
2939
3031
|
try options.chance.multiHit(player, n);
|
|
2940
3032
|
}
|
|
@@ -2957,7 +3049,9 @@ pub const Rolls = struct {
|
|
|
2957
3049
|
else
|
|
2958
3050
|
null;
|
|
2959
3051
|
|
|
2960
|
-
const slot: u4 = overridden orelse
|
|
3052
|
+
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3053
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3054
|
+
else slot: {
|
|
2961
3055
|
if (showdown) {
|
|
2962
3056
|
if (check_pp == 0) {
|
|
2963
3057
|
var i: usize = moves.len;
|
|
@@ -3041,7 +3135,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3041
3135
|
const stored = side.stored();
|
|
3042
3136
|
|
|
3043
3137
|
// While players are not given any input options on the cartridge in these cases,
|
|
3044
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3138
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3045
3139
|
//
|
|
3046
3140
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3047
3141
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3049,7 +3143,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3049
3143
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3050
3144
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3051
3145
|
// strictly correct as the player would not have been presented the option to move or
|
|
3052
|
-
// switch at all
|
|
3146
|
+
// switch at all
|
|
3053
3147
|
if (isForced(active)) {
|
|
3054
3148
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3055
3149
|
n += 1;
|
|
@@ -3068,7 +3162,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3162
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3069
3163
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3070
3164
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3071
|
-
// move normally if sleeping/frozen/bound
|
|
3165
|
+
// move normally if sleeping/frozen/bound
|
|
3072
3166
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3073
3167
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3074
3168
|
{
|
|
@@ -3081,7 +3175,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3081
3175
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3082
3176
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3083
3177
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3084
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3178
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3085
3179
|
if (limited) {
|
|
3086
3180
|
assert(showdown);
|
|
3087
3181
|
assert(side.last_selected_move != .None);
|