@pkmn/engine 0.1.0-dev.eaae4aa6 → 0.1.0-dev.f34af416
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -2
- package/README.md +15 -11
- package/build/pkg/addon.js +6 -7
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +2 -3
- package/build/pkg/index.d.ts +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +4 -4
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +8 -3
- package/build/test/display.js.map +1 -1
- package/build/tools/debug.js +2 -3
- package/build/tools/debug.js.map +1 -1
- package/build.zig +189 -72
- package/package.json +17 -16
- package/src/bin/install-pkmn-engine +17 -7
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +2 -2
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +51 -44
- package/src/lib/common/rational.zig +114 -24
- package/src/lib/common/rng.zig +10 -10
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +508 -128
- package/src/lib/gen1/chance.zig +383 -276
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data.zig +29 -28
- package/src/lib/gen1/helpers.zig +96 -536
- package/src/lib/gen1/mechanics.zig +266 -160
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +91 -500
- package/src/lib/gen2/data.zig +15 -14
- package/src/lib/gen2/helpers.zig +41 -9
- package/src/lib/gen2/mechanics.zig +158 -97
- package/src/lib/pkmn.zig +10 -16
- package/src/test/display/pkmn.html.tmpl +5 -5
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
|
@@ -4,10 +4,12 @@ const pkmn = @import("../pkmn.zig");
|
|
|
4
4
|
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
6
|
const DEBUG = @import("../common/debug.zig").print;
|
|
7
|
+
const optional = @import("../common/optional.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
10
|
|
|
10
11
|
const data = @import("data.zig");
|
|
12
|
+
const chance = @import("chance.zig");
|
|
11
13
|
|
|
12
14
|
const assert = std.debug.assert;
|
|
13
15
|
|
|
@@ -166,7 +168,7 @@ fn selectMove(
|
|
|
166
168
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
169
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
170
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
171
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
172
|
saveMove(battle, player, choice);
|
|
171
173
|
} else {
|
|
172
174
|
assert(choice.data == 0);
|
|
@@ -253,11 +255,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
255
|
active.moves = incoming.moves;
|
|
254
256
|
|
|
255
257
|
statusModify(incoming.status, &active.stats);
|
|
258
|
+
options.chance.switched(player, slot);
|
|
256
259
|
|
|
257
260
|
foe.active.volatiles.Binding = false;
|
|
258
261
|
|
|
259
262
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
263
|
|
|
262
264
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
265
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +349,13 @@ fn doTurn(
|
|
|
347
349
|
)) |r| return r;
|
|
348
350
|
if (!replace) {
|
|
349
351
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
352
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
353
|
}
|
|
352
354
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
355
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
356
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
357
|
|
|
356
|
-
options.chance.clearPending();
|
|
358
|
+
if (!showdown) options.chance.clearPending();
|
|
357
359
|
|
|
358
360
|
residual = true;
|
|
359
361
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +371,10 @@ fn doTurn(
|
|
|
369
371
|
)) |r| return r;
|
|
370
372
|
if (!replace) {
|
|
371
373
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
374
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
375
|
}
|
|
374
376
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
377
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
378
|
}
|
|
377
379
|
|
|
378
380
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -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
|
|
@@ -484,17 +486,22 @@ fn beforeMove(
|
|
|
484
486
|
|
|
485
487
|
if (Status.is(stored.status, .SLP)) {
|
|
486
488
|
const before = stored.status;
|
|
489
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
490
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
491
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
492
|
+
.started, .ended => stored.status = 0,
|
|
493
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
494
|
+
stored.status -= 1;
|
|
495
|
+
},
|
|
496
|
+
else => unreachable,
|
|
490
497
|
} else {
|
|
491
498
|
stored.status -= 1;
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
502
|
+
try options.chance.sleep(player, if (slf)
|
|
503
|
+
.None
|
|
504
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
505
|
|
|
499
506
|
if (duration == 0) {
|
|
500
507
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +539,12 @@ fn beforeMove(
|
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
volatiles.disable_duration =
|
|
543
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
544
|
+
try options.chance.disable(
|
|
545
|
+
player,
|
|
546
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
547
|
+
);
|
|
541
548
|
|
|
542
549
|
if (volatiles.disable_duration == 0) {
|
|
543
550
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +563,19 @@ fn beforeMove(
|
|
|
556
563
|
|
|
557
564
|
if (volatiles.Confusion) {
|
|
558
565
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
566
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
+
.started => volatiles.confusion -= 1,
|
|
568
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
569
|
+
volatiles.confusion -= 1;
|
|
570
|
+
},
|
|
571
|
+
.ended => volatiles.confusion = 0,
|
|
572
|
+
.overwritten => {},
|
|
573
|
+
else => unreachable,
|
|
562
574
|
} else {
|
|
563
575
|
volatiles.confusion -= 1;
|
|
564
576
|
}
|
|
565
|
-
|
|
577
|
+
|
|
578
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
579
|
|
|
567
580
|
if (volatiles.confusion == 0) {
|
|
568
581
|
volatiles.Confusion = false;
|
|
@@ -580,7 +593,9 @@ fn beforeMove(
|
|
|
580
593
|
volatiles.Charging = false;
|
|
581
594
|
volatiles.Binding = false;
|
|
582
595
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
596
|
+
|
|
597
|
+
options.chance.observe(.attacking, player, .None);
|
|
598
|
+
options.chance.observe(.binding, player, .None);
|
|
584
599
|
{
|
|
585
600
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
601
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +603,9 @@ fn beforeMove(
|
|
|
588
603
|
const def = foe.active.stats.def;
|
|
589
604
|
foe.active.stats.def = active.stats.def;
|
|
590
605
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
606
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
607
|
+
return .err;
|
|
608
|
+
}
|
|
592
609
|
}
|
|
593
610
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
611
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +634,10 @@ fn beforeMove(
|
|
|
617
634
|
volatiles.Thrashing = false;
|
|
618
635
|
volatiles.Charging = false;
|
|
619
636
|
volatiles.Binding = false;
|
|
620
|
-
|
|
637
|
+
|
|
638
|
+
options.chance.observe(.attacking, player, .None);
|
|
639
|
+
options.chance.observe(.binding, player, .None);
|
|
640
|
+
|
|
621
641
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
642
|
try log.cant(.{ ident, .Paralysis });
|
|
623
643
|
return .done;
|
|
@@ -635,12 +655,8 @@ fn beforeMove(
|
|
|
635
655
|
volatiles.state +%= battle.last_damage;
|
|
636
656
|
}
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
658
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
659
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
660
|
|
|
645
661
|
if (volatiles.attacks != 0) {
|
|
646
662
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +690,36 @@ fn beforeMove(
|
|
|
674
690
|
}
|
|
675
691
|
|
|
676
692
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
693
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
694
|
+
|
|
695
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
696
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
697
|
+
|
|
698
|
+
if (volatiles.attacks == 0) {
|
|
699
|
+
const overwritten = volatiles.Confusion;
|
|
683
700
|
|
|
684
|
-
|
|
701
|
+
volatiles.Thrashing = false;
|
|
702
|
+
volatiles.Confusion = true;
|
|
703
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
704
|
+
|
|
705
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
706
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
707
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
708
|
+
// correct player)
|
|
685
709
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
710
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
686
711
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
712
|
+
|
|
713
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
714
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
715
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
716
|
+
|
|
696
717
|
return .skip_can;
|
|
697
718
|
}
|
|
698
719
|
|
|
699
720
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
721
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
722
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
723
|
|
|
707
724
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
725
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -795,7 +812,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
812
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
813
|
}
|
|
797
814
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
815
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
816
|
var active = &side.active;
|
|
800
817
|
const volatiles = &active.volatiles;
|
|
801
818
|
|
|
@@ -803,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
820
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
821
|
if (showdown and volatiles.Transform) return;
|
|
805
822
|
|
|
823
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
824
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
825
|
+
|
|
806
826
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
827
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
828
|
}
|
|
809
829
|
|
|
810
830
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -852,13 +872,13 @@ fn doMove(
|
|
|
852
872
|
break :miss true;
|
|
853
873
|
}
|
|
854
874
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
875
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
876
|
};
|
|
857
877
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
878
|
|
|
859
879
|
var late = showdown and move.effect != .Explode;
|
|
860
880
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
881
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
882
|
if (showdown and move.effect.isMulti()) {
|
|
863
883
|
try Effects.multiHit(battle, player, move, options);
|
|
864
884
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +900,9 @@ fn doMove(
|
|
|
880
900
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
901
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
902
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
903
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
904
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
905
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
906
|
return @as(?Result, Result.Error);
|
|
886
907
|
}
|
|
887
908
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +916,12 @@ fn doMove(
|
|
|
895
916
|
}
|
|
896
917
|
}
|
|
897
918
|
|
|
919
|
+
var missed: bool = undefined;
|
|
898
920
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
921
|
+
if (!showdown and !skip) {
|
|
922
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
923
|
+
miss = missed or zero;
|
|
924
|
+
}
|
|
903
925
|
|
|
904
926
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
927
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +948,32 @@ fn doMove(
|
|
|
926
948
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
949
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
950
|
try log.immune(.{ foe_ident, .None });
|
|
951
|
+
if (!showdown) {
|
|
952
|
+
if (move.effect == .Binding) {
|
|
953
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
954
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
955
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
956
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
957
|
+
// (considered missing)
|
|
958
|
+
assert(zero);
|
|
959
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
960
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
961
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
962
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
963
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
964
|
+
// roll)
|
|
965
|
+
try options.chance.commit(player, .binding);
|
|
966
|
+
} else {
|
|
967
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
968
|
+
// division-by-zero freeze alternative timeline scenario
|
|
969
|
+
try options.chance.commit(player, .glitch);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
929
972
|
} else if (mist) {
|
|
930
973
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
974
|
try log.fail(.{ foe_ident, .None });
|
|
932
975
|
} else {
|
|
933
|
-
if (showdown
|
|
976
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
977
|
try log.lastmiss(.{});
|
|
935
978
|
try log.miss(.{battle.active(player)});
|
|
936
979
|
}
|
|
@@ -945,17 +988,15 @@ fn doMove(
|
|
|
945
988
|
try buildRage(battle, player.foe(), options);
|
|
946
989
|
} else if (showdown and move.effect == .Disable) {
|
|
947
990
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
991
|
}
|
|
951
992
|
return null;
|
|
952
|
-
} else {
|
|
993
|
+
} else if (!showdown) {
|
|
953
994
|
try options.chance.commit(player, .hit);
|
|
954
995
|
}
|
|
955
996
|
|
|
956
997
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
998
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
999
|
+
// it up above before damage calculation)
|
|
959
1000
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1001
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1002
|
hits = side.active.volatiles.attacks;
|
|
@@ -1014,12 +1055,12 @@ fn doMove(
|
|
|
1014
1055
|
}
|
|
1015
1056
|
|
|
1016
1057
|
// 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
|
|
1058
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1059
|
if (nullified) return null;
|
|
1019
1060
|
|
|
1020
1061
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1062
|
// 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
|
|
1063
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1064
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1065
|
|
|
1025
1066
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1075,7 @@ fn doMove(
|
|
|
1034
1075
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1076
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1077
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1078
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1079
|
if (move.effect == .Twineedle) {
|
|
1039
1080
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1081
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1099,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1099
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1100
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1101
|
|
|
1061
|
-
|
|
1102
|
+
const spurious =
|
|
1103
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1104
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1105
|
}
|
|
1063
1106
|
|
|
1064
1107
|
fn calcDamage(
|
|
@@ -1067,6 +1110,7 @@ fn calcDamage(
|
|
|
1067
1110
|
target_player: Player,
|
|
1068
1111
|
m: ?Move.Data,
|
|
1069
1112
|
crit: bool,
|
|
1113
|
+
options: anytype,
|
|
1070
1114
|
) bool {
|
|
1071
1115
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1116
|
const cfz = m == null;
|
|
@@ -1078,28 +1122,8 @@ fn calcDamage(
|
|
|
1078
1122
|
|
|
1079
1123
|
const special = move.type.special();
|
|
1080
1124
|
|
|
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
|
|
1125
|
+
var atk = attack(side, crit, special);
|
|
1126
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1127
|
|
|
1104
1128
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1129
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1124,11 +1148,41 @@ fn calcDamage(
|
|
|
1124
1148
|
d = @min(997, d);
|
|
1125
1149
|
d += 2;
|
|
1126
1150
|
|
|
1151
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1152
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1153
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1154
|
+
// that results in counter intuitive probabilities in the common case
|
|
1155
|
+
if (pkmn.options.chance and !showdown) {
|
|
1156
|
+
atk = attack(side, !crit, special);
|
|
1157
|
+
def = defense(target, !crit, special, false);
|
|
1158
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1127
1161
|
battle.last_damage = @intCast(d);
|
|
1128
1162
|
|
|
1129
1163
|
return true;
|
|
1130
1164
|
}
|
|
1131
1165
|
|
|
1166
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1167
|
+
return if (crit)
|
|
1168
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1169
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1173
|
+
return if (crit)
|
|
1174
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1175
|
+
else
|
|
1176
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1177
|
+
if (special)
|
|
1178
|
+
target.active.stats.spc *
|
|
1179
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1180
|
+
else
|
|
1181
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1182
|
+
target.active.stats.def *
|
|
1183
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1132
1186
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1187
|
const side = battle.side(player);
|
|
1134
1188
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1227
|
const foe = battle.foe(player);
|
|
1174
1228
|
|
|
1175
1229
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1230
|
|
|
1178
1231
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1232
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1234,9 +1287,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1234
1287
|
|
|
1235
1288
|
battle.last_damage *|= 2;
|
|
1236
1289
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1290
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1291
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1292
|
|
|
1241
1293
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1294
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1317,7 @@ fn applyDamage(
|
|
|
1265
1317
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1318
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1319
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1320
|
+
options.calc.capped(sub_player);
|
|
1269
1321
|
return true;
|
|
1270
1322
|
} else {
|
|
1271
1323
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1351,7 @@ fn mirrorMove(
|
|
|
1299
1351
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1352
|
return null;
|
|
1301
1353
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1354
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1355
|
}
|
|
1304
1356
|
|
|
1305
1357
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1372,7 @@ fn metronome(
|
|
|
1320
1372
|
var side = battle.side(player);
|
|
1321
1373
|
|
|
1322
1374
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1375
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1376
|
|
|
1325
1377
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1378
|
return null;
|
|
@@ -1332,7 +1384,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1384
|
var immune = false;
|
|
1333
1385
|
var mist = false;
|
|
1334
1386
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1387
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1388
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1336
1391
|
|
|
1337
1392
|
assert(!immune);
|
|
1338
1393
|
if (mist) {
|
|
@@ -1341,11 +1396,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1396
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1397
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1398
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1399
|
try options.log.lastmiss(.{});
|
|
1346
1400
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1401
|
}
|
|
1348
1402
|
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1404
|
return false;
|
|
1350
1405
|
}
|
|
1351
1406
|
|
|
@@ -1356,7 +1411,7 @@ fn moveHit(
|
|
|
1356
1411
|
immune: *bool,
|
|
1357
1412
|
mist: *bool,
|
|
1358
1413
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1414
|
+
) !bool {
|
|
1360
1415
|
var side = battle.side(player);
|
|
1361
1416
|
const foe = battle.foe(player);
|
|
1362
1417
|
|
|
@@ -1407,7 +1462,7 @@ fn moveHit(
|
|
|
1407
1462
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1463
|
|
|
1409
1464
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1465
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1466
|
};
|
|
1412
1467
|
|
|
1413
1468
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1473,17 @@ fn moveHit(
|
|
|
1418
1473
|
|
|
1419
1474
|
if (!miss) return true;
|
|
1420
1475
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1476
|
+
|
|
1421
1477
|
side.active.volatiles.Binding = false;
|
|
1478
|
+
options.chance.observe(.binding, player, .None);
|
|
1479
|
+
|
|
1422
1480
|
return false;
|
|
1423
1481
|
}
|
|
1424
1482
|
|
|
1425
1483
|
fn checkFaint(
|
|
1426
1484
|
battle: anytype,
|
|
1427
1485
|
player: Player,
|
|
1486
|
+
cap: bool,
|
|
1428
1487
|
options: anytype,
|
|
1429
1488
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1489
|
const side = battle.side(player);
|
|
@@ -1437,8 +1496,8 @@ fn checkFaint(
|
|
|
1437
1496
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1497
|
const more = player_out or foe_out;
|
|
1439
1498
|
|
|
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;
|
|
1499
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1500
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1501
|
|
|
1443
1502
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1503
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1518,7 @@ fn checkFaint(
|
|
|
1459
1518
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1519
|
}
|
|
1461
1520
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1521
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1522
|
var side = battle.side(player);
|
|
1464
1523
|
var foe = battle.foe(player);
|
|
1465
1524
|
assert(side.stored().hp == 0);
|
|
@@ -1493,7 +1552,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1552
|
}
|
|
1494
1553
|
|
|
1495
1554
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1555
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1556
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1557
|
+
if (cap) options.calc.capped(player);
|
|
1497
1558
|
return null;
|
|
1498
1559
|
}
|
|
1499
1560
|
|
|
@@ -1653,17 +1714,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1714
|
}
|
|
1654
1715
|
}
|
|
1655
1716
|
|
|
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
1717
|
fn onBegin(
|
|
1668
1718
|
battle: anytype,
|
|
1669
1719
|
player: Player,
|
|
@@ -1750,8 +1800,10 @@ pub const Effects = struct {
|
|
|
1750
1800
|
|
|
1751
1801
|
side.active.volatiles.Bide = true;
|
|
1752
1802
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1803
|
+
|
|
1753
1804
|
side.active.volatiles.state = 0;
|
|
1754
1805
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1806
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1807
|
|
|
1756
1808
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1809
|
}
|
|
@@ -1813,12 +1865,13 @@ pub const Effects = struct {
|
|
|
1813
1865
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1866
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1867
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1868
|
}
|
|
1818
1869
|
|
|
1819
1870
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1871
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1872
|
+
|
|
1873
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1874
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1822
1875
|
|
|
1823
1876
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1877
|
}
|
|
@@ -1846,10 +1899,7 @@ pub const Effects = struct {
|
|
|
1846
1899
|
const foe_ident = battle.active(player.foe());
|
|
1847
1900
|
|
|
1848
1901
|
// 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
|
-
}
|
|
1902
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1903
|
|
|
1854
1904
|
if (volatiles.disable_move != 0) {
|
|
1855
1905
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1879,6 +1929,7 @@ pub const Effects = struct {
|
|
|
1879
1929
|
volatiles.disable_move =
|
|
1880
1930
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1931
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1932
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1933
|
|
|
1883
1934
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1935
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1985,22 +2036,24 @@ pub const Effects = struct {
|
|
|
1985
2036
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2037
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2038
|
s.stored().status = 0;
|
|
2039
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2040
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2041
|
s.stored().status = Status.init(.PSN);
|
|
1990
2042
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2043
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2044
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2045
|
}
|
|
1994
2046
|
} else {
|
|
1995
2047
|
if (Status.any(foe_stored.status)) {
|
|
1996
2048
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2049
|
foe.last_selected_move = .SKIP_TURN;
|
|
2050
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2051
|
}
|
|
1999
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2053
|
foe_stored.status = 0;
|
|
2001
2054
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2055
|
+
try clearVolatiles(battle, player, options);
|
|
2056
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2057
|
}
|
|
2005
2058
|
}
|
|
2006
2059
|
|
|
@@ -2051,7 +2104,6 @@ pub const Effects = struct {
|
|
|
2051
2104
|
}
|
|
2052
2105
|
}
|
|
2053
2106
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2107
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2108
|
|
|
2057
2109
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2144,10 @@ pub const Effects = struct {
|
|
|
2092
2144
|
if (!has_mimic) {
|
|
2093
2145
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2146
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2147
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2148
|
}
|
|
2098
2149
|
}
|
|
2099
2150
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2151
|
|
|
2102
2152
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2153
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2201,6 @@ pub const Effects = struct {
|
|
|
2151
2201
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2202
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2203
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2204
|
|
|
2156
2205
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2206
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2268,6 @@ pub const Effects = struct {
|
|
|
2219
2268
|
|
|
2220
2269
|
if (move.effect == .Poison) {
|
|
2221
2270
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2271
|
} else {
|
|
2224
2272
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2273
|
return;
|
|
@@ -2288,9 +2336,8 @@ pub const Effects = struct {
|
|
|
2288
2336
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2337
|
});
|
|
2290
2338
|
}
|
|
2291
|
-
// If
|
|
2339
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2340
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2341
|
}
|
|
2295
2342
|
|
|
2296
2343
|
// Sleep Clause Mod
|
|
@@ -2302,6 +2349,8 @@ pub const Effects = struct {
|
|
|
2302
2349
|
foe.active.volatiles.Recharging = false;
|
|
2303
2350
|
|
|
2304
2351
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2352
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2353
|
+
|
|
2305
2354
|
const last = battle.side(player).last_selected_move;
|
|
2306
2355
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2356
|
}
|
|
@@ -2343,9 +2392,7 @@ pub const Effects = struct {
|
|
|
2343
2392
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2393
|
if (showdown) {
|
|
2345
2394
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2395
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2396
|
battle.last_damage = 0;
|
|
2350
2397
|
} else {
|
|
2351
2398
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2407,8 @@ pub const Effects = struct {
|
|
|
2360
2407
|
|
|
2361
2408
|
volatiles.Thrashing = true;
|
|
2362
2409
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2410
|
+
|
|
2411
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2412
|
}
|
|
2364
2413
|
|
|
2365
2414
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2405,6 +2454,7 @@ pub const Effects = struct {
|
|
|
2405
2454
|
|
|
2406
2455
|
side.active.volatiles.attacks =
|
|
2407
2456
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2457
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2458
|
}
|
|
2409
2459
|
|
|
2410
2460
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2571,6 @@ pub const Effects = struct {
|
|
|
2521
2571
|
} else {
|
|
2522
2572
|
// checkHit already checks for Invulnerable
|
|
2523
2573
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2574
|
}
|
|
2526
2575
|
|
|
2527
2576
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2684,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2684
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2685
|
}
|
|
2637
2686
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2687
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2688
|
var log = options.log;
|
|
2640
2689
|
var side = battle.side(who);
|
|
2641
2690
|
var volatiles = &side.active.volatiles;
|
|
2642
2691
|
const ident = battle.active(who);
|
|
2643
2692
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2693
|
if (volatiles.disable_move != 0) {
|
|
2647
2694
|
volatiles.disable_move = 0;
|
|
2648
2695
|
volatiles.disable_duration = 0;
|
|
2696
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2697
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2698
|
}
|
|
2651
2699
|
if (volatiles.Confusion) {
|
|
2652
2700
|
// volatiles.confusion is left unchanged
|
|
2653
2701
|
volatiles.Confusion = false;
|
|
2702
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2703
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2704
|
}
|
|
2656
2705
|
if (volatiles.Mist) {
|
|
@@ -2685,10 +2734,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2734
|
}
|
|
2686
2735
|
}
|
|
2687
2736
|
|
|
2737
|
+
fn decrement(
|
|
2738
|
+
comptime field: chance.Action.Field,
|
|
2739
|
+
player: Player,
|
|
2740
|
+
options: anytype,
|
|
2741
|
+
n: anytype,
|
|
2742
|
+
) @TypeOf(n) {
|
|
2743
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2744
|
+
.started, .ended => 0,
|
|
2745
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2746
|
+
else => unreachable,
|
|
2747
|
+
} else n - 1;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2688
2750
|
pub const Rolls = struct {
|
|
2689
2751
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2752
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2691
2753
|
player == .P1
|
|
2754
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2755
|
+
battle.rng.speedTie()
|
|
2692
2756
|
else if (showdown)
|
|
2693
2757
|
battle.rng.range(u8, 0, 2) == 0
|
|
2694
2758
|
else
|
|
@@ -2698,21 +2762,31 @@ pub const Rolls = struct {
|
|
|
2698
2762
|
return p1;
|
|
2699
2763
|
}
|
|
2700
2764
|
|
|
2701
|
-
fn criticalHit(
|
|
2765
|
+
fn criticalHit(
|
|
2766
|
+
battle: anytype,
|
|
2767
|
+
player: Player,
|
|
2768
|
+
rate: u8,
|
|
2769
|
+
spurious: bool,
|
|
2770
|
+
options: anytype,
|
|
2771
|
+
) !bool {
|
|
2702
2772
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2703
2773
|
val == .true
|
|
2774
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2775
|
+
battle.rng.criticalHit(player, rate)
|
|
2704
2776
|
else if (showdown)
|
|
2705
2777
|
battle.rng.chance(u8, rate, 256)
|
|
2706
2778
|
else
|
|
2707
2779
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2708
2780
|
|
|
2709
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2781
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2710
2782
|
return crit;
|
|
2711
2783
|
}
|
|
2712
2784
|
|
|
2713
2785
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2714
2786
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2715
2787
|
val
|
|
2788
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2789
|
+
battle.rng.damage(player)
|
|
2716
2790
|
else roll: {
|
|
2717
2791
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2718
2792
|
while (true) {
|
|
@@ -2726,21 +2800,27 @@ pub const Rolls = struct {
|
|
|
2726
2800
|
return roll;
|
|
2727
2801
|
}
|
|
2728
2802
|
|
|
2729
|
-
|
|
2803
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2804
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2805
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2730
2806
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2731
2807
|
val == .true
|
|
2808
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2809
|
+
battle.rng.hit(player, acc)
|
|
2732
2810
|
else if (showdown)
|
|
2733
|
-
battle.rng.chance(
|
|
2811
|
+
battle.rng.chance(uN, acc, 256)
|
|
2734
2812
|
else
|
|
2735
|
-
battle.rng.next() <
|
|
2813
|
+
battle.rng.next() < acc;
|
|
2736
2814
|
|
|
2737
|
-
options.chance.hit(ok,
|
|
2815
|
+
try options.chance.hit(player, ok, acc);
|
|
2738
2816
|
return ok;
|
|
2739
2817
|
}
|
|
2740
2818
|
|
|
2741
2819
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2742
2820
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2743
2821
|
val == .true
|
|
2822
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2823
|
+
battle.rng.confused(player)
|
|
2744
2824
|
else if (showdown)
|
|
2745
2825
|
!battle.rng.chance(u8, 128, 256)
|
|
2746
2826
|
else
|
|
@@ -2753,6 +2833,8 @@ pub const Rolls = struct {
|
|
|
2753
2833
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2754
2834
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2755
2835
|
val == .true
|
|
2836
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2837
|
+
battle.rng.paralyzed(player)
|
|
2756
2838
|
else if (showdown)
|
|
2757
2839
|
battle.rng.chance(u8, 63, 256)
|
|
2758
2840
|
else
|
|
@@ -2765,6 +2847,8 @@ pub const Rolls = struct {
|
|
|
2765
2847
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2766
2848
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2767
2849
|
val == .true
|
|
2850
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2851
|
+
battle.rng.secondaryChance(player, 25)
|
|
2768
2852
|
else if (showdown)
|
|
2769
2853
|
battle.rng.chance(u8, 25, 256)
|
|
2770
2854
|
else
|
|
@@ -2779,6 +2863,8 @@ pub const Rolls = struct {
|
|
|
2779
2863
|
|
|
2780
2864
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2781
2865
|
val == .true
|
|
2866
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2867
|
+
battle.rng.secondaryChance(player, rate)
|
|
2782
2868
|
else if (showdown)
|
|
2783
2869
|
battle.rng.chance(u8, rate, 256)
|
|
2784
2870
|
else
|
|
@@ -2793,6 +2879,8 @@ pub const Rolls = struct {
|
|
|
2793
2879
|
|
|
2794
2880
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2795
2881
|
val == .true
|
|
2882
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2883
|
+
battle.rng.secondaryChance(player, rate)
|
|
2796
2884
|
else if (showdown)
|
|
2797
2885
|
battle.rng.chance(u8, rate, 256)
|
|
2798
2886
|
else
|
|
@@ -2805,6 +2893,8 @@ pub const Rolls = struct {
|
|
|
2805
2893
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2806
2894
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2807
2895
|
val == .true
|
|
2896
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2897
|
+
battle.rng.secondaryChance(player, 85)
|
|
2808
2898
|
else if (showdown)
|
|
2809
2899
|
battle.rng.chance(u8, 85, 256)
|
|
2810
2900
|
else
|
|
@@ -2817,6 +2907,8 @@ pub const Rolls = struct {
|
|
|
2817
2907
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2818
2908
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2819
2909
|
val
|
|
2910
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2911
|
+
battle.rng.metronome(player)
|
|
2820
2912
|
else if (showdown)
|
|
2821
2913
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2822
2914
|
else move: {
|
|
@@ -2835,6 +2927,8 @@ pub const Rolls = struct {
|
|
|
2835
2927
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2836
2928
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2837
2929
|
val - 1
|
|
2930
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2931
|
+
battle.rng.psywave(player)
|
|
2838
2932
|
else if (showdown)
|
|
2839
2933
|
battle.rng.range(u8, 0, max)
|
|
2840
2934
|
else power: {
|
|
@@ -2852,6 +2946,8 @@ pub const Rolls = struct {
|
|
|
2852
2946
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2853
2947
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2854
2948
|
@intCast(val)
|
|
2949
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2950
|
+
battle.rng.sleepDuration(player)
|
|
2855
2951
|
else if (showdown)
|
|
2856
2952
|
battle.rng.range(u3, 1, 8)
|
|
2857
2953
|
else duration: {
|
|
@@ -2862,46 +2958,52 @@ pub const Rolls = struct {
|
|
|
2862
2958
|
};
|
|
2863
2959
|
|
|
2864
2960
|
assert(duration >= 1 and duration <= 7);
|
|
2865
|
-
options.chance.duration(
|
|
2961
|
+
options.chance.duration(player, duration);
|
|
2866
2962
|
return duration;
|
|
2867
2963
|
}
|
|
2868
2964
|
|
|
2869
2965
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2870
2966
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2871
2967
|
val
|
|
2968
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2969
|
+
battle.rng.disableDuration(player)
|
|
2872
2970
|
else if (showdown)
|
|
2873
2971
|
battle.rng.range(u4, 1, 9)
|
|
2874
2972
|
else
|
|
2875
2973
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2876
2974
|
|
|
2877
2975
|
assert(duration >= 1 and duration <= 8);
|
|
2878
|
-
options.chance.duration(
|
|
2976
|
+
options.chance.duration(player, duration);
|
|
2879
2977
|
return duration;
|
|
2880
2978
|
}
|
|
2881
2979
|
|
|
2882
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2980
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2883
2981
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2884
2982
|
@intCast(val)
|
|
2983
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2984
|
+
battle.rng.confusionDuration(player)
|
|
2885
2985
|
else if (showdown)
|
|
2886
2986
|
battle.rng.range(u3, 2, 6)
|
|
2887
2987
|
else
|
|
2888
2988
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2889
2989
|
|
|
2890
2990
|
assert(duration >= 2 and duration <= 5);
|
|
2891
|
-
options.chance.duration(
|
|
2991
|
+
options.chance.duration(player, duration);
|
|
2892
2992
|
return duration;
|
|
2893
2993
|
}
|
|
2894
2994
|
|
|
2895
2995
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2896
2996
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
2997
|
@intCast(val)
|
|
2998
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2999
|
+
battle.rng.attackingDuration(player)
|
|
2898
3000
|
else if (showdown)
|
|
2899
3001
|
battle.rng.range(u3, 2, 4)
|
|
2900
3002
|
else
|
|
2901
3003
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2902
3004
|
|
|
2903
3005
|
assert(duration >= 2 and duration <= 3);
|
|
2904
|
-
options.chance.duration(
|
|
3006
|
+
options.chance.duration(player, duration);
|
|
2905
3007
|
return duration;
|
|
2906
3008
|
}
|
|
2907
3009
|
|
|
@@ -2916,6 +3018,8 @@ pub const Rolls = struct {
|
|
|
2916
3018
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2917
3019
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2918
3020
|
@intCast(val)
|
|
3021
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3022
|
+
battle.rng.distribution(player)
|
|
2919
3023
|
else if (showdown)
|
|
2920
3024
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2921
3025
|
else n: {
|
|
@@ -2925,7 +3029,7 @@ pub const Rolls = struct {
|
|
|
2925
3029
|
|
|
2926
3030
|
assert(n >= 2 and n <= 5);
|
|
2927
3031
|
if (effect == .Binding) {
|
|
2928
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, n);
|
|
2929
3033
|
} else {
|
|
2930
3034
|
try options.chance.multiHit(player, n);
|
|
2931
3035
|
}
|
|
@@ -2948,7 +3052,9 @@ pub const Rolls = struct {
|
|
|
2948
3052
|
else
|
|
2949
3053
|
null;
|
|
2950
3054
|
|
|
2951
|
-
const slot: u4 = overridden orelse
|
|
3055
|
+
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3056
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3057
|
+
else slot: {
|
|
2952
3058
|
if (showdown) {
|
|
2953
3059
|
if (check_pp == 0) {
|
|
2954
3060
|
var i: usize = moves.len;
|
|
@@ -3032,7 +3138,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3032
3138
|
const stored = side.stored();
|
|
3033
3139
|
|
|
3034
3140
|
// While players are not given any input options on the cartridge in these cases,
|
|
3035
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3141
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3036
3142
|
//
|
|
3037
3143
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3038
3144
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3040,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3040
3146
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3041
3147
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3042
3148
|
// strictly correct as the player would not have been presented the option to move or
|
|
3043
|
-
// switch at all
|
|
3149
|
+
// switch at all
|
|
3044
3150
|
if (isForced(active)) {
|
|
3045
3151
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3046
3152
|
n += 1;
|
|
@@ -3059,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3059
3165
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3060
3166
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3061
3167
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3062
|
-
// move normally if sleeping/frozen/bound
|
|
3168
|
+
// move normally if sleeping/frozen/bound
|
|
3063
3169
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3064
3170
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3065
3171
|
{
|
|
@@ -3072,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3072
3178
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3073
3179
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3074
3180
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3075
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3181
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3076
3182
|
if (limited) {
|
|
3077
3183
|
assert(showdown);
|
|
3078
3184
|
assert(side.last_selected_move != .None);
|