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