@pkmn/engine 0.1.0-dev.32adfe77 → 0.1.0-dev.3393edb3
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 +43 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +96 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon.d.ts +2 -0
- package/build/pkg/addon.js +35 -58
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +25 -4
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +22 -9
- package/build/pkg/index.js +15 -10
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +36 -70
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +132 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +186 -65
- package/package.json +46 -29
- 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 +32 -6
- 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 +46 -36
- package/src/lib/common/rational.zig +114 -24
- package/src/lib/common/rng.zig +4 -4
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +663 -130
- package/src/lib/gen1/chance.zig +384 -275
- package/src/lib/gen1/data.zig +31 -30
- package/src/lib/gen1/helpers.zig +89 -538
- package/src/lib/gen1/mechanics.zig +275 -197
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +90 -502
- package/src/lib/gen2/data.zig +15 -14
- package/src/lib/gen2/helpers.zig +41 -13
- package/src/lib/gen2/mechanics.zig +158 -103
- package/src/lib/pkmn.zig +10 -16
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +351 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
|
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
9
|
|
|
10
10
|
const data = @import("data.zig");
|
|
11
|
+
const chance = @import("chance.zig");
|
|
11
12
|
|
|
12
13
|
const assert = std.debug.assert;
|
|
13
14
|
|
|
@@ -27,11 +28,9 @@ const Heal = protocol.Heal;
|
|
|
27
28
|
|
|
28
29
|
const Gen12 = rng.Gen12;
|
|
29
30
|
|
|
30
|
-
const ActivePokemon = data.ActivePokemon;
|
|
31
31
|
const Effectiveness = data.Effectiveness;
|
|
32
32
|
const Move = data.Move;
|
|
33
33
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const Pokemon = data.Pokemon;
|
|
35
34
|
const Side = data.Side;
|
|
36
35
|
const Species = data.Species;
|
|
37
36
|
const Stats = data.Stats;
|
|
@@ -166,7 +165,7 @@ fn selectMove(
|
|
|
166
165
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
166
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
167
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
168
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
169
|
saveMove(battle, player, choice);
|
|
171
170
|
} else {
|
|
172
171
|
assert(choice.data == 0);
|
|
@@ -253,11 +252,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
252
|
active.moves = incoming.moves;
|
|
254
253
|
|
|
255
254
|
statusModify(incoming.status, &active.stats);
|
|
255
|
+
options.chance.switched(player, slot);
|
|
256
256
|
|
|
257
257
|
foe.active.volatiles.Binding = false;
|
|
258
258
|
|
|
259
259
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
260
|
|
|
262
261
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
262
|
incoming.status = Status.init(.PSN);
|
|
@@ -342,18 +341,19 @@ fn doTurn(
|
|
|
342
341
|
player_choice,
|
|
343
342
|
player_rewrap,
|
|
344
343
|
player_skip,
|
|
344
|
+
true,
|
|
345
345
|
&residual,
|
|
346
346
|
options,
|
|
347
347
|
)) |r| return r;
|
|
348
348
|
if (!replace) {
|
|
349
349
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
350
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
351
|
}
|
|
352
352
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
353
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
354
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
355
|
|
|
356
|
-
options.chance.clearPending();
|
|
356
|
+
if (!showdown) options.chance.clearPending();
|
|
357
357
|
|
|
358
358
|
residual = true;
|
|
359
359
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -364,15 +364,16 @@ fn doTurn(
|
|
|
364
364
|
foe_choice,
|
|
365
365
|
foe_rewrap,
|
|
366
366
|
foe_skip,
|
|
367
|
+
false,
|
|
367
368
|
&residual,
|
|
368
369
|
options,
|
|
369
370
|
)) |r| return r;
|
|
370
371
|
if (!replace) {
|
|
371
372
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
373
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
374
|
}
|
|
374
375
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
376
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -391,6 +392,7 @@ fn executeMove(
|
|
|
391
392
|
choice: Choice,
|
|
392
393
|
rewrap: bool,
|
|
393
394
|
skip: bool,
|
|
395
|
+
first: bool,
|
|
394
396
|
residual: *bool,
|
|
395
397
|
options: anytype,
|
|
396
398
|
) !?Result {
|
|
@@ -431,7 +433,7 @@ fn executeMove(
|
|
|
431
433
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
434
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
435
|
// 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
|
|
436
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
437
|
if (!auto) {
|
|
436
438
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
439
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -450,7 +452,7 @@ fn executeMove(
|
|
|
450
452
|
|
|
451
453
|
var skip_can = false;
|
|
452
454
|
var skip_pp = false;
|
|
453
|
-
switch (try beforeMove(battle, player, skip, residual, options)) {
|
|
455
|
+
switch (try beforeMove(battle, player, skip, first, residual, options)) {
|
|
454
456
|
.done => return null,
|
|
455
457
|
.skip_can => skip_can = true,
|
|
456
458
|
.skip_pp => skip_pp = true,
|
|
@@ -471,6 +473,7 @@ fn beforeMove(
|
|
|
471
473
|
battle: anytype,
|
|
472
474
|
player: Player,
|
|
473
475
|
skip: bool,
|
|
476
|
+
first: bool,
|
|
474
477
|
residual: *bool,
|
|
475
478
|
options: anytype,
|
|
476
479
|
) !BeforeMove {
|
|
@@ -484,17 +487,22 @@ fn beforeMove(
|
|
|
484
487
|
|
|
485
488
|
if (Status.is(stored.status, .SLP)) {
|
|
486
489
|
const before = stored.status;
|
|
490
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
491
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
492
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
493
|
+
.started, .ended => stored.status = 0,
|
|
494
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
495
|
+
stored.status -= 1;
|
|
496
|
+
},
|
|
497
|
+
else => unreachable,
|
|
490
498
|
} else {
|
|
491
499
|
stored.status -= 1;
|
|
492
500
|
}
|
|
493
501
|
|
|
494
502
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
503
|
+
try options.chance.sleep(player, if (slf)
|
|
504
|
+
.None
|
|
505
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
506
|
|
|
499
507
|
if (duration == 0) {
|
|
500
508
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +540,12 @@ fn beforeMove(
|
|
|
532
540
|
}
|
|
533
541
|
|
|
534
542
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
543
|
+
volatiles.disable_duration =
|
|
544
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
545
|
+
try options.chance.disable(
|
|
546
|
+
player,
|
|
547
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
548
|
+
);
|
|
541
549
|
|
|
542
550
|
if (volatiles.disable_duration == 0) {
|
|
543
551
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +564,25 @@ fn beforeMove(
|
|
|
556
564
|
|
|
557
565
|
if (volatiles.Confusion) {
|
|
558
566
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
567
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
568
|
+
.started => {
|
|
569
|
+
if (!first or (options.calc.overridden(player, .confused) orelse .None) != .None) {
|
|
570
|
+
volatiles.confusion -= 1;
|
|
571
|
+
} else {
|
|
572
|
+
volatiles.confusion = 0;
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
576
|
+
volatiles.confusion -= 1;
|
|
577
|
+
},
|
|
578
|
+
.ended => volatiles.confusion = 0,
|
|
579
|
+
.overwritten => {},
|
|
580
|
+
else => unreachable,
|
|
562
581
|
} else {
|
|
563
582
|
volatiles.confusion -= 1;
|
|
564
583
|
}
|
|
565
|
-
|
|
584
|
+
|
|
585
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
586
|
|
|
567
587
|
if (volatiles.confusion == 0) {
|
|
568
588
|
volatiles.Confusion = false;
|
|
@@ -580,7 +600,9 @@ fn beforeMove(
|
|
|
580
600
|
volatiles.Charging = false;
|
|
581
601
|
volatiles.Binding = false;
|
|
582
602
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
603
|
+
|
|
604
|
+
options.chance.observe(.attacking, player, .None);
|
|
605
|
+
options.chance.observe(.binding, player, .None);
|
|
584
606
|
{
|
|
585
607
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
608
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +610,9 @@ fn beforeMove(
|
|
|
588
610
|
const def = foe.active.stats.def;
|
|
589
611
|
foe.active.stats.def = active.stats.def;
|
|
590
612
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
613
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
614
|
+
return .err;
|
|
615
|
+
}
|
|
592
616
|
}
|
|
593
617
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
618
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +641,10 @@ fn beforeMove(
|
|
|
617
641
|
volatiles.Thrashing = false;
|
|
618
642
|
volatiles.Charging = false;
|
|
619
643
|
volatiles.Binding = false;
|
|
620
|
-
|
|
644
|
+
|
|
645
|
+
options.chance.observe(.attacking, player, .None);
|
|
646
|
+
options.chance.observe(.binding, player, .None);
|
|
647
|
+
|
|
621
648
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
649
|
try log.cant(.{ ident, .Paralysis });
|
|
623
650
|
return .done;
|
|
@@ -635,12 +662,8 @@ fn beforeMove(
|
|
|
635
662
|
volatiles.state +%= battle.last_damage;
|
|
636
663
|
}
|
|
637
664
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
665
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
666
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
667
|
|
|
645
668
|
if (volatiles.attacks != 0) {
|
|
646
669
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +697,36 @@ fn beforeMove(
|
|
|
674
697
|
}
|
|
675
698
|
|
|
676
699
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
700
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
701
|
+
|
|
702
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
703
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
704
|
+
|
|
705
|
+
if (volatiles.attacks == 0) {
|
|
706
|
+
const overwritten = pkmn.options.overwrite and volatiles.Confusion;
|
|
707
|
+
|
|
708
|
+
volatiles.Thrashing = false;
|
|
709
|
+
volatiles.Confusion = true;
|
|
710
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
683
711
|
|
|
684
|
-
|
|
712
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
713
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
714
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
715
|
+
// correct player)
|
|
685
716
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
717
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
686
718
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
719
|
+
|
|
720
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
721
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
722
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
723
|
+
|
|
696
724
|
return .skip_can;
|
|
697
725
|
}
|
|
698
726
|
|
|
699
727
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
728
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
729
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
730
|
|
|
707
731
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
732
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -795,7 +819,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
819
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
820
|
}
|
|
797
821
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
822
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
823
|
var active = &side.active;
|
|
800
824
|
const volatiles = &active.volatiles;
|
|
801
825
|
|
|
@@ -803,8 +827,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
827
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
828
|
if (showdown and volatiles.Transform) return;
|
|
805
829
|
|
|
830
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
831
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
832
|
+
|
|
806
833
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
834
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
835
|
}
|
|
809
836
|
|
|
810
837
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,13 +879,13 @@ fn doMove(
|
|
|
852
879
|
break :miss true;
|
|
853
880
|
}
|
|
854
881
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
882
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
883
|
};
|
|
857
884
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
885
|
|
|
859
886
|
var late = showdown and move.effect != .Explode;
|
|
860
887
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
888
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
889
|
if (showdown and move.effect.isMulti()) {
|
|
863
890
|
try Effects.multiHit(battle, player, move, options);
|
|
864
891
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +907,9 @@ fn doMove(
|
|
|
880
907
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
908
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
909
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
910
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
911
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
912
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
913
|
return @as(?Result, Result.Error);
|
|
886
914
|
}
|
|
887
915
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +923,12 @@ fn doMove(
|
|
|
895
923
|
}
|
|
896
924
|
}
|
|
897
925
|
|
|
926
|
+
var missed: bool = undefined;
|
|
898
927
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
928
|
+
if (!showdown and !skip) {
|
|
929
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
930
|
+
miss = missed or zero;
|
|
931
|
+
}
|
|
903
932
|
|
|
904
933
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
934
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +955,32 @@ fn doMove(
|
|
|
926
955
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
956
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
957
|
try log.immune(.{ foe_ident, .None });
|
|
958
|
+
if (!showdown) {
|
|
959
|
+
if (move.effect == .Binding) {
|
|
960
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
961
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
962
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
963
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
964
|
+
// (considered missing)
|
|
965
|
+
assert(zero);
|
|
966
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
967
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
968
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
969
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
970
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
971
|
+
// roll)
|
|
972
|
+
try options.chance.commit(player, .binding);
|
|
973
|
+
} else {
|
|
974
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
975
|
+
// division-by-zero freeze alternative timeline scenario
|
|
976
|
+
try options.chance.commit(player, .glitch);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
929
979
|
} else if (mist) {
|
|
930
980
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
981
|
try log.fail(.{ foe_ident, .None });
|
|
932
982
|
} else {
|
|
933
|
-
if (showdown
|
|
983
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
984
|
try log.lastmiss(.{});
|
|
935
985
|
try log.miss(.{battle.active(player)});
|
|
936
986
|
}
|
|
@@ -945,17 +995,15 @@ fn doMove(
|
|
|
945
995
|
try buildRage(battle, player.foe(), options);
|
|
946
996
|
} else if (showdown and move.effect == .Disable) {
|
|
947
997
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
998
|
}
|
|
951
999
|
return null;
|
|
952
|
-
} else {
|
|
1000
|
+
} else if (!showdown) {
|
|
953
1001
|
try options.chance.commit(player, .hit);
|
|
954
1002
|
}
|
|
955
1003
|
|
|
956
1004
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
1005
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
1006
|
+
// it up above before damage calculation)
|
|
959
1007
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1008
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1009
|
hits = side.active.volatiles.attacks;
|
|
@@ -994,32 +1042,30 @@ fn doMove(
|
|
|
994
1042
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1043
|
}
|
|
996
1044
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1045
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1046
|
// 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;
|
|
1047
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1048
|
+
if (immune) {
|
|
1049
|
+
battle.last_damage = 0;
|
|
1050
|
+
assert(foe.stored().hp > 0);
|
|
1051
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1052
|
+
if (sub) {
|
|
1053
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1054
|
+
} else {
|
|
1055
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1056
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1057
|
}
|
|
1058
|
+
return null;
|
|
1013
1059
|
}
|
|
1014
1060
|
}
|
|
1015
1061
|
|
|
1016
1062
|
// 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
|
|
1063
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1064
|
if (nullified) return null;
|
|
1019
1065
|
|
|
1020
1066
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1067
|
// 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
|
|
1068
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1069
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1070
|
|
|
1025
1071
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1080,7 @@ fn doMove(
|
|
|
1034
1080
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1081
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1082
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1083
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1084
|
if (move.effect == .Twineedle) {
|
|
1039
1085
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1086
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1104,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1104
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1105
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1106
|
|
|
1061
|
-
|
|
1107
|
+
const spurious =
|
|
1108
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1109
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1110
|
}
|
|
1063
1111
|
|
|
1064
1112
|
fn calcDamage(
|
|
@@ -1067,6 +1115,7 @@ fn calcDamage(
|
|
|
1067
1115
|
target_player: Player,
|
|
1068
1116
|
m: ?Move.Data,
|
|
1069
1117
|
crit: bool,
|
|
1118
|
+
options: anytype,
|
|
1070
1119
|
) bool {
|
|
1071
1120
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1121
|
const cfz = m == null;
|
|
@@ -1078,28 +1127,8 @@ fn calcDamage(
|
|
|
1078
1127
|
|
|
1079
1128
|
const special = move.type.special();
|
|
1080
1129
|
|
|
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
|
|
1130
|
+
var atk = attack(side, crit, special);
|
|
1131
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1132
|
|
|
1104
1133
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1134
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1124,11 +1153,41 @@ fn calcDamage(
|
|
|
1124
1153
|
d = @min(997, d);
|
|
1125
1154
|
d += 2;
|
|
1126
1155
|
|
|
1156
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1157
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1158
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1159
|
+
// that results in counter intuitive probabilities in the common case
|
|
1160
|
+
if (pkmn.options.chance and !showdown) {
|
|
1161
|
+
atk = attack(side, !crit, special);
|
|
1162
|
+
def = defense(target, !crit, special, false);
|
|
1163
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1127
1166
|
battle.last_damage = @intCast(d);
|
|
1128
1167
|
|
|
1129
1168
|
return true;
|
|
1130
1169
|
}
|
|
1131
1170
|
|
|
1171
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1172
|
+
return if (crit)
|
|
1173
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1174
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1180
|
+
else
|
|
1181
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1182
|
+
if (special)
|
|
1183
|
+
target.active.stats.spc *
|
|
1184
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1185
|
+
else
|
|
1186
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1187
|
+
target.active.stats.def *
|
|
1188
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1132
1191
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1192
|
const side = battle.side(player);
|
|
1134
1193
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1232,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1232
|
const foe = battle.foe(player);
|
|
1174
1233
|
|
|
1175
1234
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1235
|
|
|
1178
1236
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1237
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1234,9 +1292,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1292
|
|
|
1235
1293
|
battle.last_damage *|= 2;
|
|
1236
1294
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1295
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1296
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1297
|
|
|
1241
1298
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1299
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1322,7 @@ fn applyDamage(
|
|
|
1265
1322
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1323
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1324
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1325
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1326
|
return true;
|
|
1270
1327
|
} else {
|
|
1271
1328
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1356,7 @@ fn mirrorMove(
|
|
|
1299
1356
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1357
|
return null;
|
|
1301
1358
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1359
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1360
|
}
|
|
1304
1361
|
|
|
1305
1362
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1377,7 @@ fn metronome(
|
|
|
1320
1377
|
var side = battle.side(player);
|
|
1321
1378
|
|
|
1322
1379
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1380
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1381
|
|
|
1325
1382
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1383
|
return null;
|
|
@@ -1332,7 +1389,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1389
|
var immune = false;
|
|
1333
1390
|
var mist = false;
|
|
1334
1391
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1392
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1393
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1336
1396
|
|
|
1337
1397
|
assert(!immune);
|
|
1338
1398
|
if (mist) {
|
|
@@ -1341,11 +1401,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1401
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1402
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1403
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1404
|
try options.log.lastmiss(.{});
|
|
1346
1405
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1406
|
}
|
|
1348
1407
|
|
|
1408
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1409
|
return false;
|
|
1350
1410
|
}
|
|
1351
1411
|
|
|
@@ -1356,7 +1416,7 @@ fn moveHit(
|
|
|
1356
1416
|
immune: *bool,
|
|
1357
1417
|
mist: *bool,
|
|
1358
1418
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1419
|
+
) !bool {
|
|
1360
1420
|
var side = battle.side(player);
|
|
1361
1421
|
const foe = battle.foe(player);
|
|
1362
1422
|
|
|
@@ -1407,7 +1467,7 @@ fn moveHit(
|
|
|
1407
1467
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1468
|
|
|
1409
1469
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1470
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1471
|
};
|
|
1412
1472
|
|
|
1413
1473
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1478,17 @@ fn moveHit(
|
|
|
1418
1478
|
|
|
1419
1479
|
if (!miss) return true;
|
|
1420
1480
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1481
|
+
|
|
1421
1482
|
side.active.volatiles.Binding = false;
|
|
1483
|
+
options.chance.observe(.binding, player, .None);
|
|
1484
|
+
|
|
1422
1485
|
return false;
|
|
1423
1486
|
}
|
|
1424
1487
|
|
|
1425
1488
|
fn checkFaint(
|
|
1426
1489
|
battle: anytype,
|
|
1427
1490
|
player: Player,
|
|
1491
|
+
cap: bool,
|
|
1428
1492
|
options: anytype,
|
|
1429
1493
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1494
|
const side = battle.side(player);
|
|
@@ -1437,8 +1501,8 @@ fn checkFaint(
|
|
|
1437
1501
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1502
|
const more = player_out or foe_out;
|
|
1439
1503
|
|
|
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;
|
|
1504
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1505
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1506
|
|
|
1443
1507
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1508
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1523,7 @@ fn checkFaint(
|
|
|
1459
1523
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1524
|
}
|
|
1461
1525
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1526
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1527
|
var side = battle.side(player);
|
|
1464
1528
|
var foe = battle.foe(player);
|
|
1465
1529
|
assert(side.stored().hp == 0);
|
|
@@ -1493,7 +1557,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1557
|
}
|
|
1494
1558
|
|
|
1495
1559
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1560
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1561
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1562
|
+
if (cap) options.calc.capped(player);
|
|
1497
1563
|
return null;
|
|
1498
1564
|
}
|
|
1499
1565
|
|
|
@@ -1653,17 +1719,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1719
|
}
|
|
1654
1720
|
}
|
|
1655
1721
|
|
|
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
1722
|
fn onBegin(
|
|
1668
1723
|
battle: anytype,
|
|
1669
1724
|
player: Player,
|
|
@@ -1750,8 +1805,10 @@ pub const Effects = struct {
|
|
|
1750
1805
|
|
|
1751
1806
|
side.active.volatiles.Bide = true;
|
|
1752
1807
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1808
|
+
|
|
1753
1809
|
side.active.volatiles.state = 0;
|
|
1754
1810
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1811
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1812
|
|
|
1756
1813
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1814
|
}
|
|
@@ -1813,12 +1870,13 @@ pub const Effects = struct {
|
|
|
1813
1870
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1871
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1872
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1873
|
}
|
|
1818
1874
|
|
|
1819
1875
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1876
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1877
|
+
|
|
1878
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1879
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1822
1880
|
|
|
1823
1881
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1882
|
}
|
|
@@ -1846,10 +1904,7 @@ pub const Effects = struct {
|
|
|
1846
1904
|
const foe_ident = battle.active(player.foe());
|
|
1847
1905
|
|
|
1848
1906
|
// 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
|
-
}
|
|
1907
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1908
|
|
|
1854
1909
|
if (volatiles.disable_move != 0) {
|
|
1855
1910
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1879,6 +1934,7 @@ pub const Effects = struct {
|
|
|
1879
1934
|
volatiles.disable_move =
|
|
1880
1935
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1936
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1937
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1938
|
|
|
1883
1939
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1940
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1985,22 +2041,24 @@ pub const Effects = struct {
|
|
|
1985
2041
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2042
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2043
|
s.stored().status = 0;
|
|
2044
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2045
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2046
|
s.stored().status = Status.init(.PSN);
|
|
1990
2047
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2048
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2049
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2050
|
}
|
|
1994
2051
|
} else {
|
|
1995
2052
|
if (Status.any(foe_stored.status)) {
|
|
1996
2053
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2054
|
foe.last_selected_move = .SKIP_TURN;
|
|
2055
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2056
|
}
|
|
1999
2057
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2058
|
foe_stored.status = 0;
|
|
2001
2059
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2060
|
+
try clearVolatiles(battle, player, options);
|
|
2061
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2062
|
}
|
|
2005
2063
|
}
|
|
2006
2064
|
|
|
@@ -2051,7 +2109,6 @@ pub const Effects = struct {
|
|
|
2051
2109
|
}
|
|
2052
2110
|
}
|
|
2053
2111
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2112
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2113
|
|
|
2057
2114
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2149,10 @@ pub const Effects = struct {
|
|
|
2092
2149
|
if (!has_mimic) {
|
|
2093
2150
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2151
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2152
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2153
|
}
|
|
2098
2154
|
}
|
|
2099
2155
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2156
|
|
|
2102
2157
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2158
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2206,6 @@ pub const Effects = struct {
|
|
|
2151
2206
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2207
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2208
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2209
|
|
|
2156
2210
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2211
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2273,6 @@ pub const Effects = struct {
|
|
|
2219
2273
|
|
|
2220
2274
|
if (move.effect == .Poison) {
|
|
2221
2275
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2276
|
} else {
|
|
2224
2277
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2278
|
return;
|
|
@@ -2288,9 +2341,8 @@ pub const Effects = struct {
|
|
|
2288
2341
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2342
|
});
|
|
2290
2343
|
}
|
|
2291
|
-
// If
|
|
2344
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2345
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2346
|
}
|
|
2295
2347
|
|
|
2296
2348
|
// Sleep Clause Mod
|
|
@@ -2302,6 +2354,8 @@ pub const Effects = struct {
|
|
|
2302
2354
|
foe.active.volatiles.Recharging = false;
|
|
2303
2355
|
|
|
2304
2356
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2357
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2358
|
+
|
|
2305
2359
|
const last = battle.side(player).last_selected_move;
|
|
2306
2360
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2361
|
}
|
|
@@ -2343,9 +2397,7 @@ pub const Effects = struct {
|
|
|
2343
2397
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2398
|
if (showdown) {
|
|
2345
2399
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2400
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2401
|
battle.last_damage = 0;
|
|
2350
2402
|
} else {
|
|
2351
2403
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2412,8 @@ pub const Effects = struct {
|
|
|
2360
2412
|
|
|
2361
2413
|
volatiles.Thrashing = true;
|
|
2362
2414
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2415
|
+
|
|
2416
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2417
|
}
|
|
2364
2418
|
|
|
2365
2419
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2405,6 +2459,7 @@ pub const Effects = struct {
|
|
|
2405
2459
|
|
|
2406
2460
|
side.active.volatiles.attacks =
|
|
2407
2461
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2462
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2463
|
}
|
|
2409
2464
|
|
|
2410
2465
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2576,6 @@ pub const Effects = struct {
|
|
|
2521
2576
|
} else {
|
|
2522
2577
|
// checkHit already checks for Invulnerable
|
|
2523
2578
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2579
|
}
|
|
2526
2580
|
|
|
2527
2581
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2689,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2689
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2690
|
}
|
|
2637
2691
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2692
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2693
|
var log = options.log;
|
|
2640
2694
|
var side = battle.side(who);
|
|
2641
2695
|
var volatiles = &side.active.volatiles;
|
|
2642
2696
|
const ident = battle.active(who);
|
|
2643
2697
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2698
|
if (volatiles.disable_move != 0) {
|
|
2647
2699
|
volatiles.disable_move = 0;
|
|
2648
2700
|
volatiles.disable_duration = 0;
|
|
2701
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2702
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2703
|
}
|
|
2651
2704
|
if (volatiles.Confusion) {
|
|
2652
2705
|
// volatiles.confusion is left unchanged
|
|
2653
2706
|
volatiles.Confusion = false;
|
|
2707
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2708
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2709
|
}
|
|
2656
2710
|
if (volatiles.Mist) {
|
|
@@ -2685,6 +2739,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2739
|
}
|
|
2686
2740
|
}
|
|
2687
2741
|
|
|
2742
|
+
fn decrement(
|
|
2743
|
+
comptime field: chance.Action.Field,
|
|
2744
|
+
player: Player,
|
|
2745
|
+
options: anytype,
|
|
2746
|
+
n: anytype,
|
|
2747
|
+
) @TypeOf(n) {
|
|
2748
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2749
|
+
.started, .ended => 0,
|
|
2750
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2751
|
+
else => unreachable,
|
|
2752
|
+
} else n - 1;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2688
2755
|
pub const Rolls = struct {
|
|
2689
2756
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2757
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2700,7 +2767,13 @@ pub const Rolls = struct {
|
|
|
2700
2767
|
return p1;
|
|
2701
2768
|
}
|
|
2702
2769
|
|
|
2703
|
-
fn criticalHit(
|
|
2770
|
+
fn criticalHit(
|
|
2771
|
+
battle: anytype,
|
|
2772
|
+
player: Player,
|
|
2773
|
+
rate: u8,
|
|
2774
|
+
spurious: bool,
|
|
2775
|
+
options: anytype,
|
|
2776
|
+
) !bool {
|
|
2704
2777
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2778
|
val == .true
|
|
2706
2779
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2783,7 @@ pub const Rolls = struct {
|
|
|
2710
2783
|
else
|
|
2711
2784
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2785
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2786
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2787
|
return crit;
|
|
2715
2788
|
}
|
|
2716
2789
|
|
|
@@ -2732,17 +2805,19 @@ pub const Rolls = struct {
|
|
|
2732
2805
|
return roll;
|
|
2733
2806
|
}
|
|
2734
2807
|
|
|
2735
|
-
|
|
2808
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2809
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2810
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2811
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2812
|
val == .true
|
|
2738
2813
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2814
|
+
battle.rng.hit(player, acc)
|
|
2740
2815
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2816
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2817
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2818
|
+
battle.rng.next() < acc;
|
|
2744
2819
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2820
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2821
|
return ok;
|
|
2747
2822
|
}
|
|
2748
2823
|
|
|
@@ -2874,9 +2949,10 @@ pub const Rolls = struct {
|
|
|
2874
2949
|
}
|
|
2875
2950
|
|
|
2876
2951
|
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
|
|
2952
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2953
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2954
|
+
else
|
|
2955
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2880
2956
|
battle.rng.sleepDuration(player)
|
|
2881
2957
|
else if (showdown)
|
|
2882
2958
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,14 +2964,15 @@ pub const Rolls = struct {
|
|
|
2888
2964
|
};
|
|
2889
2965
|
|
|
2890
2966
|
assert(duration >= 1 and duration <= 7);
|
|
2891
|
-
options.chance.duration(
|
|
2967
|
+
options.chance.duration(player, duration);
|
|
2892
2968
|
return duration;
|
|
2893
2969
|
}
|
|
2894
2970
|
|
|
2895
2971
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2896
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
val
|
|
2898
|
-
else
|
|
2972
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2973
|
+
if (val >= 1 and val <= 8) val else null
|
|
2974
|
+
else
|
|
2975
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2899
2976
|
battle.rng.disableDuration(player)
|
|
2900
2977
|
else if (showdown)
|
|
2901
2978
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,29 +2980,31 @@ pub const Rolls = struct {
|
|
|
2903
2980
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2904
2981
|
|
|
2905
2982
|
assert(duration >= 1 and duration <= 8);
|
|
2906
|
-
options.chance.duration(
|
|
2983
|
+
options.chance.duration(player, duration);
|
|
2907
2984
|
return duration;
|
|
2908
2985
|
}
|
|
2909
2986
|
|
|
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
|
|
2987
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2988
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2989
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
2990
|
+
else
|
|
2991
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2992
|
+
battle.rng.confusionDuration(player)
|
|
2915
2993
|
else if (showdown)
|
|
2916
2994
|
battle.rng.range(u3, 2, 6)
|
|
2917
2995
|
else
|
|
2918
2996
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2919
2997
|
|
|
2920
2998
|
assert(duration >= 2 and duration <= 5);
|
|
2921
|
-
options.chance.duration(
|
|
2999
|
+
options.chance.duration(player, duration);
|
|
2922
3000
|
return duration;
|
|
2923
3001
|
}
|
|
2924
3002
|
|
|
2925
3003
|
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
|
|
3004
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3005
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3006
|
+
else
|
|
3007
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2929
3008
|
battle.rng.attackingDuration(player)
|
|
2930
3009
|
else if (showdown)
|
|
2931
3010
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2933,7 +3012,7 @@ pub const Rolls = struct {
|
|
|
2933
3012
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2934
3013
|
|
|
2935
3014
|
assert(duration >= 2 and duration <= 3);
|
|
2936
|
-
options.chance.duration(
|
|
3015
|
+
options.chance.duration(player, duration);
|
|
2937
3016
|
return duration;
|
|
2938
3017
|
}
|
|
2939
3018
|
|
|
@@ -2946,9 +3025,10 @@ pub const Rolls = struct {
|
|
|
2946
3025
|
options: anytype,
|
|
2947
3026
|
) !u3 {
|
|
2948
3027
|
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
|
|
3028
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3029
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3030
|
+
else
|
|
3031
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2952
3032
|
battle.rng.distribution(player)
|
|
2953
3033
|
else if (showdown)
|
|
2954
3034
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2959,7 +3039,7 @@ pub const Rolls = struct {
|
|
|
2959
3039
|
|
|
2960
3040
|
assert(n >= 2 and n <= 5);
|
|
2961
3041
|
if (effect == .Binding) {
|
|
2962
|
-
options.chance.duration(
|
|
3042
|
+
options.chance.duration(player, n);
|
|
2963
3043
|
} else {
|
|
2964
3044
|
try options.chance.multiHit(player, n);
|
|
2965
3045
|
}
|
|
@@ -2974,15 +3054,13 @@ pub const Rolls = struct {
|
|
|
2974
3054
|
options: anytype,
|
|
2975
3055
|
) !u4 {
|
|
2976
3056
|
// TODO: consider throwing error instead of rerolling?
|
|
2977
|
-
const
|
|
3057
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2978
3058
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2979
3059
|
val
|
|
2980
3060
|
else
|
|
2981
3061
|
null
|
|
2982
3062
|
else
|
|
2983
|
-
null
|
|
2984
|
-
|
|
2985
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3063
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2986
3064
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2987
3065
|
else slot: {
|
|
2988
3066
|
if (showdown) {
|
|
@@ -3068,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3146
|
const stored = side.stored();
|
|
3069
3147
|
|
|
3070
3148
|
// 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
|
|
3149
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3150
|
//
|
|
3073
3151
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3152
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,7 +3154,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3154
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3155
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3156
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3157
|
+
// switch at all
|
|
3080
3158
|
if (isForced(active)) {
|
|
3081
3159
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3160
|
n += 1;
|
|
@@ -3095,7 +3173,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3173
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3174
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3175
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3176
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3177
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3178
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3179
|
{
|
|
@@ -3108,7 +3186,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3186
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3187
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3188
|
// 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
|
|
3189
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3190
|
if (limited) {
|
|
3113
3191
|
assert(showdown);
|
|
3114
3192
|
assert(side.last_selected_move != .None);
|