@pkmn/engine 0.1.0-dev.c544e10e → 0.1.0-dev.cbdbb719
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 +41 -14
- package/build/pkg/addon.js +6 -7
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +4 -4
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +3 -2
- 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 +208 -81
- package/package.json +32 -21
- package/src/bin/install-pkmn-engine +25 -17
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +4 -4
- package/src/lib/common/debug.zig +2 -2
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +14 -10
- package/src/lib/common/options.zig +2 -0
- package/src/lib/common/protocol.zig +73 -73
- package/src/lib/common/rational.zig +121 -27
- package/src/lib/common/rng.zig +7 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +511 -131
- package/src/lib/gen1/chance.zig +374 -267
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data.zig +15 -14
- package/src/lib/gen1/helpers.zig +96 -536
- package/src/lib/gen1/mechanics.zig +285 -189
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +85 -492
- package/src/lib/gen2/data.zig +8 -7
- package/src/lib/gen2/generated.zig +1 -3
- package/src/lib/gen2/helpers.zig +41 -9
- package/src/lib/gen2/mechanics.zig +178 -121
- package/src/lib/{binding/node.zig → node.zig} +9 -12
- package/src/lib/pkmn.zig +32 -15
- package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
- 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
|
|
|
@@ -71,8 +73,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
73
|
var p1 = battle.side(.P1);
|
|
72
74
|
var p2 = battle.side(.P2);
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
77
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
78
|
|
|
77
79
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
80
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,11 +92,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
92
|
const p1 = battle.side(.P1);
|
|
91
93
|
const p2 = battle.side(.P2);
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
96
|
assert(!showdown or p1_slot == 1);
|
|
95
97
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
const p2_slot = findFirstAlive(p2);
|
|
98
100
|
assert(!showdown or p2_slot == 1);
|
|
99
101
|
if (p2_slot == 0) return Result.Win;
|
|
100
102
|
|
|
@@ -166,7 +168,7 @@ fn selectMove(
|
|
|
166
168
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
169
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
170
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
171
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
172
|
saveMove(battle, player, choice);
|
|
171
173
|
} else {
|
|
172
174
|
assert(choice.data == 0);
|
|
@@ -253,11 +255,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
255
|
active.moves = incoming.moves;
|
|
254
256
|
|
|
255
257
|
statusModify(incoming.status, &active.stats);
|
|
258
|
+
options.chance.switched(player, slot);
|
|
256
259
|
|
|
257
260
|
foe.active.volatiles.Binding = false;
|
|
258
261
|
|
|
259
262
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
263
|
|
|
262
264
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
265
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +349,13 @@ fn doTurn(
|
|
|
347
349
|
)) |r| return r;
|
|
348
350
|
if (!replace) {
|
|
349
351
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
352
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
353
|
}
|
|
352
354
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
355
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
356
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
357
|
|
|
356
|
-
options.chance.clearPending();
|
|
358
|
+
if (!showdown) options.chance.clearPending();
|
|
357
359
|
|
|
358
360
|
residual = true;
|
|
359
361
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +371,10 @@ fn doTurn(
|
|
|
369
371
|
)) |r| return r;
|
|
370
372
|
if (!replace) {
|
|
371
373
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
374
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
375
|
}
|
|
374
376
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
377
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
378
|
}
|
|
377
379
|
|
|
378
380
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -431,7 +433,7 @@ fn executeMove(
|
|
|
431
433
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
434
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
435
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
434
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
436
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
437
|
if (!auto) {
|
|
436
438
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
439
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -484,17 +486,22 @@ fn beforeMove(
|
|
|
484
486
|
|
|
485
487
|
if (Status.is(stored.status, .SLP)) {
|
|
486
488
|
const before = stored.status;
|
|
489
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
490
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
491
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
492
|
+
.started, .ended => stored.status = 0,
|
|
493
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
494
|
+
stored.status -= 1;
|
|
495
|
+
},
|
|
496
|
+
else => unreachable,
|
|
490
497
|
} else {
|
|
491
498
|
stored.status -= 1;
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
502
|
+
try options.chance.sleep(player, if (slf)
|
|
503
|
+
.None
|
|
504
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
505
|
|
|
499
506
|
if (duration == 0) {
|
|
500
507
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +539,12 @@ fn beforeMove(
|
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
volatiles.disable_duration =
|
|
543
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
544
|
+
try options.chance.disable(
|
|
545
|
+
player,
|
|
546
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
547
|
+
);
|
|
541
548
|
|
|
542
549
|
if (volatiles.disable_duration == 0) {
|
|
543
550
|
volatiles.disable_move = 0;
|
|
@@ -556,13 +563,19 @@ fn beforeMove(
|
|
|
556
563
|
|
|
557
564
|
if (volatiles.Confusion) {
|
|
558
565
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
566
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
567
|
+
.started => volatiles.confusion -= 1,
|
|
568
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
569
|
+
volatiles.confusion -= 1;
|
|
570
|
+
},
|
|
571
|
+
.ended => volatiles.confusion = 0,
|
|
572
|
+
.overwritten => {},
|
|
573
|
+
else => unreachable,
|
|
562
574
|
} else {
|
|
563
575
|
volatiles.confusion -= 1;
|
|
564
576
|
}
|
|
565
|
-
|
|
577
|
+
|
|
578
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
579
|
|
|
567
580
|
if (volatiles.confusion == 0) {
|
|
568
581
|
volatiles.Confusion = false;
|
|
@@ -580,7 +593,9 @@ fn beforeMove(
|
|
|
580
593
|
volatiles.Charging = false;
|
|
581
594
|
volatiles.Binding = false;
|
|
582
595
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
596
|
+
|
|
597
|
+
options.chance.observe(.attacking, player, .None);
|
|
598
|
+
options.chance.observe(.binding, player, .None);
|
|
584
599
|
{
|
|
585
600
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
601
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +603,9 @@ fn beforeMove(
|
|
|
588
603
|
const def = foe.active.stats.def;
|
|
589
604
|
foe.active.stats.def = active.stats.def;
|
|
590
605
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
606
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
607
|
+
return .err;
|
|
608
|
+
}
|
|
592
609
|
}
|
|
593
610
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
611
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +634,10 @@ fn beforeMove(
|
|
|
617
634
|
volatiles.Thrashing = false;
|
|
618
635
|
volatiles.Charging = false;
|
|
619
636
|
volatiles.Binding = false;
|
|
620
|
-
|
|
637
|
+
|
|
638
|
+
options.chance.observe(.attacking, player, .None);
|
|
639
|
+
options.chance.observe(.binding, player, .None);
|
|
640
|
+
|
|
621
641
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
642
|
try log.cant(.{ ident, .Paralysis });
|
|
623
643
|
return .done;
|
|
@@ -635,14 +655,8 @@ fn beforeMove(
|
|
|
635
655
|
volatiles.state +%= battle.last_damage;
|
|
636
656
|
}
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
641
|
-
if (!extend) volatiles.attacks = 0;
|
|
642
|
-
} else {
|
|
643
|
-
volatiles.attacks -= 1;
|
|
644
|
-
}
|
|
645
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
658
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
659
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
646
660
|
|
|
647
661
|
if (volatiles.attacks != 0) {
|
|
648
662
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +690,36 @@ fn beforeMove(
|
|
|
676
690
|
}
|
|
677
691
|
|
|
678
692
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
693
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
694
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
695
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
696
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
697
|
+
|
|
698
|
+
if (volatiles.attacks == 0) {
|
|
699
|
+
const overwritten = volatiles.Confusion;
|
|
687
700
|
|
|
688
|
-
|
|
701
|
+
volatiles.Thrashing = false;
|
|
702
|
+
volatiles.Confusion = true;
|
|
703
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
704
|
+
|
|
705
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
706
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
707
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
708
|
+
// correct player)
|
|
689
709
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
710
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
690
711
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
712
|
+
|
|
713
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
714
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
715
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
716
|
+
|
|
700
717
|
return .skip_can;
|
|
701
718
|
}
|
|
702
719
|
|
|
703
720
|
if (volatiles.Binding) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (options.calc.modify(player, .binding)) |extend| {
|
|
707
|
-
if (!extend) volatiles.attacks = 0;
|
|
708
|
-
} else {
|
|
709
|
-
volatiles.attacks -= 1;
|
|
710
|
-
}
|
|
711
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
721
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
722
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
712
723
|
|
|
713
724
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
725
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +749,6 @@ fn canMove(
|
|
|
738
749
|
const player_ident = battle.active(player);
|
|
739
750
|
const move = Move.get(side.last_selected_move);
|
|
740
751
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
752
|
if (side.active.volatiles.Charging) {
|
|
743
753
|
side.active.volatiles.Charging = false;
|
|
744
754
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +760,7 @@ fn canMove(
|
|
|
750
760
|
}
|
|
751
761
|
|
|
752
762
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
763
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
764
|
|
|
755
765
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
766
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -802,7 +812,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
812
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
813
|
}
|
|
804
814
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
815
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
816
|
var active = &side.active;
|
|
807
817
|
const volatiles = &active.volatiles;
|
|
808
818
|
|
|
@@ -810,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
820
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
821
|
if (showdown and volatiles.Transform) return;
|
|
812
822
|
|
|
823
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
824
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
825
|
+
|
|
813
826
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
827
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
815
828
|
}
|
|
816
829
|
|
|
817
830
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -859,13 +872,13 @@ fn doMove(
|
|
|
859
872
|
break :miss true;
|
|
860
873
|
}
|
|
861
874
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
875
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
876
|
};
|
|
864
877
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
878
|
|
|
866
879
|
var late = showdown and move.effect != .Explode;
|
|
867
880
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
881
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
882
|
if (showdown and move.effect.isMulti()) {
|
|
870
883
|
try Effects.multiHit(battle, player, move, options);
|
|
871
884
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,8 +900,9 @@ fn doMove(
|
|
|
887
900
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
901
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
902
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
903
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
904
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
905
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
892
906
|
return @as(?Result, Result.Error);
|
|
893
907
|
}
|
|
894
908
|
if (battle.last_damage == 0) {
|
|
@@ -902,11 +916,12 @@ fn doMove(
|
|
|
902
916
|
}
|
|
903
917
|
}
|
|
904
918
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
919
|
+
var missed: bool = undefined;
|
|
920
|
+
const zero = battle.last_damage == 0;
|
|
921
|
+
if (!showdown and !skip) {
|
|
922
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
923
|
+
miss = missed or zero;
|
|
924
|
+
}
|
|
910
925
|
|
|
911
926
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
927
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +948,32 @@ fn doMove(
|
|
|
933
948
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
949
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
950
|
try log.immune(.{ foe_ident, .None });
|
|
951
|
+
if (!showdown) {
|
|
952
|
+
if (move.effect == .Binding) {
|
|
953
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
954
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
955
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
956
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
957
|
+
// (considered missing)
|
|
958
|
+
assert(zero);
|
|
959
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
960
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
961
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
962
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
963
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
964
|
+
// roll)
|
|
965
|
+
try options.chance.commit(player, .binding);
|
|
966
|
+
} else {
|
|
967
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
968
|
+
// division-by-zero freeze alternative timeline scenario
|
|
969
|
+
try options.chance.commit(player, .glitch);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
936
972
|
} else if (mist) {
|
|
937
973
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
974
|
try log.fail(.{ foe_ident, .None });
|
|
939
975
|
} else {
|
|
940
|
-
if (showdown
|
|
976
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
977
|
try log.lastmiss(.{});
|
|
942
978
|
try log.miss(.{battle.active(player)});
|
|
943
979
|
}
|
|
@@ -952,17 +988,15 @@ fn doMove(
|
|
|
952
988
|
try buildRage(battle, player.foe(), options);
|
|
953
989
|
} else if (showdown and move.effect == .Disable) {
|
|
954
990
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
991
|
}
|
|
958
992
|
return null;
|
|
959
|
-
} else {
|
|
993
|
+
} else if (!showdown) {
|
|
960
994
|
try options.chance.commit(player, .hit);
|
|
961
995
|
}
|
|
962
996
|
|
|
963
997
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
998
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
999
|
+
// it up above before damage calculation)
|
|
966
1000
|
if (!showdown and move.effect.isMulti()) {
|
|
967
1001
|
try Effects.multiHit(battle, player, move, options);
|
|
968
1002
|
hits = side.active.volatiles.attacks;
|
|
@@ -1021,12 +1055,12 @@ fn doMove(
|
|
|
1021
1055
|
}
|
|
1022
1056
|
|
|
1023
1057
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1024
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1058
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1025
1059
|
if (nullified) return null;
|
|
1026
1060
|
|
|
1027
1061
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1062
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1029
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1063
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1030
1064
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1065
|
|
|
1032
1066
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1075,7 @@ fn doMove(
|
|
|
1041
1075
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1076
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1077
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1078
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1079
|
if (move.effect == .Twineedle) {
|
|
1046
1080
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1081
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1094,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1094
|
const side = battle.side(player);
|
|
1061
1095
|
|
|
1062
1096
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1097
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1098
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1099
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1100
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1101
|
|
|
1068
|
-
|
|
1102
|
+
const spurious =
|
|
1103
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1104
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1069
1105
|
}
|
|
1070
1106
|
|
|
1071
1107
|
fn calcDamage(
|
|
@@ -1074,6 +1110,7 @@ fn calcDamage(
|
|
|
1074
1110
|
target_player: Player,
|
|
1075
1111
|
m: ?Move.Data,
|
|
1076
1112
|
crit: bool,
|
|
1113
|
+
options: anytype,
|
|
1077
1114
|
) bool {
|
|
1078
1115
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1116
|
const cfz = m == null;
|
|
@@ -1085,28 +1122,8 @@ fn calcDamage(
|
|
|
1085
1122
|
|
|
1086
1123
|
const special = move.type.special();
|
|
1087
1124
|
|
|
1088
|
-
|
|
1089
|
-
var
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) side.stored().stats.spc
|
|
1092
|
-
else side.stored().stats.atk
|
|
1093
|
-
else
|
|
1094
|
-
if (special) side.active.stats.spc
|
|
1095
|
-
else side.active.stats.atk;
|
|
1096
|
-
var def: u32 =
|
|
1097
|
-
if (crit)
|
|
1098
|
-
if (special) target.stored().stats.spc
|
|
1099
|
-
else target.stored().stats.def
|
|
1100
|
-
else
|
|
1101
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1102
|
-
if (special)
|
|
1103
|
-
target.active.stats.spc *
|
|
1104
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1105
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1106
|
-
else
|
|
1107
|
-
target.active.stats.def *
|
|
1108
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1109
|
-
// zig fmt: on
|
|
1125
|
+
var atk = attack(side, crit, special);
|
|
1126
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1127
|
|
|
1111
1128
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1129
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1117,9 +1134,9 @@ fn calcDamage(
|
|
|
1117
1134
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1118
1135
|
}
|
|
1119
1136
|
|
|
1120
|
-
const lvl =
|
|
1137
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1138
|
|
|
1122
|
-
def =
|
|
1139
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1140
|
|
|
1124
1141
|
if (def == 0) return false;
|
|
1125
1142
|
|
|
@@ -1131,11 +1148,41 @@ fn calcDamage(
|
|
|
1131
1148
|
d = @min(997, d);
|
|
1132
1149
|
d += 2;
|
|
1133
1150
|
|
|
1151
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1152
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1153
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1154
|
+
// that results in counter intuitive probabilities in the common case
|
|
1155
|
+
if (pkmn.options.chance and !showdown) {
|
|
1156
|
+
atk = attack(side, !crit, special);
|
|
1157
|
+
def = defense(target, !crit, special, false);
|
|
1158
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1134
1161
|
battle.last_damage = @intCast(d);
|
|
1135
1162
|
|
|
1136
1163
|
return true;
|
|
1137
1164
|
}
|
|
1138
1165
|
|
|
1166
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1167
|
+
return if (crit)
|
|
1168
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1169
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1173
|
+
return if (crit)
|
|
1174
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1175
|
+
else
|
|
1176
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1177
|
+
if (special)
|
|
1178
|
+
target.active.stats.spc *
|
|
1179
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1180
|
+
else
|
|
1181
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1182
|
+
target.active.stats.def *
|
|
1183
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1139
1186
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1187
|
const side = battle.side(player);
|
|
1141
1188
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1227
|
const foe = battle.foe(player);
|
|
1181
1228
|
|
|
1182
1229
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1230
|
|
|
1185
1231
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1232
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1241,9 +1287,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1241
1287
|
|
|
1242
1288
|
battle.last_damage *|= 2;
|
|
1243
1289
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1290
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1291
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1292
|
|
|
1248
1293
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1294
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1317,7 @@ fn applyDamage(
|
|
|
1272
1317
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1318
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1319
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1320
|
+
options.calc.capped(sub_player);
|
|
1276
1321
|
return true;
|
|
1277
1322
|
} else {
|
|
1278
1323
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1351,7 @@ fn mirrorMove(
|
|
|
1306
1351
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1352
|
return null;
|
|
1308
1353
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1354
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1355
|
}
|
|
1311
1356
|
|
|
1312
1357
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1372,7 @@ fn metronome(
|
|
|
1327
1372
|
var side = battle.side(player);
|
|
1328
1373
|
|
|
1329
1374
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1375
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1376
|
|
|
1332
1377
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1378
|
return null;
|
|
@@ -1339,7 +1384,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1384
|
var immune = false;
|
|
1340
1385
|
var mist = false;
|
|
1341
1386
|
|
|
1342
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1387
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1388
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1343
1391
|
|
|
1344
1392
|
assert(!immune);
|
|
1345
1393
|
if (mist) {
|
|
@@ -1348,11 +1396,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1396
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1397
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1398
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1399
|
try options.log.lastmiss(.{});
|
|
1353
1400
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1401
|
}
|
|
1355
1402
|
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1404
|
return false;
|
|
1357
1405
|
}
|
|
1358
1406
|
|
|
@@ -1363,7 +1411,7 @@ fn moveHit(
|
|
|
1363
1411
|
immune: *bool,
|
|
1364
1412
|
mist: *bool,
|
|
1365
1413
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1414
|
+
) !bool {
|
|
1367
1415
|
var side = battle.side(player);
|
|
1368
1416
|
const foe = battle.foe(player);
|
|
1369
1417
|
|
|
@@ -1402,7 +1450,7 @@ fn moveHit(
|
|
|
1402
1450
|
const overwritten = overwrite and state > 0;
|
|
1403
1451
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1452
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1453
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1454
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1455
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1456
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1462,7 @@ fn moveHit(
|
|
|
1414
1462
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1463
|
|
|
1416
1464
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1465
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1466
|
};
|
|
1419
1467
|
|
|
1420
1468
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1473,17 @@ fn moveHit(
|
|
|
1425
1473
|
|
|
1426
1474
|
if (!miss) return true;
|
|
1427
1475
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1476
|
+
|
|
1428
1477
|
side.active.volatiles.Binding = false;
|
|
1478
|
+
options.chance.observe(.binding, player, .None);
|
|
1479
|
+
|
|
1429
1480
|
return false;
|
|
1430
1481
|
}
|
|
1431
1482
|
|
|
1432
1483
|
fn checkFaint(
|
|
1433
1484
|
battle: anytype,
|
|
1434
1485
|
player: Player,
|
|
1486
|
+
cap: bool,
|
|
1435
1487
|
options: anytype,
|
|
1436
1488
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1489
|
const side = battle.side(player);
|
|
@@ -1444,8 +1496,8 @@ fn checkFaint(
|
|
|
1444
1496
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1497
|
const more = player_out or foe_out;
|
|
1446
1498
|
|
|
1447
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1448
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1499
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1500
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1449
1501
|
|
|
1450
1502
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1503
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1466,7 +1518,7 @@ fn checkFaint(
|
|
|
1466
1518
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1519
|
}
|
|
1468
1520
|
|
|
1469
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1521
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1470
1522
|
var side = battle.side(player);
|
|
1471
1523
|
var foe = battle.foe(player);
|
|
1472
1524
|
assert(side.stored().hp == 0);
|
|
@@ -1500,7 +1552,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1552
|
}
|
|
1501
1553
|
|
|
1502
1554
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
1555
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1556
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1557
|
+
if (cap) options.calc.capped(player);
|
|
1504
1558
|
return null;
|
|
1505
1559
|
}
|
|
1506
1560
|
|
|
@@ -1660,17 +1714,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1714
|
}
|
|
1661
1715
|
}
|
|
1662
1716
|
|
|
1663
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1664
|
-
var volatiles = &active.volatiles;
|
|
1665
|
-
assert(volatiles.Thrashing);
|
|
1666
|
-
if (volatiles.attacks > 0) return false;
|
|
1667
|
-
|
|
1668
|
-
volatiles.Thrashing = false;
|
|
1669
|
-
volatiles.Confusion = true;
|
|
1670
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
1717
|
fn onBegin(
|
|
1675
1718
|
battle: anytype,
|
|
1676
1719
|
player: Player,
|
|
@@ -1679,7 +1722,6 @@ fn onBegin(
|
|
|
1679
1722
|
residual: *bool,
|
|
1680
1723
|
options: anytype,
|
|
1681
1724
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1725
|
return switch (move.effect) {
|
|
1684
1726
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1727
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1744,6 @@ fn onBegin(
|
|
|
1702
1744
|
}
|
|
1703
1745
|
|
|
1704
1746
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1747
|
return switch (move.effect) {
|
|
1707
1748
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1749
|
// zig fmt: off
|
|
@@ -1724,7 +1765,6 @@ fn alwaysHappens(
|
|
|
1724
1765
|
residual: *bool,
|
|
1725
1766
|
options: anytype,
|
|
1726
1767
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1768
|
return switch (move.effect) {
|
|
1729
1769
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1770
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1800,10 @@ pub const Effects = struct {
|
|
|
1760
1800
|
|
|
1761
1801
|
side.active.volatiles.Bide = true;
|
|
1762
1802
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1803
|
+
|
|
1763
1804
|
side.active.volatiles.state = 0;
|
|
1764
1805
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1806
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1807
|
|
|
1766
1808
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1809
|
}
|
|
@@ -1823,12 +1865,13 @@ pub const Effects = struct {
|
|
|
1823
1865
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1866
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1867
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1868
|
}
|
|
1828
1869
|
|
|
1829
1870
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1871
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
1872
|
+
|
|
1873
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1874
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1832
1875
|
|
|
1833
1876
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1877
|
}
|
|
@@ -1856,10 +1899,7 @@ pub const Effects = struct {
|
|
|
1856
1899
|
const foe_ident = battle.active(player.foe());
|
|
1857
1900
|
|
|
1858
1901
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1859
|
-
if (!showdown)
|
|
1860
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1861
|
-
try options.chance.commit(player, .hit);
|
|
1862
|
-
}
|
|
1902
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1903
|
|
|
1864
1904
|
if (volatiles.disable_move != 0) {
|
|
1865
1905
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1889,6 +1929,7 @@ pub const Effects = struct {
|
|
|
1889
1929
|
volatiles.disable_move =
|
|
1890
1930
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1931
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1932
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1933
|
|
|
1893
1934
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1935
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1972,7 +2013,7 @@ pub const Effects = struct {
|
|
|
1972
2013
|
var side = battle.side(player);
|
|
1973
2014
|
var foe = battle.foe(player);
|
|
1974
2015
|
|
|
1975
|
-
|
|
2016
|
+
const side_stored = side.stored();
|
|
1976
2017
|
var foe_stored = foe.stored();
|
|
1977
2018
|
|
|
1978
2019
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2036,24 @@ pub const Effects = struct {
|
|
|
1995
2036
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2037
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2038
|
s.stored().status = 0;
|
|
2039
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2040
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2041
|
s.stored().status = Status.init(.PSN);
|
|
2000
2042
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2043
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2044
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2045
|
}
|
|
2004
2046
|
} else {
|
|
2005
2047
|
if (Status.any(foe_stored.status)) {
|
|
2006
2048
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2049
|
foe.last_selected_move = .SKIP_TURN;
|
|
2050
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2051
|
}
|
|
2009
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2053
|
foe_stored.status = 0;
|
|
2011
2054
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2055
|
+
try clearVolatiles(battle, player, options);
|
|
2056
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2057
|
}
|
|
2015
2058
|
}
|
|
2016
2059
|
|
|
@@ -2061,7 +2104,6 @@ pub const Effects = struct {
|
|
|
2061
2104
|
}
|
|
2062
2105
|
}
|
|
2063
2106
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2107
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2108
|
|
|
2067
2109
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2144,10 @@ pub const Effects = struct {
|
|
|
2102
2144
|
if (!has_mimic) {
|
|
2103
2145
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2146
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2147
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2148
|
}
|
|
2108
2149
|
}
|
|
2109
2150
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2151
|
|
|
2112
2152
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2153
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2201,6 @@ pub const Effects = struct {
|
|
|
2161
2201
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2202
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2203
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2204
|
|
|
2166
2205
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2206
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2268,6 @@ pub const Effects = struct {
|
|
|
2229
2268
|
|
|
2230
2269
|
if (move.effect == .Poison) {
|
|
2231
2270
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2271
|
} else {
|
|
2234
2272
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2273
|
return;
|
|
@@ -2298,9 +2336,8 @@ pub const Effects = struct {
|
|
|
2298
2336
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2337
|
});
|
|
2300
2338
|
}
|
|
2301
|
-
// If
|
|
2339
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2340
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2341
|
}
|
|
2305
2342
|
|
|
2306
2343
|
// Sleep Clause Mod
|
|
@@ -2312,6 +2349,8 @@ pub const Effects = struct {
|
|
|
2312
2349
|
foe.active.volatiles.Recharging = false;
|
|
2313
2350
|
|
|
2314
2351
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2352
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2353
|
+
|
|
2315
2354
|
const last = battle.side(player).last_selected_move;
|
|
2316
2355
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2356
|
}
|
|
@@ -2353,9 +2392,7 @@ pub const Effects = struct {
|
|
|
2353
2392
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2393
|
if (showdown) {
|
|
2355
2394
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2395
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2396
|
battle.last_damage = 0;
|
|
2360
2397
|
} else {
|
|
2361
2398
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2407,8 @@ pub const Effects = struct {
|
|
|
2370
2407
|
|
|
2371
2408
|
volatiles.Thrashing = true;
|
|
2372
2409
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2410
|
+
|
|
2411
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2412
|
}
|
|
2374
2413
|
|
|
2375
2414
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2415,6 +2454,7 @@ pub const Effects = struct {
|
|
|
2415
2454
|
|
|
2416
2455
|
side.active.volatiles.attacks =
|
|
2417
2456
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2457
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2458
|
}
|
|
2419
2459
|
|
|
2420
2460
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2481,7 @@ pub const Effects = struct {
|
|
|
2441
2481
|
}
|
|
2442
2482
|
return log.fail(.{ ident, .None });
|
|
2443
2483
|
}
|
|
2444
|
-
|
|
2484
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2485
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2486
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2487
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2501,7 @@ pub const Effects = struct {
|
|
|
2461
2501
|
}
|
|
2462
2502
|
return log.fail(.{ ident, .None });
|
|
2463
2503
|
}
|
|
2464
|
-
|
|
2504
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2505
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2506
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2507
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2518,7 @@ pub const Effects = struct {
|
|
|
2478
2518
|
}
|
|
2479
2519
|
return log.fail(.{ ident, .None });
|
|
2480
2520
|
}
|
|
2481
|
-
|
|
2521
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2522
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2523
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2524
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2538,7 @@ pub const Effects = struct {
|
|
|
2498
2538
|
}
|
|
2499
2539
|
return log.fail(.{ ident, .None });
|
|
2500
2540
|
}
|
|
2501
|
-
|
|
2541
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2542
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2543
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2544
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2571,6 @@ pub const Effects = struct {
|
|
|
2531
2571
|
} else {
|
|
2532
2572
|
// checkHit already checks for Invulnerable
|
|
2533
2573
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2574
|
}
|
|
2536
2575
|
|
|
2537
2576
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2590,7 @@ pub const Effects = struct {
|
|
|
2551
2590
|
}
|
|
2552
2591
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2592
|
}
|
|
2554
|
-
|
|
2593
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2594
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2595
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2596
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2608,7 @@ pub const Effects = struct {
|
|
|
2569
2608
|
}
|
|
2570
2609
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2610
|
}
|
|
2572
|
-
|
|
2611
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2612
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2613
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2614
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2625,7 @@ pub const Effects = struct {
|
|
|
2586
2625
|
}
|
|
2587
2626
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2627
|
}
|
|
2589
|
-
|
|
2628
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2629
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2630
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2631
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2645,7 @@ pub const Effects = struct {
|
|
|
2606
2645
|
}
|
|
2607
2646
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2647
|
}
|
|
2609
|
-
|
|
2648
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2649
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2650
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2651
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2684,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2684
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2685
|
}
|
|
2647
2686
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2687
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2688
|
var log = options.log;
|
|
2650
2689
|
var side = battle.side(who);
|
|
2651
2690
|
var volatiles = &side.active.volatiles;
|
|
2652
2691
|
const ident = battle.active(who);
|
|
2653
2692
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2693
|
if (volatiles.disable_move != 0) {
|
|
2657
2694
|
volatiles.disable_move = 0;
|
|
2658
2695
|
volatiles.disable_duration = 0;
|
|
2696
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2697
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2698
|
}
|
|
2661
2699
|
if (volatiles.Confusion) {
|
|
2662
2700
|
// volatiles.confusion is left unchanged
|
|
2663
2701
|
volatiles.Confusion = false;
|
|
2702
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2703
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2704
|
}
|
|
2666
2705
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2734,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2734
|
}
|
|
2696
2735
|
}
|
|
2697
2736
|
|
|
2737
|
+
fn decrement(
|
|
2738
|
+
comptime field: chance.Action.Field,
|
|
2739
|
+
player: Player,
|
|
2740
|
+
options: anytype,
|
|
2741
|
+
n: anytype,
|
|
2742
|
+
) @TypeOf(n) {
|
|
2743
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2744
|
+
.started, .ended => 0,
|
|
2745
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2746
|
+
else => unreachable,
|
|
2747
|
+
} else n - 1;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2698
2750
|
pub const Rolls = struct {
|
|
2699
2751
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2752
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2753
|
player == .P1
|
|
2754
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2755
|
+
battle.rng.speedTie()
|
|
2702
2756
|
else if (showdown)
|
|
2703
2757
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2758
|
else
|
|
@@ -2708,21 +2762,31 @@ pub const Rolls = struct {
|
|
|
2708
2762
|
return p1;
|
|
2709
2763
|
}
|
|
2710
2764
|
|
|
2711
|
-
fn criticalHit(
|
|
2765
|
+
fn criticalHit(
|
|
2766
|
+
battle: anytype,
|
|
2767
|
+
player: Player,
|
|
2768
|
+
rate: u8,
|
|
2769
|
+
spurious: bool,
|
|
2770
|
+
options: anytype,
|
|
2771
|
+
) !bool {
|
|
2712
2772
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2773
|
val == .true
|
|
2774
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2775
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2776
|
else if (showdown)
|
|
2715
2777
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2778
|
else
|
|
2717
2779
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2780
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2781
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2782
|
return crit;
|
|
2721
2783
|
}
|
|
2722
2784
|
|
|
2723
2785
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2786
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2787
|
val
|
|
2788
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2789
|
+
battle.rng.damage(player)
|
|
2726
2790
|
else roll: {
|
|
2727
2791
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2792
|
while (true) {
|
|
@@ -2736,21 +2800,27 @@ pub const Rolls = struct {
|
|
|
2736
2800
|
return roll;
|
|
2737
2801
|
}
|
|
2738
2802
|
|
|
2739
|
-
|
|
2803
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2804
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2805
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2740
2806
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2807
|
val == .true
|
|
2808
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2809
|
+
battle.rng.hit(player, acc)
|
|
2742
2810
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2811
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2812
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2813
|
+
battle.rng.next() < acc;
|
|
2746
2814
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2815
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2816
|
return ok;
|
|
2749
2817
|
}
|
|
2750
2818
|
|
|
2751
2819
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2820
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2821
|
val == .true
|
|
2822
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2823
|
+
battle.rng.confused(player)
|
|
2754
2824
|
else if (showdown)
|
|
2755
2825
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2826
|
else
|
|
@@ -2763,6 +2833,8 @@ pub const Rolls = struct {
|
|
|
2763
2833
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2834
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2835
|
val == .true
|
|
2836
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2837
|
+
battle.rng.paralyzed(player)
|
|
2766
2838
|
else if (showdown)
|
|
2767
2839
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2840
|
else
|
|
@@ -2775,6 +2847,8 @@ pub const Rolls = struct {
|
|
|
2775
2847
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2848
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2849
|
val == .true
|
|
2850
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2851
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2852
|
else if (showdown)
|
|
2779
2853
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2854
|
else
|
|
@@ -2789,6 +2863,8 @@ pub const Rolls = struct {
|
|
|
2789
2863
|
|
|
2790
2864
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2865
|
val == .true
|
|
2866
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2867
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2868
|
else if (showdown)
|
|
2793
2869
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2870
|
else
|
|
@@ -2803,6 +2879,8 @@ pub const Rolls = struct {
|
|
|
2803
2879
|
|
|
2804
2880
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2881
|
val == .true
|
|
2882
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2883
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2884
|
else if (showdown)
|
|
2807
2885
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2886
|
else
|
|
@@ -2815,6 +2893,8 @@ pub const Rolls = struct {
|
|
|
2815
2893
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2894
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2895
|
val == .true
|
|
2896
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2897
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2898
|
else if (showdown)
|
|
2819
2899
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2900
|
else
|
|
@@ -2827,6 +2907,8 @@ pub const Rolls = struct {
|
|
|
2827
2907
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2908
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2909
|
val
|
|
2910
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2911
|
+
battle.rng.metronome(player)
|
|
2830
2912
|
else if (showdown)
|
|
2831
2913
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2914
|
else move: {
|
|
@@ -2845,6 +2927,8 @@ pub const Rolls = struct {
|
|
|
2845
2927
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2928
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2929
|
val - 1
|
|
2930
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2931
|
+
battle.rng.psywave(player)
|
|
2848
2932
|
else if (showdown)
|
|
2849
2933
|
battle.rng.range(u8, 0, max)
|
|
2850
2934
|
else power: {
|
|
@@ -2862,6 +2946,8 @@ pub const Rolls = struct {
|
|
|
2862
2946
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
2947
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
2948
|
@intCast(val)
|
|
2949
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2950
|
+
battle.rng.sleepDuration(player)
|
|
2865
2951
|
else if (showdown)
|
|
2866
2952
|
battle.rng.range(u3, 1, 8)
|
|
2867
2953
|
else duration: {
|
|
@@ -2872,46 +2958,52 @@ pub const Rolls = struct {
|
|
|
2872
2958
|
};
|
|
2873
2959
|
|
|
2874
2960
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2961
|
+
options.chance.duration(player, duration);
|
|
2876
2962
|
return duration;
|
|
2877
2963
|
}
|
|
2878
2964
|
|
|
2879
2965
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
2966
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
2967
|
val
|
|
2968
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2969
|
+
battle.rng.disableDuration(player)
|
|
2882
2970
|
else if (showdown)
|
|
2883
2971
|
battle.rng.range(u4, 1, 9)
|
|
2884
2972
|
else
|
|
2885
2973
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2974
|
|
|
2887
2975
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
2976
|
+
options.chance.duration(player, duration);
|
|
2889
2977
|
return duration;
|
|
2890
2978
|
}
|
|
2891
2979
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2980
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2893
2981
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
2982
|
@intCast(val)
|
|
2983
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2984
|
+
battle.rng.confusionDuration(player)
|
|
2895
2985
|
else if (showdown)
|
|
2896
2986
|
battle.rng.range(u3, 2, 6)
|
|
2897
2987
|
else
|
|
2898
2988
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
2989
|
|
|
2900
2990
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
2991
|
+
options.chance.duration(player, duration);
|
|
2902
2992
|
return duration;
|
|
2903
2993
|
}
|
|
2904
2994
|
|
|
2905
2995
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
2996
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
2997
|
@intCast(val)
|
|
2998
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2999
|
+
battle.rng.attackingDuration(player)
|
|
2908
3000
|
else if (showdown)
|
|
2909
3001
|
battle.rng.range(u3, 2, 4)
|
|
2910
3002
|
else
|
|
2911
3003
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3004
|
|
|
2913
3005
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3006
|
+
options.chance.duration(player, duration);
|
|
2915
3007
|
return duration;
|
|
2916
3008
|
}
|
|
2917
3009
|
|
|
@@ -2926,6 +3018,8 @@ pub const Rolls = struct {
|
|
|
2926
3018
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
3019
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
3020
|
@intCast(val)
|
|
3021
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3022
|
+
battle.rng.distribution(player)
|
|
2929
3023
|
else if (showdown)
|
|
2930
3024
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3025
|
else n: {
|
|
@@ -2935,7 +3029,7 @@ pub const Rolls = struct {
|
|
|
2935
3029
|
|
|
2936
3030
|
assert(n >= 2 and n <= 5);
|
|
2937
3031
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, n);
|
|
2939
3033
|
} else {
|
|
2940
3034
|
try options.chance.multiHit(player, n);
|
|
2941
3035
|
}
|
|
@@ -2958,7 +3052,9 @@ pub const Rolls = struct {
|
|
|
2958
3052
|
else
|
|
2959
3053
|
null;
|
|
2960
3054
|
|
|
2961
|
-
const slot: u4 = overridden orelse
|
|
3055
|
+
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3056
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3057
|
+
else slot: {
|
|
2962
3058
|
if (showdown) {
|
|
2963
3059
|
if (check_pp == 0) {
|
|
2964
3060
|
var i: usize = moves.len;
|
|
@@ -3038,11 +3134,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3134
|
const side = battle.side(player);
|
|
3039
3135
|
const foe = battle.foe(player);
|
|
3040
3136
|
|
|
3041
|
-
|
|
3137
|
+
const active = &side.active;
|
|
3042
3138
|
const stored = side.stored();
|
|
3043
3139
|
|
|
3044
3140
|
// While players are not given any input options on the cartridge in these cases,
|
|
3045
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3141
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3142
|
//
|
|
3047
3143
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3144
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3146
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3147
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3148
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3149
|
+
// switch at all
|
|
3054
3150
|
if (isForced(active)) {
|
|
3055
3151
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3152
|
n += 1;
|
|
@@ -3069,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3165
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3166
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3167
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3168
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3169
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3170
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3171
|
{
|
|
@@ -3082,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3178
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3179
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3180
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3085
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3181
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3182
|
if (limited) {
|
|
3087
3183
|
assert(showdown);
|
|
3088
3184
|
assert(side.last_selected_move != .None);
|