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