@pkmn/engine 0.1.0-dev.047d6e71 → 0.1.0-dev.04a96f24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -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 +97 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +21 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +22 -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 +141 -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 +213 -75
- package/package.json +53 -33
- 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 +45 -44
- package/src/lib/common/rational.zig +121 -29
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +683 -156
- package/src/lib/gen1/chance.zig +415 -290
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +1 -2
- package/src/lib/gen1/data.zig +38 -41
- package/src/lib/gen1/helpers.zig +92 -548
- package/src/lib/gen1/mechanics.zig +280 -216
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +90 -502
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +1 -2
- package/src/lib/gen2/data.zig +26 -29
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +66 -104
- package/src/lib/node.zig +2 -4
- package/src/lib/pkmn.zig +14 -16
- package/src/lib/wasm.zig +1 -2
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const chance = @import("chance.zig");
|
|
5
4
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
5
|
+
const data = @import("data.zig");
|
|
6
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
7
|
const protocol = @import("../common/protocol.zig");
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
9
|
+
const std = @import("std");
|
|
11
10
|
|
|
12
11
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
12
|
const Boost = protocol.Boost;
|
|
13
|
+
const Choice = common.Choice;
|
|
24
14
|
const Damage = protocol.Damage;
|
|
15
|
+
const Effectiveness = data.Effectiveness;
|
|
16
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
17
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
18
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
19
|
+
const Heal = protocol.Heal;
|
|
20
|
+
const ID = common.ID;
|
|
32
21
|
const Move = data.Move;
|
|
33
22
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
23
|
+
const Player = common.Player;
|
|
24
|
+
const Result = common.Result;
|
|
25
|
+
const showdown = pkmn.options.showdown;
|
|
35
26
|
const Side = data.Side;
|
|
36
27
|
const Species = data.Species;
|
|
37
28
|
const Stats = data.Stats;
|
|
@@ -166,7 +157,7 @@ fn selectMove(
|
|
|
166
157
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
158
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
159
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
160
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
161
|
saveMove(battle, player, choice);
|
|
171
162
|
} else {
|
|
172
163
|
assert(choice.data == 0);
|
|
@@ -253,11 +244,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
244
|
active.moves = incoming.moves;
|
|
254
245
|
|
|
255
246
|
statusModify(incoming.status, &active.stats);
|
|
247
|
+
options.chance.switched(player, slot);
|
|
256
248
|
|
|
257
249
|
foe.active.volatiles.Binding = false;
|
|
258
250
|
|
|
259
251
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
252
|
|
|
262
253
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
254
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +338,13 @@ fn doTurn(
|
|
|
347
338
|
)) |r| return r;
|
|
348
339
|
if (!replace) {
|
|
349
340
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
341
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
342
|
}
|
|
352
343
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
344
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
345
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
346
|
|
|
356
|
-
options.chance.clearPending();
|
|
347
|
+
if (!showdown) options.chance.clearPending();
|
|
357
348
|
|
|
358
349
|
residual = true;
|
|
359
350
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +360,10 @@ fn doTurn(
|
|
|
369
360
|
)) |r| return r;
|
|
370
361
|
if (!replace) {
|
|
371
362
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
363
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
364
|
}
|
|
374
365
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
366
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
367
|
}
|
|
377
368
|
|
|
378
369
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -431,7 +422,7 @@ fn executeMove(
|
|
|
431
422
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
423
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
424
|
// 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
|
|
425
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
426
|
if (!auto) {
|
|
436
427
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
428
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -484,17 +475,22 @@ fn beforeMove(
|
|
|
484
475
|
|
|
485
476
|
if (Status.is(stored.status, .SLP)) {
|
|
486
477
|
const before = stored.status;
|
|
478
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
479
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
480
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
481
|
+
.started, .ended => stored.status = 0,
|
|
482
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
483
|
+
stored.status -= 1;
|
|
484
|
+
},
|
|
485
|
+
else => unreachable,
|
|
490
486
|
} else {
|
|
491
487
|
stored.status -= 1;
|
|
492
488
|
}
|
|
493
489
|
|
|
494
490
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
try options.chance.sleep(player, if (slf)
|
|
492
|
+
.None
|
|
493
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
494
|
|
|
499
495
|
if (duration == 0) {
|
|
500
496
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +528,12 @@ fn beforeMove(
|
|
|
532
528
|
}
|
|
533
529
|
|
|
534
530
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
531
|
+
volatiles.disable_duration =
|
|
532
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
533
|
+
try options.chance.disable(
|
|
534
|
+
player,
|
|
535
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
536
|
+
);
|
|
541
537
|
|
|
542
538
|
if (volatiles.disable_duration == 0) {
|
|
543
539
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +552,18 @@ fn beforeMove(
|
|
|
556
552
|
|
|
557
553
|
if (volatiles.Confusion) {
|
|
558
554
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
555
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
556
|
+
.started, .ended => volatiles.confusion = 0,
|
|
557
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
558
|
+
volatiles.confusion -= 1;
|
|
559
|
+
},
|
|
560
|
+
.overwritten => {},
|
|
561
|
+
else => unreachable,
|
|
562
562
|
} else {
|
|
563
563
|
volatiles.confusion -= 1;
|
|
564
564
|
}
|
|
565
|
-
|
|
565
|
+
|
|
566
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
567
|
|
|
567
568
|
if (volatiles.confusion == 0) {
|
|
568
569
|
volatiles.Confusion = false;
|
|
@@ -580,7 +581,9 @@ fn beforeMove(
|
|
|
580
581
|
volatiles.Charging = false;
|
|
581
582
|
volatiles.Binding = false;
|
|
582
583
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
584
|
+
|
|
585
|
+
options.chance.observe(.attacking, player, .None);
|
|
586
|
+
options.chance.observe(.binding, player, .None);
|
|
584
587
|
{
|
|
585
588
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
589
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +591,9 @@ fn beforeMove(
|
|
|
588
591
|
const def = foe.active.stats.def;
|
|
589
592
|
foe.active.stats.def = active.stats.def;
|
|
590
593
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
594
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
595
|
+
return .err;
|
|
596
|
+
}
|
|
592
597
|
}
|
|
593
598
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
599
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +622,10 @@ fn beforeMove(
|
|
|
617
622
|
volatiles.Thrashing = false;
|
|
618
623
|
volatiles.Charging = false;
|
|
619
624
|
volatiles.Binding = false;
|
|
620
|
-
|
|
625
|
+
|
|
626
|
+
options.chance.observe(.attacking, player, .None);
|
|
627
|
+
options.chance.observe(.binding, player, .None);
|
|
628
|
+
|
|
621
629
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
630
|
try log.cant(.{ ident, .Paralysis });
|
|
623
631
|
return .done;
|
|
@@ -635,12 +643,8 @@ fn beforeMove(
|
|
|
635
643
|
volatiles.state +%= battle.last_damage;
|
|
636
644
|
}
|
|
637
645
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
646
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
647
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
648
|
|
|
645
649
|
if (volatiles.attacks != 0) {
|
|
646
650
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +678,40 @@ fn beforeMove(
|
|
|
674
678
|
}
|
|
675
679
|
|
|
676
680
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
681
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
682
|
+
|
|
683
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
684
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
683
685
|
|
|
684
|
-
if (
|
|
686
|
+
if (volatiles.attacks == 0) {
|
|
687
|
+
const overwritten = volatiles.Confusion;
|
|
688
|
+
|
|
689
|
+
volatiles.Thrashing = false;
|
|
690
|
+
volatiles.Confusion = true;
|
|
691
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
692
|
+
|
|
693
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
694
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
695
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
696
|
+
// correct player)
|
|
685
697
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
699
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
700
|
+
else
|
|
701
|
+
.started);
|
|
702
|
+
if (!overwritten) options.calc.confusion(player);
|
|
686
703
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
704
|
+
|
|
705
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
706
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
707
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
708
|
+
|
|
696
709
|
return .skip_can;
|
|
697
710
|
}
|
|
698
711
|
|
|
699
712
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
713
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
714
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
715
|
|
|
707
716
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
717
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -795,7 +804,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
804
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
805
|
}
|
|
797
806
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
807
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
808
|
var active = &side.active;
|
|
800
809
|
const volatiles = &active.volatiles;
|
|
801
810
|
|
|
@@ -803,8 +812,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
812
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
813
|
if (showdown and volatiles.Transform) return;
|
|
805
814
|
|
|
815
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
816
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
817
|
+
|
|
806
818
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
819
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
820
|
}
|
|
809
821
|
|
|
810
822
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,13 +864,13 @@ fn doMove(
|
|
|
852
864
|
break :miss true;
|
|
853
865
|
}
|
|
854
866
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
867
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
868
|
};
|
|
857
869
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
870
|
|
|
859
871
|
var late = showdown and move.effect != .Explode;
|
|
860
872
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
873
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
874
|
if (showdown and move.effect.isMulti()) {
|
|
863
875
|
try Effects.multiHit(battle, player, move, options);
|
|
864
876
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +892,9 @@ fn doMove(
|
|
|
880
892
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
893
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
894
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
895
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
896
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
897
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
898
|
return @as(?Result, Result.Error);
|
|
886
899
|
}
|
|
887
900
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +908,12 @@ fn doMove(
|
|
|
895
908
|
}
|
|
896
909
|
}
|
|
897
910
|
|
|
911
|
+
var missed: bool = undefined;
|
|
898
912
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
913
|
+
if (!showdown and !skip) {
|
|
914
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
915
|
+
miss = missed or zero;
|
|
916
|
+
}
|
|
903
917
|
|
|
904
918
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
919
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +940,32 @@ fn doMove(
|
|
|
926
940
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
941
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
942
|
try log.immune(.{ foe_ident, .None });
|
|
943
|
+
if (!showdown) {
|
|
944
|
+
if (move.effect == .Binding) {
|
|
945
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
946
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
947
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
948
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
949
|
+
// (considered missing)
|
|
950
|
+
assert(zero);
|
|
951
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
952
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
953
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
954
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
955
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
956
|
+
// roll)
|
|
957
|
+
try options.chance.commit(player, .binding);
|
|
958
|
+
} else {
|
|
959
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
960
|
+
// division-by-zero freeze alternative timeline scenario
|
|
961
|
+
try options.chance.commit(player, .glitch);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
929
964
|
} else if (mist) {
|
|
930
965
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
966
|
try log.fail(.{ foe_ident, .None });
|
|
932
967
|
} else {
|
|
933
|
-
if (showdown
|
|
968
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
969
|
try log.lastmiss(.{});
|
|
935
970
|
try log.miss(.{battle.active(player)});
|
|
936
971
|
}
|
|
@@ -945,17 +980,15 @@ fn doMove(
|
|
|
945
980
|
try buildRage(battle, player.foe(), options);
|
|
946
981
|
} else if (showdown and move.effect == .Disable) {
|
|
947
982
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
983
|
}
|
|
951
984
|
return null;
|
|
952
|
-
} else {
|
|
985
|
+
} else if (!showdown) {
|
|
953
986
|
try options.chance.commit(player, .hit);
|
|
954
987
|
}
|
|
955
988
|
|
|
956
989
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
990
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
991
|
+
// it up above before damage calculation)
|
|
959
992
|
if (!showdown and move.effect.isMulti()) {
|
|
960
993
|
try Effects.multiHit(battle, player, move, options);
|
|
961
994
|
hits = side.active.volatiles.attacks;
|
|
@@ -994,32 +1027,30 @@ fn doMove(
|
|
|
994
1027
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1028
|
}
|
|
996
1029
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1030
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1031
|
// 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;
|
|
1032
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1033
|
+
if (immune) {
|
|
1034
|
+
battle.last_damage = 0;
|
|
1035
|
+
assert(foe.stored().hp > 0);
|
|
1036
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1037
|
+
if (sub) {
|
|
1038
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1039
|
+
} else {
|
|
1040
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1041
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1042
|
}
|
|
1043
|
+
return null;
|
|
1013
1044
|
}
|
|
1014
1045
|
}
|
|
1015
1046
|
|
|
1016
1047
|
// 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
|
|
1048
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1049
|
if (nullified) return null;
|
|
1019
1050
|
|
|
1020
1051
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1052
|
// 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
|
|
1053
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1054
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1055
|
|
|
1025
1056
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1065,7 @@ fn doMove(
|
|
|
1034
1065
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1066
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1067
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1068
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1069
|
if (move.effect == .Twineedle) {
|
|
1039
1070
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1071
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1089,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1089
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1090
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1091
|
|
|
1061
|
-
|
|
1092
|
+
const spurious =
|
|
1093
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1094
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1095
|
}
|
|
1063
1096
|
|
|
1064
1097
|
fn calcDamage(
|
|
@@ -1067,6 +1100,7 @@ fn calcDamage(
|
|
|
1067
1100
|
target_player: Player,
|
|
1068
1101
|
m: ?Move.Data,
|
|
1069
1102
|
crit: bool,
|
|
1103
|
+
options: anytype,
|
|
1070
1104
|
) bool {
|
|
1071
1105
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1106
|
const cfz = m == null;
|
|
@@ -1078,28 +1112,8 @@ fn calcDamage(
|
|
|
1078
1112
|
|
|
1079
1113
|
const special = move.type.special();
|
|
1080
1114
|
|
|
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
|
|
1115
|
+
var atk = attack(side, crit, special);
|
|
1116
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1117
|
|
|
1104
1118
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1119
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1124,11 +1138,41 @@ fn calcDamage(
|
|
|
1124
1138
|
d = @min(997, d);
|
|
1125
1139
|
d += 2;
|
|
1126
1140
|
|
|
1141
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1142
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1143
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1144
|
+
// that results in counter intuitive probabilities in the common case
|
|
1145
|
+
if (pkmn.options.chance and !showdown) {
|
|
1146
|
+
atk = attack(side, !crit, special);
|
|
1147
|
+
def = defense(target, !crit, special, false);
|
|
1148
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1127
1151
|
battle.last_damage = @intCast(d);
|
|
1128
1152
|
|
|
1129
1153
|
return true;
|
|
1130
1154
|
}
|
|
1131
1155
|
|
|
1156
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1157
|
+
return if (crit)
|
|
1158
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1159
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1163
|
+
return if (crit)
|
|
1164
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1165
|
+
else
|
|
1166
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1167
|
+
if (special)
|
|
1168
|
+
target.active.stats.spc *
|
|
1169
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1170
|
+
else
|
|
1171
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1172
|
+
target.active.stats.def *
|
|
1173
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1132
1176
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1177
|
const side = battle.side(player);
|
|
1134
1178
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1217,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1217
|
const foe = battle.foe(player);
|
|
1174
1218
|
|
|
1175
1219
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1220
|
|
|
1178
1221
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1222
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1234,9 +1277,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1277
|
|
|
1235
1278
|
battle.last_damage *|= 2;
|
|
1236
1279
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1280
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1281
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1282
|
|
|
1241
1283
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1284
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1307,7 @@ fn applyDamage(
|
|
|
1265
1307
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1308
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1309
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1310
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1311
|
return true;
|
|
1270
1312
|
} else {
|
|
1271
1313
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1341,7 @@ fn mirrorMove(
|
|
|
1299
1341
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1342
|
return null;
|
|
1301
1343
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1344
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1345
|
}
|
|
1304
1346
|
|
|
1305
1347
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1362,7 @@ fn metronome(
|
|
|
1320
1362
|
var side = battle.side(player);
|
|
1321
1363
|
|
|
1322
1364
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1365
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1366
|
|
|
1325
1367
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1368
|
return null;
|
|
@@ -1332,7 +1374,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1374
|
var immune = false;
|
|
1333
1375
|
var mist = false;
|
|
1334
1376
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1377
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1378
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1336
1381
|
|
|
1337
1382
|
assert(!immune);
|
|
1338
1383
|
if (mist) {
|
|
@@ -1341,11 +1386,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1386
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1387
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1388
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1389
|
try options.log.lastmiss(.{});
|
|
1346
1390
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1391
|
}
|
|
1348
1392
|
|
|
1393
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1394
|
return false;
|
|
1350
1395
|
}
|
|
1351
1396
|
|
|
@@ -1356,7 +1401,7 @@ fn moveHit(
|
|
|
1356
1401
|
immune: *bool,
|
|
1357
1402
|
mist: *bool,
|
|
1358
1403
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1404
|
+
) !bool {
|
|
1360
1405
|
var side = battle.side(player);
|
|
1361
1406
|
const foe = battle.foe(player);
|
|
1362
1407
|
|
|
@@ -1407,7 +1452,7 @@ fn moveHit(
|
|
|
1407
1452
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1453
|
|
|
1409
1454
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1455
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1456
|
};
|
|
1412
1457
|
|
|
1413
1458
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1463,17 @@ fn moveHit(
|
|
|
1418
1463
|
|
|
1419
1464
|
if (!miss) return true;
|
|
1420
1465
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1466
|
+
|
|
1421
1467
|
side.active.volatiles.Binding = false;
|
|
1468
|
+
options.chance.observe(.binding, player, .None);
|
|
1469
|
+
|
|
1422
1470
|
return false;
|
|
1423
1471
|
}
|
|
1424
1472
|
|
|
1425
1473
|
fn checkFaint(
|
|
1426
1474
|
battle: anytype,
|
|
1427
1475
|
player: Player,
|
|
1476
|
+
cap: bool,
|
|
1428
1477
|
options: anytype,
|
|
1429
1478
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1479
|
const side = battle.side(player);
|
|
@@ -1437,8 +1486,8 @@ fn checkFaint(
|
|
|
1437
1486
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1487
|
const more = player_out or foe_out;
|
|
1439
1488
|
|
|
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;
|
|
1489
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1490
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1491
|
|
|
1443
1492
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1493
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1508,7 @@ fn checkFaint(
|
|
|
1459
1508
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1509
|
}
|
|
1461
1510
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1511
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1512
|
var side = battle.side(player);
|
|
1464
1513
|
var foe = battle.foe(player);
|
|
1465
1514
|
assert(side.stored().hp == 0);
|
|
@@ -1493,7 +1542,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1542
|
}
|
|
1494
1543
|
|
|
1495
1544
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1545
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1546
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1547
|
+
if (cap) options.calc.capped(player);
|
|
1497
1548
|
return null;
|
|
1498
1549
|
}
|
|
1499
1550
|
|
|
@@ -1653,17 +1704,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1704
|
}
|
|
1654
1705
|
}
|
|
1655
1706
|
|
|
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
1707
|
fn onBegin(
|
|
1668
1708
|
battle: anytype,
|
|
1669
1709
|
player: Player,
|
|
@@ -1750,8 +1790,10 @@ pub const Effects = struct {
|
|
|
1750
1790
|
|
|
1751
1791
|
side.active.volatiles.Bide = true;
|
|
1752
1792
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1793
|
+
|
|
1753
1794
|
side.active.volatiles.state = 0;
|
|
1754
1795
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1796
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1797
|
|
|
1756
1798
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1799
|
}
|
|
@@ -1813,12 +1855,14 @@ pub const Effects = struct {
|
|
|
1813
1855
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1856
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1857
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1858
|
}
|
|
1818
1859
|
|
|
1819
1860
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1861
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1862
|
+
|
|
1863
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1864
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1865
|
+
options.calc.confusion(player.foe());
|
|
1822
1866
|
|
|
1823
1867
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1868
|
}
|
|
@@ -1846,10 +1890,7 @@ pub const Effects = struct {
|
|
|
1846
1890
|
const foe_ident = battle.active(player.foe());
|
|
1847
1891
|
|
|
1848
1892
|
// 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
|
-
}
|
|
1893
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1894
|
|
|
1854
1895
|
if (volatiles.disable_move != 0) {
|
|
1855
1896
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1879,6 +1920,7 @@ pub const Effects = struct {
|
|
|
1879
1920
|
volatiles.disable_move =
|
|
1880
1921
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1922
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1923
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1924
|
|
|
1883
1925
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1926
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1985,22 +2027,24 @@ pub const Effects = struct {
|
|
|
1985
2027
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2028
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2029
|
s.stored().status = 0;
|
|
2030
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2031
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2032
|
s.stored().status = Status.init(.PSN);
|
|
1990
2033
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2034
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2035
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2036
|
}
|
|
1994
2037
|
} else {
|
|
1995
2038
|
if (Status.any(foe_stored.status)) {
|
|
1996
2039
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2040
|
foe.last_selected_move = .SKIP_TURN;
|
|
2041
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2042
|
}
|
|
1999
2043
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2044
|
foe_stored.status = 0;
|
|
2001
2045
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2046
|
+
try clearVolatiles(battle, player, options);
|
|
2047
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2048
|
}
|
|
2005
2049
|
}
|
|
2006
2050
|
|
|
@@ -2051,7 +2095,6 @@ pub const Effects = struct {
|
|
|
2051
2095
|
}
|
|
2052
2096
|
}
|
|
2053
2097
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2098
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2099
|
|
|
2057
2100
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2135,10 @@ pub const Effects = struct {
|
|
|
2092
2135
|
if (!has_mimic) {
|
|
2093
2136
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2137
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2138
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2139
|
}
|
|
2098
2140
|
}
|
|
2099
2141
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2142
|
|
|
2102
2143
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2144
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2192,6 @@ pub const Effects = struct {
|
|
|
2151
2192
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2193
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2194
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2195
|
|
|
2156
2196
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2197
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2259,6 @@ pub const Effects = struct {
|
|
|
2219
2259
|
|
|
2220
2260
|
if (move.effect == .Poison) {
|
|
2221
2261
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2262
|
} else {
|
|
2224
2263
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2264
|
return;
|
|
@@ -2288,9 +2327,8 @@ pub const Effects = struct {
|
|
|
2288
2327
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2328
|
});
|
|
2290
2329
|
}
|
|
2291
|
-
// If
|
|
2330
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2331
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2332
|
}
|
|
2295
2333
|
|
|
2296
2334
|
// Sleep Clause Mod
|
|
@@ -2302,6 +2340,8 @@ pub const Effects = struct {
|
|
|
2302
2340
|
foe.active.volatiles.Recharging = false;
|
|
2303
2341
|
|
|
2304
2342
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2343
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2344
|
+
|
|
2305
2345
|
const last = battle.side(player).last_selected_move;
|
|
2306
2346
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2347
|
}
|
|
@@ -2343,9 +2383,7 @@ pub const Effects = struct {
|
|
|
2343
2383
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2384
|
if (showdown) {
|
|
2345
2385
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2386
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2387
|
battle.last_damage = 0;
|
|
2350
2388
|
} else {
|
|
2351
2389
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2398,8 @@ pub const Effects = struct {
|
|
|
2360
2398
|
|
|
2361
2399
|
volatiles.Thrashing = true;
|
|
2362
2400
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2401
|
+
|
|
2402
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2403
|
}
|
|
2364
2404
|
|
|
2365
2405
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2405,6 +2445,7 @@ pub const Effects = struct {
|
|
|
2405
2445
|
|
|
2406
2446
|
side.active.volatiles.attacks =
|
|
2407
2447
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2448
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2449
|
}
|
|
2409
2450
|
|
|
2410
2451
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2562,6 @@ pub const Effects = struct {
|
|
|
2521
2562
|
} else {
|
|
2522
2563
|
// checkHit already checks for Invulnerable
|
|
2523
2564
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2565
|
}
|
|
2526
2566
|
|
|
2527
2567
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2675,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2675
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2676
|
}
|
|
2637
2677
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2678
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2679
|
var log = options.log;
|
|
2640
2680
|
var side = battle.side(who);
|
|
2641
2681
|
var volatiles = &side.active.volatiles;
|
|
2642
2682
|
const ident = battle.active(who);
|
|
2643
2683
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2684
|
if (volatiles.disable_move != 0) {
|
|
2647
2685
|
volatiles.disable_move = 0;
|
|
2648
2686
|
volatiles.disable_duration = 0;
|
|
2687
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2688
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2689
|
}
|
|
2651
2690
|
if (volatiles.Confusion) {
|
|
2652
2691
|
// volatiles.confusion is left unchanged
|
|
2653
2692
|
volatiles.Confusion = false;
|
|
2693
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2694
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2695
|
}
|
|
2656
2696
|
if (volatiles.Mist) {
|
|
@@ -2685,6 +2725,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2725
|
}
|
|
2686
2726
|
}
|
|
2687
2727
|
|
|
2728
|
+
fn decrement(
|
|
2729
|
+
comptime field: chance.Action.Field,
|
|
2730
|
+
player: Player,
|
|
2731
|
+
options: anytype,
|
|
2732
|
+
n: anytype,
|
|
2733
|
+
) @TypeOf(n) {
|
|
2734
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2735
|
+
.started, .ended => 0,
|
|
2736
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2737
|
+
else => unreachable,
|
|
2738
|
+
} else n - 1;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2688
2741
|
pub const Rolls = struct {
|
|
2689
2742
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2743
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2700,7 +2753,13 @@ pub const Rolls = struct {
|
|
|
2700
2753
|
return p1;
|
|
2701
2754
|
}
|
|
2702
2755
|
|
|
2703
|
-
fn criticalHit(
|
|
2756
|
+
fn criticalHit(
|
|
2757
|
+
battle: anytype,
|
|
2758
|
+
player: Player,
|
|
2759
|
+
rate: u8,
|
|
2760
|
+
spurious: bool,
|
|
2761
|
+
options: anytype,
|
|
2762
|
+
) !bool {
|
|
2704
2763
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2764
|
val == .true
|
|
2706
2765
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2769,7 @@ pub const Rolls = struct {
|
|
|
2710
2769
|
else
|
|
2711
2770
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2771
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2772
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2773
|
return crit;
|
|
2715
2774
|
}
|
|
2716
2775
|
|
|
@@ -2732,17 +2791,19 @@ pub const Rolls = struct {
|
|
|
2732
2791
|
return roll;
|
|
2733
2792
|
}
|
|
2734
2793
|
|
|
2735
|
-
|
|
2794
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2795
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2796
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2797
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2798
|
val == .true
|
|
2738
2799
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2800
|
+
battle.rng.hit(player, acc)
|
|
2740
2801
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2802
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2803
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2804
|
+
battle.rng.next() < acc;
|
|
2744
2805
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2806
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2807
|
return ok;
|
|
2747
2808
|
}
|
|
2748
2809
|
|
|
@@ -2874,9 +2935,10 @@ pub const Rolls = struct {
|
|
|
2874
2935
|
}
|
|
2875
2936
|
|
|
2876
2937
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2877
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2878
|
-
@intCast(val)
|
|
2879
|
-
else
|
|
2938
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2939
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2940
|
+
else
|
|
2941
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2880
2942
|
battle.rng.sleepDuration(player)
|
|
2881
2943
|
else if (showdown)
|
|
2882
2944
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,14 +2950,15 @@ pub const Rolls = struct {
|
|
|
2888
2950
|
};
|
|
2889
2951
|
|
|
2890
2952
|
assert(duration >= 1 and duration <= 7);
|
|
2891
|
-
options.chance.duration(
|
|
2953
|
+
options.chance.duration(player, duration);
|
|
2892
2954
|
return duration;
|
|
2893
2955
|
}
|
|
2894
2956
|
|
|
2895
2957
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2896
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
val
|
|
2898
|
-
else
|
|
2958
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2959
|
+
if (val >= 1 and val <= 8) val else null
|
|
2960
|
+
else
|
|
2961
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2899
2962
|
battle.rng.disableDuration(player)
|
|
2900
2963
|
else if (showdown)
|
|
2901
2964
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,29 +2966,31 @@ pub const Rolls = struct {
|
|
|
2903
2966
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2904
2967
|
|
|
2905
2968
|
assert(duration >= 1 and duration <= 8);
|
|
2906
|
-
options.chance.duration(
|
|
2969
|
+
options.chance.duration(player, duration);
|
|
2907
2970
|
return duration;
|
|
2908
2971
|
}
|
|
2909
2972
|
|
|
2910
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2911
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2912
|
-
@intCast(val)
|
|
2913
|
-
else
|
|
2914
|
-
battle.rng
|
|
2973
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2974
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2975
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
2976
|
+
else
|
|
2977
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2978
|
+
battle.rng.confusionDuration(player)
|
|
2915
2979
|
else if (showdown)
|
|
2916
2980
|
battle.rng.range(u3, 2, 6)
|
|
2917
2981
|
else
|
|
2918
2982
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2919
2983
|
|
|
2920
2984
|
assert(duration >= 2 and duration <= 5);
|
|
2921
|
-
options.chance.duration(
|
|
2985
|
+
options.chance.duration(player, duration);
|
|
2922
2986
|
return duration;
|
|
2923
2987
|
}
|
|
2924
2988
|
|
|
2925
2989
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2926
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2927
|
-
@intCast(val)
|
|
2928
|
-
else
|
|
2990
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2991
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
2992
|
+
else
|
|
2993
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2929
2994
|
battle.rng.attackingDuration(player)
|
|
2930
2995
|
else if (showdown)
|
|
2931
2996
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2933,7 +2998,7 @@ pub const Rolls = struct {
|
|
|
2933
2998
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2934
2999
|
|
|
2935
3000
|
assert(duration >= 2 and duration <= 3);
|
|
2936
|
-
options.chance.duration(
|
|
3001
|
+
options.chance.duration(player, duration);
|
|
2937
3002
|
return duration;
|
|
2938
3003
|
}
|
|
2939
3004
|
|
|
@@ -2946,9 +3011,10 @@ pub const Rolls = struct {
|
|
|
2946
3011
|
options: anytype,
|
|
2947
3012
|
) !u3 {
|
|
2948
3013
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2949
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2950
|
-
@intCast(val)
|
|
2951
|
-
else
|
|
3014
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3015
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3016
|
+
else
|
|
3017
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2952
3018
|
battle.rng.distribution(player)
|
|
2953
3019
|
else if (showdown)
|
|
2954
3020
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2959,7 +3025,7 @@ pub const Rolls = struct {
|
|
|
2959
3025
|
|
|
2960
3026
|
assert(n >= 2 and n <= 5);
|
|
2961
3027
|
if (effect == .Binding) {
|
|
2962
|
-
options.chance.duration(
|
|
3028
|
+
options.chance.duration(player, n);
|
|
2963
3029
|
} else {
|
|
2964
3030
|
try options.chance.multiHit(player, n);
|
|
2965
3031
|
}
|
|
@@ -2974,15 +3040,13 @@ pub const Rolls = struct {
|
|
|
2974
3040
|
options: anytype,
|
|
2975
3041
|
) !u4 {
|
|
2976
3042
|
// TODO: consider throwing error instead of rerolling?
|
|
2977
|
-
const
|
|
3043
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2978
3044
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2979
3045
|
val
|
|
2980
3046
|
else
|
|
2981
3047
|
null
|
|
2982
3048
|
else
|
|
2983
|
-
null
|
|
2984
|
-
|
|
2985
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3049
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2986
3050
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2987
3051
|
else slot: {
|
|
2988
3052
|
if (showdown) {
|
|
@@ -3068,7 +3132,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3132
|
const stored = side.stored();
|
|
3069
3133
|
|
|
3070
3134
|
// While players are not given any input options on the cartridge in these cases,
|
|
3071
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3135
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3136
|
//
|
|
3073
3137
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3138
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,7 +3140,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3140
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3141
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3142
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3143
|
+
// switch at all
|
|
3080
3144
|
if (isForced(active)) {
|
|
3081
3145
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3146
|
n += 1;
|
|
@@ -3095,7 +3159,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3159
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3160
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3161
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3162
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3163
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3164
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3165
|
{
|
|
@@ -3108,7 +3172,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3172
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3173
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3174
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3111
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3175
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3176
|
if (limited) {
|
|
3113
3177
|
assert(showdown);
|
|
3114
3178
|
assert(side.last_selected_move != .None);
|