@pkmn/engine 0.1.0-dev.b3f3e3a6 → 0.1.0-dev.b9015a94
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/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +43 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +96 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon.d.ts +2 -0
- package/build/pkg/addon.js +35 -58
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data.d.ts +27 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +24 -10
- package/build/pkg/index.js +15 -10
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +35 -69
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +128 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +132 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +349 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +18 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +206 -81
- package/package.json +49 -28
- package/src/bin/install-pkmn-engine +29 -19
- package/src/bin/pkmn-debug +1 -1
- 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 +75 -74
- 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 +509 -131
- package/src/lib/gen1/chance.zig +374 -267
- package/src/lib/gen1/data/species.zig +1 -1
- package/src/lib/gen1/data.zig +17 -16
- package/src/lib/gen1/helpers.zig +95 -538
- package/src/lib/gen1/mechanics.zig +284 -191
- package/src/lib/gen2/calc.zig +34 -16
- package/src/lib/gen2/chance.zig +84 -496
- package/src/lib/gen2/data.zig +8 -7
- package/src/lib/gen2/generated.zig +1 -3
- package/src/lib/gen2/helpers.zig +41 -13
- package/src/lib/gen2/mechanics.zig +178 -127
- 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/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +351 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/lib/gen1/README.md +0 -894
- package/src/lib/gen2/README.md +0 -329
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -8,6 +8,7 @@ const protocol = @import("../common/protocol.zig");
|
|
|
8
8
|
const rng = @import("../common/rng.zig");
|
|
9
9
|
|
|
10
10
|
const data = @import("data.zig");
|
|
11
|
+
const chance = @import("chance.zig");
|
|
11
12
|
|
|
12
13
|
const assert = std.debug.assert;
|
|
13
14
|
|
|
@@ -27,11 +28,9 @@ const Heal = protocol.Heal;
|
|
|
27
28
|
|
|
28
29
|
const Gen12 = rng.Gen12;
|
|
29
30
|
|
|
30
|
-
const ActivePokemon = data.ActivePokemon;
|
|
31
31
|
const Effectiveness = data.Effectiveness;
|
|
32
32
|
const Move = data.Move;
|
|
33
33
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const Pokemon = data.Pokemon;
|
|
35
34
|
const Side = data.Side;
|
|
36
35
|
const Species = data.Species;
|
|
37
36
|
const Stats = data.Stats;
|
|
@@ -71,8 +70,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
71
70
|
var p1 = battle.side(.P1);
|
|
72
71
|
var p2 = battle.side(.P2);
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
|
|
74
|
+
const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
|
|
76
75
|
|
|
77
76
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
78
77
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -90,11 +89,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
90
89
|
const p1 = battle.side(.P1);
|
|
91
90
|
const p2 = battle.side(.P2);
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
const p1_slot = findFirstAlive(p1);
|
|
94
93
|
assert(!showdown or p1_slot == 1);
|
|
95
94
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
const p2_slot = findFirstAlive(p2);
|
|
98
97
|
assert(!showdown or p2_slot == 1);
|
|
99
98
|
if (p2_slot == 0) return Result.Win;
|
|
100
99
|
|
|
@@ -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,14 +652,8 @@ fn beforeMove(
|
|
|
635
652
|
volatiles.state +%= battle.last_damage;
|
|
636
653
|
}
|
|
637
654
|
|
|
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);
|
|
655
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
656
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
646
657
|
|
|
647
658
|
if (volatiles.attacks != 0) {
|
|
648
659
|
try log.activate(.{ ident, .Bide });
|
|
@@ -676,39 +687,36 @@ fn beforeMove(
|
|
|
676
687
|
}
|
|
677
688
|
|
|
678
689
|
if (volatiles.Thrashing) {
|
|
679
|
-
|
|
690
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
680
691
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
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;
|
|
687
697
|
|
|
688
|
-
|
|
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)
|
|
689
706
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
707
|
+
options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
|
|
690
708
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
697
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
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
|
+
|
|
700
714
|
return .skip_can;
|
|
701
715
|
}
|
|
702
716
|
|
|
703
717
|
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);
|
|
718
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
719
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
712
720
|
|
|
713
721
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
714
722
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -738,7 +746,6 @@ fn canMove(
|
|
|
738
746
|
const player_ident = battle.active(player);
|
|
739
747
|
const move = Move.get(side.last_selected_move);
|
|
740
748
|
|
|
741
|
-
var skip = skip_pp;
|
|
742
749
|
if (side.active.volatiles.Charging) {
|
|
743
750
|
side.active.volatiles.Charging = false;
|
|
744
751
|
side.active.volatiles.Invulnerable = false;
|
|
@@ -750,7 +757,7 @@ fn canMove(
|
|
|
750
757
|
}
|
|
751
758
|
|
|
752
759
|
side.last_used_move = side.last_selected_move;
|
|
753
|
-
if (!
|
|
760
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
754
761
|
|
|
755
762
|
const target = if (move.target == .Self) player else player.foe();
|
|
756
763
|
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
@@ -802,7 +809,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
802
809
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
803
810
|
}
|
|
804
811
|
|
|
805
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
812
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
806
813
|
var active = &side.active;
|
|
807
814
|
const volatiles = &active.volatiles;
|
|
808
815
|
|
|
@@ -810,8 +817,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
810
817
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
811
818
|
if (showdown and volatiles.Transform) return;
|
|
812
819
|
|
|
820
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
821
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
822
|
+
|
|
813
823
|
assert(mslot > 0 and mslot <= 4);
|
|
814
|
-
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;
|
|
815
825
|
}
|
|
816
826
|
|
|
817
827
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -859,13 +869,13 @@ fn doMove(
|
|
|
859
869
|
break :miss true;
|
|
860
870
|
}
|
|
861
871
|
break :miss (move.target != .Self and
|
|
862
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
872
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
863
873
|
};
|
|
864
874
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
865
875
|
|
|
866
876
|
var late = showdown and move.effect != .Explode;
|
|
867
877
|
const skip = status or immune;
|
|
868
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
878
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
869
879
|
if (showdown and move.effect.isMulti()) {
|
|
870
880
|
try Effects.multiHit(battle, player, move, options);
|
|
871
881
|
hits = side.active.volatiles.attacks;
|
|
@@ -887,8 +897,9 @@ fn doMove(
|
|
|
887
897
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
888
898
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
889
899
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
890
|
-
if (showdown) break :
|
|
891
|
-
} 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);
|
|
892
903
|
return @as(?Result, Result.Error);
|
|
893
904
|
}
|
|
894
905
|
if (battle.last_damage == 0) {
|
|
@@ -902,11 +913,12 @@ fn doMove(
|
|
|
902
913
|
}
|
|
903
914
|
}
|
|
904
915
|
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
916
|
+
var missed: bool = undefined;
|
|
917
|
+
const zero = battle.last_damage == 0;
|
|
918
|
+
if (!showdown and !skip) {
|
|
919
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
920
|
+
miss = missed or zero;
|
|
921
|
+
}
|
|
910
922
|
|
|
911
923
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
912
924
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -933,11 +945,32 @@ fn doMove(
|
|
|
933
945
|
try log.immune(.{ foe_ident, .OHKO });
|
|
934
946
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
935
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
|
+
}
|
|
936
969
|
} else if (mist) {
|
|
937
970
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
938
971
|
try log.fail(.{ foe_ident, .None });
|
|
939
972
|
} else {
|
|
940
|
-
if (showdown
|
|
973
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
941
974
|
try log.lastmiss(.{});
|
|
942
975
|
try log.miss(.{battle.active(player)});
|
|
943
976
|
}
|
|
@@ -952,17 +985,15 @@ fn doMove(
|
|
|
952
985
|
try buildRage(battle, player.foe(), options);
|
|
953
986
|
} else if (showdown and move.effect == .Disable) {
|
|
954
987
|
try buildRage(battle, player.foe(), options);
|
|
955
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
956
|
-
try options.chance.commit(player, .binding);
|
|
957
988
|
}
|
|
958
989
|
return null;
|
|
959
|
-
} else {
|
|
990
|
+
} else if (!showdown) {
|
|
960
991
|
try options.chance.commit(player, .hit);
|
|
961
992
|
}
|
|
962
993
|
|
|
963
994
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
964
995
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
965
|
-
// it up above before damage calculation)
|
|
996
|
+
// it up above before damage calculation)
|
|
966
997
|
if (!showdown and move.effect.isMulti()) {
|
|
967
998
|
try Effects.multiHit(battle, player, move, options);
|
|
968
999
|
hits = side.active.volatiles.attacks;
|
|
@@ -1021,12 +1052,12 @@ fn doMove(
|
|
|
1021
1052
|
}
|
|
1022
1053
|
|
|
1023
1054
|
// 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
|
|
1055
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1025
1056
|
if (nullified) return null;
|
|
1026
1057
|
|
|
1027
1058
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1028
1059
|
// 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
|
|
1060
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1030
1061
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1031
1062
|
|
|
1032
1063
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1041,7 +1072,7 @@ fn doMove(
|
|
|
1041
1072
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1042
1073
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1043
1074
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1044
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1075
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1045
1076
|
if (move.effect == .Twineedle) {
|
|
1046
1077
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1047
1078
|
} else if (move.effect == .Disable) {
|
|
@@ -1060,12 +1091,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1060
1091
|
const side = battle.side(player);
|
|
1061
1092
|
|
|
1062
1093
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1063
|
-
var rate =
|
|
1094
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1064
1095
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1065
1096
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1066
1097
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1067
1098
|
|
|
1068
|
-
|
|
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);
|
|
1069
1102
|
}
|
|
1070
1103
|
|
|
1071
1104
|
fn calcDamage(
|
|
@@ -1074,6 +1107,7 @@ fn calcDamage(
|
|
|
1074
1107
|
target_player: Player,
|
|
1075
1108
|
m: ?Move.Data,
|
|
1076
1109
|
crit: bool,
|
|
1110
|
+
options: anytype,
|
|
1077
1111
|
) bool {
|
|
1078
1112
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1079
1113
|
const cfz = m == null;
|
|
@@ -1085,28 +1119,8 @@ fn calcDamage(
|
|
|
1085
1119
|
|
|
1086
1120
|
const special = move.type.special();
|
|
1087
1121
|
|
|
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
|
|
1122
|
+
var atk = attack(side, crit, special);
|
|
1123
|
+
var def = defense(target, crit, special, cfz);
|
|
1110
1124
|
|
|
1111
1125
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1112
1126
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1117,9 +1131,9 @@ fn calcDamage(
|
|
|
1117
1131
|
def = @max((def / 4) & 255, if (showdown) 1 else 0);
|
|
1118
1132
|
}
|
|
1119
1133
|
|
|
1120
|
-
const lvl =
|
|
1134
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1121
1135
|
|
|
1122
|
-
def =
|
|
1136
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1123
1137
|
|
|
1124
1138
|
if (def == 0) return false;
|
|
1125
1139
|
|
|
@@ -1131,11 +1145,41 @@ fn calcDamage(
|
|
|
1131
1145
|
d = @min(997, d);
|
|
1132
1146
|
d += 2;
|
|
1133
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
|
+
|
|
1134
1158
|
battle.last_damage = @intCast(d);
|
|
1135
1159
|
|
|
1136
1160
|
return true;
|
|
1137
1161
|
}
|
|
1138
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
|
+
|
|
1139
1183
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1140
1184
|
const side = battle.side(player);
|
|
1141
1185
|
const foe = battle.foe(player);
|
|
@@ -1180,7 +1224,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1180
1224
|
const foe = battle.foe(player);
|
|
1181
1225
|
|
|
1182
1226
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1183
|
-
try options.chance.commit(player, .hit);
|
|
1184
1227
|
|
|
1185
1228
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1186
1229
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1241,9 +1284,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1241
1284
|
|
|
1242
1285
|
battle.last_damage *|= 2;
|
|
1243
1286
|
|
|
1244
|
-
// Pokémon Showdown calls
|
|
1287
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1245
1288
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1246
|
-
try options.chance.commit(player, .hit);
|
|
1247
1289
|
|
|
1248
1290
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1249
1291
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1272,7 +1314,7 @@ fn applyDamage(
|
|
|
1272
1314
|
subbed.active.volatiles.Substitute = false;
|
|
1273
1315
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1274
1316
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1275
|
-
options.calc.capped(
|
|
1317
|
+
options.calc.capped(sub_player);
|
|
1276
1318
|
return true;
|
|
1277
1319
|
} else {
|
|
1278
1320
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1306,7 +1348,7 @@ fn mirrorMove(
|
|
|
1306
1348
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1307
1349
|
return null;
|
|
1308
1350
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1309
|
-
incrementPP(side, mslot);
|
|
1351
|
+
incrementPP(side, player, mslot, options);
|
|
1310
1352
|
}
|
|
1311
1353
|
|
|
1312
1354
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1327,7 +1369,7 @@ fn metronome(
|
|
|
1327
1369
|
var side = battle.side(player);
|
|
1328
1370
|
|
|
1329
1371
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1330
|
-
incrementPP(side, mslot);
|
|
1372
|
+
incrementPP(side, player, mslot, options);
|
|
1331
1373
|
|
|
1332
1374
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1333
1375
|
return null;
|
|
@@ -1339,7 +1381,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1339
1381
|
var immune = false;
|
|
1340
1382
|
var mist = false;
|
|
1341
1383
|
|
|
1342
|
-
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
|
+
}
|
|
1343
1388
|
|
|
1344
1389
|
assert(!immune);
|
|
1345
1390
|
if (mist) {
|
|
@@ -1348,11 +1393,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1348
1393
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1349
1394
|
try options.log.fail(.{ foe_ident, .None });
|
|
1350
1395
|
} else {
|
|
1351
|
-
try options.chance.commit(player, .miss);
|
|
1352
1396
|
try options.log.lastmiss(.{});
|
|
1353
1397
|
try options.log.miss(.{battle.active(player)});
|
|
1354
1398
|
}
|
|
1355
1399
|
|
|
1400
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1356
1401
|
return false;
|
|
1357
1402
|
}
|
|
1358
1403
|
|
|
@@ -1363,7 +1408,7 @@ fn moveHit(
|
|
|
1363
1408
|
immune: *bool,
|
|
1364
1409
|
mist: *bool,
|
|
1365
1410
|
options: anytype,
|
|
1366
|
-
) bool {
|
|
1411
|
+
) !bool {
|
|
1367
1412
|
var side = battle.side(player);
|
|
1368
1413
|
const foe = battle.foe(player);
|
|
1369
1414
|
|
|
@@ -1402,7 +1447,7 @@ fn moveHit(
|
|
|
1402
1447
|
const overwritten = overwrite and state > 0;
|
|
1403
1448
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1404
1449
|
|
|
1405
|
-
var accuracy = if (overwritten) state else
|
|
1450
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1406
1451
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1407
1452
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1408
1453
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1414,7 +1459,7 @@ fn moveHit(
|
|
|
1414
1459
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1415
1460
|
|
|
1416
1461
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1417
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1462
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1418
1463
|
};
|
|
1419
1464
|
|
|
1420
1465
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,13 +1470,17 @@ fn moveHit(
|
|
|
1425
1470
|
|
|
1426
1471
|
if (!miss) return true;
|
|
1427
1472
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1473
|
+
|
|
1428
1474
|
side.active.volatiles.Binding = false;
|
|
1475
|
+
options.chance.observe(.binding, player, .None);
|
|
1476
|
+
|
|
1429
1477
|
return false;
|
|
1430
1478
|
}
|
|
1431
1479
|
|
|
1432
1480
|
fn checkFaint(
|
|
1433
1481
|
battle: anytype,
|
|
1434
1482
|
player: Player,
|
|
1483
|
+
cap: bool,
|
|
1435
1484
|
options: anytype,
|
|
1436
1485
|
) @TypeOf(options.log).Error!?Result {
|
|
1437
1486
|
const side = battle.side(player);
|
|
@@ -1444,8 +1493,8 @@ fn checkFaint(
|
|
|
1444
1493
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1445
1494
|
const more = player_out or foe_out;
|
|
1446
1495
|
|
|
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;
|
|
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;
|
|
1449
1498
|
|
|
1450
1499
|
assert(!side.active.volatiles.MultiHit);
|
|
1451
1500
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1466,7 +1515,7 @@ fn checkFaint(
|
|
|
1466
1515
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1467
1516
|
}
|
|
1468
1517
|
|
|
1469
|
-
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 {
|
|
1470
1519
|
var side = battle.side(player);
|
|
1471
1520
|
var foe = battle.foe(player);
|
|
1472
1521
|
assert(side.stored().hp == 0);
|
|
@@ -1500,7 +1549,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1500
1549
|
}
|
|
1501
1550
|
|
|
1502
1551
|
try options.log.faint(.{ battle.active(player), done });
|
|
1503
|
-
|
|
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);
|
|
1504
1555
|
return null;
|
|
1505
1556
|
}
|
|
1506
1557
|
|
|
@@ -1660,17 +1711,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1660
1711
|
}
|
|
1661
1712
|
}
|
|
1662
1713
|
|
|
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
1714
|
fn onBegin(
|
|
1675
1715
|
battle: anytype,
|
|
1676
1716
|
player: Player,
|
|
@@ -1679,7 +1719,6 @@ fn onBegin(
|
|
|
1679
1719
|
residual: *bool,
|
|
1680
1720
|
options: anytype,
|
|
1681
1721
|
) !void {
|
|
1682
|
-
assert(move.effect.onBegin());
|
|
1683
1722
|
return switch (move.effect) {
|
|
1684
1723
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1685
1724
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1702,7 +1741,6 @@ fn onBegin(
|
|
|
1702
1741
|
}
|
|
1703
1742
|
|
|
1704
1743
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1705
|
-
assert(move.effect.onEnd());
|
|
1706
1744
|
return switch (move.effect) {
|
|
1707
1745
|
.Bide => Effects.bide(battle, player, options),
|
|
1708
1746
|
// zig fmt: off
|
|
@@ -1724,7 +1762,6 @@ fn alwaysHappens(
|
|
|
1724
1762
|
residual: *bool,
|
|
1725
1763
|
options: anytype,
|
|
1726
1764
|
) !void {
|
|
1727
|
-
assert(move.effect.alwaysHappens());
|
|
1728
1765
|
return switch (move.effect) {
|
|
1729
1766
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1730
1767
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1760,8 +1797,10 @@ pub const Effects = struct {
|
|
|
1760
1797
|
|
|
1761
1798
|
side.active.volatiles.Bide = true;
|
|
1762
1799
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1800
|
+
|
|
1763
1801
|
side.active.volatiles.state = 0;
|
|
1764
1802
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1803
|
+
options.chance.observe(.attacking, player, .started);
|
|
1765
1804
|
|
|
1766
1805
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1767
1806
|
}
|
|
@@ -1823,12 +1862,13 @@ pub const Effects = struct {
|
|
|
1823
1862
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1863
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1825
1864
|
}
|
|
1826
|
-
try options.chance.commit(player, .hit);
|
|
1827
1865
|
}
|
|
1828
1866
|
|
|
1829
1867
|
if (foe.active.volatiles.Confusion) return;
|
|
1830
1868
|
foe.active.volatiles.Confusion = true;
|
|
1831
|
-
|
|
1869
|
+
|
|
1870
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1871
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1832
1872
|
|
|
1833
1873
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1834
1874
|
}
|
|
@@ -1856,10 +1896,7 @@ pub const Effects = struct {
|
|
|
1856
1896
|
const foe_ident = battle.active(player.foe());
|
|
1857
1897
|
|
|
1858
1898
|
// 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
|
-
}
|
|
1899
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1863
1900
|
|
|
1864
1901
|
if (volatiles.disable_move != 0) {
|
|
1865
1902
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1889,6 +1926,7 @@ pub const Effects = struct {
|
|
|
1889
1926
|
volatiles.disable_move =
|
|
1890
1927
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1891
1928
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1929
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1892
1930
|
|
|
1893
1931
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1894
1932
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1972,7 +2010,7 @@ pub const Effects = struct {
|
|
|
1972
2010
|
var side = battle.side(player);
|
|
1973
2011
|
var foe = battle.foe(player);
|
|
1974
2012
|
|
|
1975
|
-
|
|
2013
|
+
const side_stored = side.stored();
|
|
1976
2014
|
var foe_stored = foe.stored();
|
|
1977
2015
|
|
|
1978
2016
|
const player_ident = battle.active(player);
|
|
@@ -1995,22 +2033,24 @@ pub const Effects = struct {
|
|
|
1995
2033
|
if (p != player and Status.any(s.stored().status)) {
|
|
1996
2034
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1997
2035
|
s.stored().status = 0;
|
|
2036
|
+
options.chance.observe(.sleep, p, .None);
|
|
1998
2037
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1999
2038
|
s.stored().status = Status.init(.PSN);
|
|
2000
2039
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2001
2040
|
}
|
|
2002
|
-
try clearVolatiles(battle, p,
|
|
2041
|
+
try clearVolatiles(battle, p, options);
|
|
2003
2042
|
}
|
|
2004
2043
|
} else {
|
|
2005
2044
|
if (Status.any(foe_stored.status)) {
|
|
2006
2045
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2007
2046
|
foe.last_selected_move = .SKIP_TURN;
|
|
2047
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2008
2048
|
}
|
|
2009
2049
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2010
2050
|
foe_stored.status = 0;
|
|
2011
2051
|
}
|
|
2012
|
-
try clearVolatiles(battle, player,
|
|
2013
|
-
try clearVolatiles(battle, player.foe(),
|
|
2052
|
+
try clearVolatiles(battle, player, options);
|
|
2053
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2014
2054
|
}
|
|
2015
2055
|
}
|
|
2016
2056
|
|
|
@@ -2061,7 +2101,6 @@ pub const Effects = struct {
|
|
|
2061
2101
|
}
|
|
2062
2102
|
}
|
|
2063
2103
|
|
|
2064
|
-
try options.chance.commit(player, .hit);
|
|
2065
2104
|
foe.active.volatiles.LeechSeed = true;
|
|
2066
2105
|
|
|
2067
2106
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2102,12 +2141,10 @@ pub const Effects = struct {
|
|
|
2102
2141
|
if (!has_mimic) {
|
|
2103
2142
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2104
2143
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2105
|
-
try options.chance.commit(player, .hit);
|
|
2106
2144
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2107
2145
|
}
|
|
2108
2146
|
}
|
|
2109
2147
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2110
|
-
try options.chance.commit(player, .hit);
|
|
2111
2148
|
|
|
2112
2149
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2113
2150
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2161,7 +2198,6 @@ pub const Effects = struct {
|
|
|
2161
2198
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2162
2199
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2163
2200
|
}
|
|
2164
|
-
try options.chance.commit(player, .hit);
|
|
2165
2201
|
|
|
2166
2202
|
foe_stored.status = Status.init(.PAR);
|
|
2167
2203
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2229,7 +2265,6 @@ pub const Effects = struct {
|
|
|
2229
2265
|
|
|
2230
2266
|
if (move.effect == .Poison) {
|
|
2231
2267
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2232
|
-
try options.chance.commit(player, .hit);
|
|
2233
2268
|
} else {
|
|
2234
2269
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2235
2270
|
return;
|
|
@@ -2298,9 +2333,8 @@ pub const Effects = struct {
|
|
|
2298
2333
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2299
2334
|
});
|
|
2300
2335
|
}
|
|
2301
|
-
// If
|
|
2336
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2302
2337
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2303
|
-
try options.chance.commit(player, .hit);
|
|
2304
2338
|
}
|
|
2305
2339
|
|
|
2306
2340
|
// Sleep Clause Mod
|
|
@@ -2312,6 +2346,8 @@ pub const Effects = struct {
|
|
|
2312
2346
|
foe.active.volatiles.Recharging = false;
|
|
2313
2347
|
|
|
2314
2348
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2349
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2350
|
+
|
|
2315
2351
|
const last = battle.side(player).last_selected_move;
|
|
2316
2352
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2317
2353
|
}
|
|
@@ -2353,9 +2389,7 @@ pub const Effects = struct {
|
|
|
2353
2389
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2390
|
if (showdown) {
|
|
2355
2391
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2392
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2393
|
battle.last_damage = 0;
|
|
2360
2394
|
} else {
|
|
2361
2395
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2370,6 +2404,8 @@ pub const Effects = struct {
|
|
|
2370
2404
|
|
|
2371
2405
|
volatiles.Thrashing = true;
|
|
2372
2406
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2407
|
+
|
|
2408
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2409
|
}
|
|
2374
2410
|
|
|
2375
2411
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2415,6 +2451,7 @@ pub const Effects = struct {
|
|
|
2415
2451
|
|
|
2416
2452
|
side.active.volatiles.attacks =
|
|
2417
2453
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2454
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2455
|
}
|
|
2419
2456
|
|
|
2420
2457
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2441,7 +2478,7 @@ pub const Effects = struct {
|
|
|
2441
2478
|
}
|
|
2442
2479
|
return log.fail(.{ ident, .None });
|
|
2443
2480
|
}
|
|
2444
|
-
|
|
2481
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2482
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2483
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
2484
|
try log.boost(.{ ident, reason, n });
|
|
@@ -2461,7 +2498,7 @@ pub const Effects = struct {
|
|
|
2461
2498
|
}
|
|
2462
2499
|
return log.fail(.{ ident, .None });
|
|
2463
2500
|
}
|
|
2464
|
-
|
|
2501
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2502
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2503
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
2504
|
try log.boost(.{ ident, .Defense, n });
|
|
@@ -2478,7 +2515,7 @@ pub const Effects = struct {
|
|
|
2478
2515
|
}
|
|
2479
2516
|
return log.fail(.{ ident, .None });
|
|
2480
2517
|
}
|
|
2481
|
-
|
|
2518
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2519
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2520
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
2521
|
try log.boost(.{ ident, .Speed, 2 });
|
|
@@ -2498,7 +2535,7 @@ pub const Effects = struct {
|
|
|
2498
2535
|
}
|
|
2499
2536
|
return log.fail(.{ ident, .None });
|
|
2500
2537
|
}
|
|
2501
|
-
|
|
2538
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2539
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2540
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
2541
|
try log.boost(.{ ident, .SpecialAttack, n });
|
|
@@ -2531,7 +2568,6 @@ pub const Effects = struct {
|
|
|
2531
2568
|
} else {
|
|
2532
2569
|
// checkHit already checks for Invulnerable
|
|
2533
2570
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2571
|
}
|
|
2536
2572
|
|
|
2537
2573
|
var stats = &foe.active.stats;
|
|
@@ -2551,7 +2587,7 @@ pub const Effects = struct {
|
|
|
2551
2587
|
}
|
|
2552
2588
|
return log.fail(.{ foe_ident, .None });
|
|
2553
2589
|
}
|
|
2554
|
-
|
|
2590
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2591
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2592
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
2593
|
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
@@ -2569,7 +2605,7 @@ pub const Effects = struct {
|
|
|
2569
2605
|
}
|
|
2570
2606
|
return log.fail(.{ foe_ident, .None });
|
|
2571
2607
|
}
|
|
2572
|
-
|
|
2608
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2609
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2610
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
2611
|
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
@@ -2586,7 +2622,7 @@ pub const Effects = struct {
|
|
|
2586
2622
|
}
|
|
2587
2623
|
return log.fail(.{ foe_ident, .None });
|
|
2588
2624
|
}
|
|
2589
|
-
|
|
2625
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2626
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2627
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
2628
|
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
@@ -2606,7 +2642,7 @@ pub const Effects = struct {
|
|
|
2606
2642
|
}
|
|
2607
2643
|
return log.fail(.{ foe_ident, .None });
|
|
2608
2644
|
}
|
|
2609
|
-
|
|
2645
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2646
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2647
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
2648
|
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
@@ -2645,22 +2681,22 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2681
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2682
|
}
|
|
2647
2683
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2684
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2685
|
var log = options.log;
|
|
2650
2686
|
var side = battle.side(who);
|
|
2651
2687
|
var volatiles = &side.active.volatiles;
|
|
2652
2688
|
const ident = battle.active(who);
|
|
2653
2689
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2690
|
if (volatiles.disable_move != 0) {
|
|
2657
2691
|
volatiles.disable_move = 0;
|
|
2658
2692
|
volatiles.disable_duration = 0;
|
|
2693
|
+
options.chance.observe(.disable, who, .None);
|
|
2659
2694
|
try log.end(.{ ident, .DisableSilent });
|
|
2660
2695
|
}
|
|
2661
2696
|
if (volatiles.Confusion) {
|
|
2662
2697
|
// volatiles.confusion is left unchanged
|
|
2663
2698
|
volatiles.Confusion = false;
|
|
2699
|
+
options.chance.observe(.confusion, who, .None);
|
|
2664
2700
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2701
|
}
|
|
2666
2702
|
if (volatiles.Mist) {
|
|
@@ -2695,10 +2731,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2695
2731
|
}
|
|
2696
2732
|
}
|
|
2697
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
|
+
|
|
2698
2747
|
pub const Rolls = struct {
|
|
2699
2748
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2749
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2750
|
player == .P1
|
|
2751
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2752
|
+
battle.rng.speedTie()
|
|
2702
2753
|
else if (showdown)
|
|
2703
2754
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2755
|
else
|
|
@@ -2708,21 +2759,31 @@ pub const Rolls = struct {
|
|
|
2708
2759
|
return p1;
|
|
2709
2760
|
}
|
|
2710
2761
|
|
|
2711
|
-
fn criticalHit(
|
|
2762
|
+
fn criticalHit(
|
|
2763
|
+
battle: anytype,
|
|
2764
|
+
player: Player,
|
|
2765
|
+
rate: u8,
|
|
2766
|
+
spurious: bool,
|
|
2767
|
+
options: anytype,
|
|
2768
|
+
) !bool {
|
|
2712
2769
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2770
|
val == .true
|
|
2771
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2772
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2773
|
else if (showdown)
|
|
2715
2774
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2775
|
else
|
|
2717
2776
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2777
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2778
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2779
|
return crit;
|
|
2721
2780
|
}
|
|
2722
2781
|
|
|
2723
2782
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2783
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2784
|
val
|
|
2785
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2786
|
+
battle.rng.damage(player)
|
|
2726
2787
|
else roll: {
|
|
2727
2788
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2789
|
while (true) {
|
|
@@ -2736,21 +2797,27 @@ pub const Rolls = struct {
|
|
|
2736
2797
|
return roll;
|
|
2737
2798
|
}
|
|
2738
2799
|
|
|
2739
|
-
|
|
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;
|
|
2740
2803
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2804
|
val == .true
|
|
2805
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2806
|
+
battle.rng.hit(player, acc)
|
|
2742
2807
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2808
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2809
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2810
|
+
battle.rng.next() < acc;
|
|
2746
2811
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2812
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2813
|
return ok;
|
|
2749
2814
|
}
|
|
2750
2815
|
|
|
2751
2816
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2817
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2818
|
val == .true
|
|
2819
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2820
|
+
battle.rng.confused(player)
|
|
2754
2821
|
else if (showdown)
|
|
2755
2822
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2823
|
else
|
|
@@ -2763,6 +2830,8 @@ pub const Rolls = struct {
|
|
|
2763
2830
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2831
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2832
|
val == .true
|
|
2833
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2834
|
+
battle.rng.paralyzed(player)
|
|
2766
2835
|
else if (showdown)
|
|
2767
2836
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2837
|
else
|
|
@@ -2775,6 +2844,8 @@ pub const Rolls = struct {
|
|
|
2775
2844
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2845
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2846
|
val == .true
|
|
2847
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2848
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2849
|
else if (showdown)
|
|
2779
2850
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2851
|
else
|
|
@@ -2789,6 +2860,8 @@ pub const Rolls = struct {
|
|
|
2789
2860
|
|
|
2790
2861
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2862
|
val == .true
|
|
2863
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2864
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2865
|
else if (showdown)
|
|
2793
2866
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2867
|
else
|
|
@@ -2803,6 +2876,8 @@ pub const Rolls = struct {
|
|
|
2803
2876
|
|
|
2804
2877
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2878
|
val == .true
|
|
2879
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2880
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2881
|
else if (showdown)
|
|
2807
2882
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2883
|
else
|
|
@@ -2815,6 +2890,8 @@ pub const Rolls = struct {
|
|
|
2815
2890
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2891
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2892
|
val == .true
|
|
2893
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2894
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2895
|
else if (showdown)
|
|
2819
2896
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2897
|
else
|
|
@@ -2827,6 +2904,8 @@ pub const Rolls = struct {
|
|
|
2827
2904
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2905
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2906
|
val
|
|
2907
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2908
|
+
battle.rng.metronome(player)
|
|
2830
2909
|
else if (showdown)
|
|
2831
2910
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2911
|
else move: {
|
|
@@ -2845,6 +2924,8 @@ pub const Rolls = struct {
|
|
|
2845
2924
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2925
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2926
|
val - 1
|
|
2927
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2928
|
+
battle.rng.psywave(player)
|
|
2848
2929
|
else if (showdown)
|
|
2849
2930
|
battle.rng.range(u8, 0, max)
|
|
2850
2931
|
else power: {
|
|
@@ -2862,6 +2943,8 @@ pub const Rolls = struct {
|
|
|
2862
2943
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
2944
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
2945
|
@intCast(val)
|
|
2946
|
+
else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2947
|
+
battle.rng.sleepDuration(player)
|
|
2865
2948
|
else if (showdown)
|
|
2866
2949
|
battle.rng.range(u3, 1, 8)
|
|
2867
2950
|
else duration: {
|
|
@@ -2872,46 +2955,52 @@ pub const Rolls = struct {
|
|
|
2872
2955
|
};
|
|
2873
2956
|
|
|
2874
2957
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2958
|
+
options.chance.duration(player, duration);
|
|
2876
2959
|
return duration;
|
|
2877
2960
|
}
|
|
2878
2961
|
|
|
2879
2962
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
2963
|
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
2964
|
val
|
|
2965
|
+
else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2966
|
+
battle.rng.disableDuration(player)
|
|
2882
2967
|
else if (showdown)
|
|
2883
2968
|
battle.rng.range(u4, 1, 9)
|
|
2884
2969
|
else
|
|
2885
2970
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2971
|
|
|
2887
2972
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
2973
|
+
options.chance.duration(player, duration);
|
|
2889
2974
|
return duration;
|
|
2890
2975
|
}
|
|
2891
2976
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2977
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2893
2978
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
2979
|
@intCast(val)
|
|
2980
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2981
|
+
battle.rng.confusionDuration(player)
|
|
2895
2982
|
else if (showdown)
|
|
2896
2983
|
battle.rng.range(u3, 2, 6)
|
|
2897
2984
|
else
|
|
2898
2985
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
2986
|
|
|
2900
2987
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
2988
|
+
options.chance.duration(player, duration);
|
|
2902
2989
|
return duration;
|
|
2903
2990
|
}
|
|
2904
2991
|
|
|
2905
2992
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
2993
|
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
2994
|
@intCast(val)
|
|
2995
|
+
else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2996
|
+
battle.rng.attackingDuration(player)
|
|
2908
2997
|
else if (showdown)
|
|
2909
2998
|
battle.rng.range(u3, 2, 4)
|
|
2910
2999
|
else
|
|
2911
3000
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3001
|
|
|
2913
3002
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3003
|
+
options.chance.duration(player, duration);
|
|
2915
3004
|
return duration;
|
|
2916
3005
|
}
|
|
2917
3006
|
|
|
@@ -2926,6 +3015,8 @@ pub const Rolls = struct {
|
|
|
2926
3015
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
3016
|
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
3017
|
@intCast(val)
|
|
3018
|
+
else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3019
|
+
battle.rng.distribution(player)
|
|
2929
3020
|
else if (showdown)
|
|
2930
3021
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3022
|
else n: {
|
|
@@ -2935,7 +3026,7 @@ pub const Rolls = struct {
|
|
|
2935
3026
|
|
|
2936
3027
|
assert(n >= 2 and n <= 5);
|
|
2937
3028
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3029
|
+
options.chance.duration(player, n);
|
|
2939
3030
|
} else {
|
|
2940
3031
|
try options.chance.multiHit(player, n);
|
|
2941
3032
|
}
|
|
@@ -2958,7 +3049,9 @@ pub const Rolls = struct {
|
|
|
2958
3049
|
else
|
|
2959
3050
|
null;
|
|
2960
3051
|
|
|
2961
|
-
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: {
|
|
2962
3055
|
if (showdown) {
|
|
2963
3056
|
if (check_pp == 0) {
|
|
2964
3057
|
var i: usize = moves.len;
|
|
@@ -3038,11 +3131,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3131
|
const side = battle.side(player);
|
|
3039
3132
|
const foe = battle.foe(player);
|
|
3040
3133
|
|
|
3041
|
-
|
|
3134
|
+
const active = &side.active;
|
|
3042
3135
|
const stored = side.stored();
|
|
3043
3136
|
|
|
3044
3137
|
// 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
|
|
3138
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3139
|
//
|
|
3047
3140
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3141
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,7 +3143,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3143
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3144
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3145
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3146
|
+
// switch at all
|
|
3054
3147
|
if (isForced(active)) {
|
|
3055
3148
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3149
|
n += 1;
|
|
@@ -3069,7 +3162,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3162
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3163
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3164
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3165
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3166
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3167
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3168
|
{
|
|
@@ -3082,7 +3175,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3175
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3176
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3177
|
// 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
|
|
3178
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3179
|
if (limited) {
|
|
3087
3180
|
assert(showdown);
|
|
3088
3181
|
assert(side.last_selected_move != .None);
|