@pkmn/engine 0.1.0-dev.b01cb48c → 0.1.0-dev.b3197aa3
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 -1
- package/README.md +54 -6
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -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 +110 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -3
- package/build/pkg/addon.js +34 -65
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -90
- package/build/pkg/index.js +23 -70
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +52 -76
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +331 -85
- package/package.json +56 -136
- package/src/bin/install-pkmn-engine +10 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- package/src/lib/bindings/node.zig +117 -0
- package/src/lib/bindings/wasm.zig +59 -0
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -6
- package/src/lib/common/napi.zig +4 -0
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +4 -0
- package/src/lib/common/protocol.zig +44 -43
- package/src/lib/common/rational.zig +317 -304
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +65 -1
- package/src/lib/gen1/calc.zig +266 -135
- package/src/lib/gen1/chance.zig +469 -110
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +2 -3
- package/src/lib/gen1/data.zig +35 -38
- package/src/lib/gen1/helpers.zig +17 -27
- package/src/lib/gen1/mechanics.zig +213 -160
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +87 -67
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +7 -5
- package/src/lib/gen2/data.zig +28 -30
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +9 -18
- package/src/lib/gen2/mechanics.zig +82 -82
- package/src/lib/node.zig +4 -121
- package/src/lib/pkmn.zig +14 -7
- package/src/lib/wasm.zig +13 -9
- package/src/tools/display/dom.ts +93 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -405
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,39 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
8
8
|
const protocol = @import("../common/protocol.zig");
|
|
9
9
|
const rng = @import("../common/rng.zig");
|
|
10
|
-
|
|
11
|
-
const data = @import("data.zig");
|
|
12
|
-
const chance = @import("chance.zig");
|
|
10
|
+
const std = @import("std");
|
|
13
11
|
|
|
14
12
|
const assert = std.debug.assert;
|
|
15
|
-
|
|
16
|
-
const expectEqual = std.testing.expectEqual;
|
|
17
|
-
|
|
18
|
-
const showdown = pkmn.options.showdown;
|
|
19
|
-
|
|
20
|
-
const Choice = common.Choice;
|
|
21
|
-
const ID = common.ID;
|
|
22
|
-
const Player = common.Player;
|
|
23
|
-
const Result = common.Result;
|
|
24
|
-
|
|
25
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
26
15
|
const Damage = protocol.Damage;
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
27
18
|
const Fail = protocol.Fail;
|
|
28
|
-
const Heal = protocol.Heal;
|
|
29
|
-
|
|
30
19
|
const Gen12 = rng.Gen12;
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
34
22
|
const Move = data.Move;
|
|
35
23
|
const MoveSlot = data.MoveSlot;
|
|
36
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
37
27
|
const Side = data.Side;
|
|
38
28
|
const Species = data.Species;
|
|
39
29
|
const Stats = data.Stats;
|
|
@@ -176,7 +166,10 @@ fn selectMove(
|
|
|
176
166
|
if (foe_choice.type == .Switch) {
|
|
177
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
178
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
179
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
180
173
|
}
|
|
181
174
|
}
|
|
182
175
|
return null;
|
|
@@ -255,6 +248,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
255
248
|
active.moves = incoming.moves;
|
|
256
249
|
|
|
257
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
258
252
|
|
|
259
253
|
foe.active.volatiles.Binding = false;
|
|
260
254
|
|
|
@@ -354,7 +348,7 @@ fn doTurn(
|
|
|
354
348
|
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
355
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
356
350
|
|
|
357
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
358
352
|
|
|
359
353
|
residual = true;
|
|
360
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -426,7 +420,7 @@ fn executeMove(
|
|
|
426
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
427
421
|
const stored = side.stored();
|
|
428
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
429
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
430
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
431
425
|
side.last_selected_move == .HyperBeam or
|
|
432
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
@@ -459,6 +453,25 @@ fn executeMove(
|
|
|
459
453
|
.err => return @as(?Result, Result.Error),
|
|
460
454
|
}
|
|
461
455
|
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
462
475
|
const can = skip_can or
|
|
463
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
464
477
|
if (!can) return null;
|
|
@@ -485,6 +498,7 @@ fn beforeMove(
|
|
|
485
498
|
|
|
486
499
|
if (Status.is(stored.status, .SLP)) {
|
|
487
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
488
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
489
503
|
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
490
504
|
.started, .ended => stored.status = 0,
|
|
@@ -497,7 +511,9 @@ fn beforeMove(
|
|
|
497
511
|
}
|
|
498
512
|
|
|
499
513
|
const duration = Status.duration(stored.status);
|
|
500
|
-
options.chance.
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
501
517
|
|
|
502
518
|
if (duration == 0) {
|
|
503
519
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -537,7 +553,10 @@ fn beforeMove(
|
|
|
537
553
|
if (volatiles.disable_duration > 0) {
|
|
538
554
|
volatiles.disable_duration =
|
|
539
555
|
decrement(.disable, player, options, volatiles.disable_duration);
|
|
540
|
-
options.chance.
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
541
560
|
|
|
542
561
|
if (volatiles.disable_duration == 0) {
|
|
543
562
|
volatiles.disable_move = 0;
|
|
@@ -550,25 +569,24 @@ fn beforeMove(
|
|
|
550
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
551
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
552
571
|
if (side.last_selected_move == .None) {
|
|
553
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
554
573
|
return .err;
|
|
555
574
|
}
|
|
556
575
|
|
|
557
576
|
if (volatiles.Confusion) {
|
|
558
577
|
assert(volatiles.confusion > 0);
|
|
559
578
|
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
560
|
-
.started => volatiles.confusion
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
561
580
|
.continuing => if (volatiles.confusion > 1) {
|
|
562
581
|
volatiles.confusion -= 1;
|
|
563
582
|
},
|
|
564
|
-
.ended => volatiles.confusion = 0,
|
|
565
583
|
.overwritten => {},
|
|
566
584
|
else => unreachable,
|
|
567
585
|
} else {
|
|
568
586
|
volatiles.confusion -= 1;
|
|
569
587
|
}
|
|
570
588
|
|
|
571
|
-
options.chance.
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
572
590
|
|
|
573
591
|
if (volatiles.confusion == 0) {
|
|
574
592
|
volatiles.Confusion = false;
|
|
@@ -587,8 +605,7 @@ fn beforeMove(
|
|
|
587
605
|
volatiles.Binding = false;
|
|
588
606
|
volatiles.Invulnerable = false;
|
|
589
607
|
|
|
590
|
-
options.chance.observe(.
|
|
591
|
-
options.chance.observe(.thrashing, player, .None);
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
592
609
|
options.chance.observe(.binding, player, .None);
|
|
593
610
|
{
|
|
594
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
@@ -597,19 +614,14 @@ fn beforeMove(
|
|
|
597
614
|
const def = foe.active.stats.def;
|
|
598
615
|
foe.active.stats.def = active.stats.def;
|
|
599
616
|
defer foe.active.stats.def = def;
|
|
600
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
601
620
|
}
|
|
602
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
603
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
604
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
605
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
606
|
-
player
|
|
607
|
-
else
|
|
608
|
-
player.foe();
|
|
609
621
|
|
|
610
622
|
const uncapped = battle.last_damage;
|
|
611
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
612
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
613
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
614
626
|
if (showdown) battle.last_damage = uncapped;
|
|
615
627
|
|
|
@@ -627,8 +639,7 @@ fn beforeMove(
|
|
|
627
639
|
volatiles.Charging = false;
|
|
628
640
|
volatiles.Binding = false;
|
|
629
641
|
|
|
630
|
-
options.chance.observe(.
|
|
631
|
-
options.chance.observe(.thrashing, player, .None);
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
632
643
|
options.chance.observe(.binding, player, .None);
|
|
633
644
|
|
|
634
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
@@ -648,8 +659,8 @@ fn beforeMove(
|
|
|
648
659
|
volatiles.state +%= battle.last_damage;
|
|
649
660
|
}
|
|
650
661
|
|
|
651
|
-
volatiles.attacks = decrement(.
|
|
652
|
-
options.chance.
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
653
664
|
|
|
654
665
|
if (volatiles.attacks != 0) {
|
|
655
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -685,8 +696,8 @@ fn beforeMove(
|
|
|
685
696
|
if (volatiles.Thrashing) {
|
|
686
697
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
687
698
|
|
|
688
|
-
volatiles.attacks = decrement(.
|
|
689
|
-
options.chance.
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
690
701
|
|
|
691
702
|
if (volatiles.attacks == 0) {
|
|
692
703
|
const overwritten = volatiles.Confusion;
|
|
@@ -700,11 +711,11 @@ fn beforeMove(
|
|
|
700
711
|
// can be expected to recognize this pattern and pass the information only to the
|
|
701
712
|
// correct player)
|
|
702
713
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
703
|
-
options.chance.observe(
|
|
704
|
-
.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
);
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
708
719
|
}
|
|
709
720
|
|
|
710
721
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -716,7 +727,7 @@ fn beforeMove(
|
|
|
716
727
|
|
|
717
728
|
if (volatiles.Binding) {
|
|
718
729
|
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
719
|
-
options.chance.
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
720
731
|
|
|
721
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
722
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -770,9 +781,14 @@ fn canMove(
|
|
|
770
781
|
|
|
771
782
|
if (move.effect == .Thrashing) {
|
|
772
783
|
Effects.thrashing(battle, player, options);
|
|
773
|
-
} else if (
|
|
774
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
775
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
776
792
|
}
|
|
777
793
|
|
|
778
794
|
return true;
|
|
@@ -809,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
809
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
810
826
|
}
|
|
811
827
|
|
|
812
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
813
829
|
var active = &side.active;
|
|
814
830
|
const volatiles = &active.volatiles;
|
|
815
831
|
|
|
@@ -817,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
817
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
818
834
|
if (showdown and volatiles.Transform) return;
|
|
819
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
820
839
|
assert(mslot > 0 and mslot <= 4);
|
|
821
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
822
841
|
}
|
|
823
842
|
|
|
824
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -872,7 +891,7 @@ fn doMove(
|
|
|
872
891
|
|
|
873
892
|
var late = showdown and move.effect != .Explode;
|
|
874
893
|
const skip = status or immune;
|
|
875
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
876
895
|
if (showdown and move.effect.isMulti()) {
|
|
877
896
|
try Effects.multiHit(battle, player, move, options);
|
|
878
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -894,8 +913,9 @@ fn doMove(
|
|
|
894
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
895
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
896
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
897
|
-
if (showdown) break :
|
|
898
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
899
919
|
return @as(?Result, Result.Error);
|
|
900
920
|
}
|
|
901
921
|
if (battle.last_damage == 0) {
|
|
@@ -909,11 +929,12 @@ fn doMove(
|
|
|
909
929
|
}
|
|
910
930
|
}
|
|
911
931
|
|
|
932
|
+
var missed: bool = undefined;
|
|
912
933
|
const zero = battle.last_damage == 0;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
917
938
|
|
|
918
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
919
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -940,24 +961,32 @@ fn doMove(
|
|
|
940
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
941
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
942
963
|
try log.immune(.{ foe_ident, .None });
|
|
943
|
-
if (!showdown
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
955
984
|
}
|
|
956
985
|
} else if (mist) {
|
|
957
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
958
987
|
try log.fail(.{ foe_ident, .None });
|
|
959
988
|
} else {
|
|
960
|
-
if (!showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
961
990
|
try log.lastmiss(.{});
|
|
962
991
|
try log.miss(.{battle.active(player)});
|
|
963
992
|
}
|
|
@@ -1019,22 +1048,20 @@ fn doMove(
|
|
|
1019
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1020
1049
|
}
|
|
1021
1050
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1022
|
-
} else if (showdown) {
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1023
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
try buildRage(battle, player.foe(), options);
|
|
1035
|
-
}
|
|
1036
|
-
return null;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1037
1063
|
}
|
|
1064
|
+
return null;
|
|
1038
1065
|
}
|
|
1039
1066
|
}
|
|
1040
1067
|
|
|
@@ -1094,6 +1121,7 @@ fn calcDamage(
|
|
|
1094
1121
|
target_player: Player,
|
|
1095
1122
|
m: ?Move.Data,
|
|
1096
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1097
1125
|
) bool {
|
|
1098
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1099
1127
|
const cfz = m == null;
|
|
@@ -1105,28 +1133,8 @@ fn calcDamage(
|
|
|
1105
1133
|
|
|
1106
1134
|
const special = move.type.special();
|
|
1107
1135
|
|
|
1108
|
-
|
|
1109
|
-
var
|
|
1110
|
-
if (crit)
|
|
1111
|
-
if (special) side.stored().stats.spc
|
|
1112
|
-
else side.stored().stats.atk
|
|
1113
|
-
else
|
|
1114
|
-
if (special) side.active.stats.spc
|
|
1115
|
-
else side.active.stats.atk;
|
|
1116
|
-
var def: u32 =
|
|
1117
|
-
if (crit)
|
|
1118
|
-
if (special) target.stored().stats.spc
|
|
1119
|
-
else target.stored().stats.def
|
|
1120
|
-
else
|
|
1121
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1122
|
-
if (special)
|
|
1123
|
-
target.active.stats.spc *
|
|
1124
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1125
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1126
|
-
else
|
|
1127
|
-
target.active.stats.def *
|
|
1128
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1129
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1130
1138
|
|
|
1131
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1132
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1134,7 +1142,7 @@ fn calcDamage(
|
|
|
1134
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1135
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1136
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1137
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1138
1146
|
}
|
|
1139
1147
|
|
|
1140
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1151,11 +1159,41 @@ fn calcDamage(
|
|
|
1151
1159
|
d = @min(997, d);
|
|
1152
1160
|
d += 2;
|
|
1153
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1154
1172
|
battle.last_damage = @intCast(d);
|
|
1155
1173
|
|
|
1156
1174
|
return true;
|
|
1157
1175
|
}
|
|
1158
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1159
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1160
1198
|
const side = battle.side(player);
|
|
1161
1199
|
const foe = battle.foe(player);
|
|
@@ -1210,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1210
1248
|
.Psywave => power: {
|
|
1211
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1212
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1213
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1214
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1215
1253
|
},
|
|
1216
1254
|
else => unreachable,
|
|
1217
1255
|
};
|
|
1218
1256
|
|
|
1219
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1220
1258
|
|
|
1221
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1222
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1253,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1253
1291
|
|
|
1254
1292
|
if (!used or !selected) {
|
|
1255
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1256
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1257
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1258
1296
|
return null;
|
|
1259
1297
|
}
|
|
1260
1298
|
|
|
1261
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1262
1304
|
|
|
1263
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1264
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1290,7 +1332,7 @@ fn applyDamage(
|
|
|
1290
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1291
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1292
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1293
|
-
options.calc.capped(sub_player);
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1294
1336
|
return true;
|
|
1295
1337
|
} else {
|
|
1296
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1324,7 +1366,7 @@ fn mirrorMove(
|
|
|
1324
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1325
1367
|
return null;
|
|
1326
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1327
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1328
1370
|
}
|
|
1329
1371
|
|
|
1330
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1345,7 +1387,7 @@ fn metronome(
|
|
|
1345
1387
|
var side = battle.side(player);
|
|
1346
1388
|
|
|
1347
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1348
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1349
1391
|
|
|
1350
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1351
1393
|
return null;
|
|
@@ -1404,7 +1446,16 @@ fn moveHit(
|
|
|
1404
1446
|
break :miss true;
|
|
1405
1447
|
}
|
|
1406
1448
|
if (!showdown) {
|
|
1407
|
-
if (move.effect == .Swift)
|
|
1449
|
+
if (move.effect == .Swift) {
|
|
1450
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1451
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1452
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1453
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1454
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1455
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1456
|
+
try options.chance.hit(player, true, 0);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1408
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1409
1460
|
}
|
|
1410
1461
|
|
|
@@ -1446,7 +1497,10 @@ fn moveHit(
|
|
|
1446
1497
|
|
|
1447
1498
|
if (!miss) return true;
|
|
1448
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1449
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1450
1504
|
return false;
|
|
1451
1505
|
}
|
|
1452
1506
|
|
|
@@ -1497,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1497
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1498
1552
|
if (foe_volatiles.Bide) {
|
|
1499
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1500
|
-
foe_volatiles.state = if (
|
|
1554
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1501
1555
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1502
1556
|
}
|
|
1503
1557
|
|
|
@@ -1588,12 +1642,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1588
1642
|
|
|
1589
1643
|
battle.turn += 1;
|
|
1590
1644
|
|
|
1591
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1592
1646
|
try options.log.tie(.{});
|
|
1593
1647
|
return Result.Tie;
|
|
1594
1648
|
}
|
|
1595
1649
|
|
|
1596
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1597
1651
|
try options.log.tie(.{});
|
|
1598
1652
|
return Result.Tie;
|
|
1599
1653
|
} else if (battle.turn >= 65535) {
|
|
@@ -1773,7 +1827,7 @@ pub const Effects = struct {
|
|
|
1773
1827
|
|
|
1774
1828
|
side.active.volatiles.state = 0;
|
|
1775
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1776
|
-
options.chance.observe(.
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1777
1831
|
|
|
1778
1832
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1779
1833
|
}
|
|
@@ -1842,6 +1896,7 @@ pub const Effects = struct {
|
|
|
1842
1896
|
|
|
1843
1897
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1844
1898
|
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1845
1900
|
|
|
1846
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1847
1902
|
}
|
|
@@ -1887,12 +1942,11 @@ pub const Effects = struct {
|
|
|
1887
1942
|
|
|
1888
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1889
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1890
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1891
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1892
1947
|
return null;
|
|
1893
1948
|
} else if (err) {
|
|
1894
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1895
|
-
assert(!showdown);
|
|
1896
1950
|
return Result.Error;
|
|
1897
1951
|
}
|
|
1898
1952
|
|
|
@@ -1970,7 +2024,7 @@ pub const Effects = struct {
|
|
|
1970
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
1971
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1972
2026
|
// Freeze Clause Mod
|
|
1973
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1974
2028
|
|
|
1975
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
1976
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -2311,7 +2365,7 @@ pub const Effects = struct {
|
|
|
2311
2365
|
}
|
|
2312
2366
|
|
|
2313
2367
|
// Sleep Clause Mod
|
|
2314
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2315
2369
|
for (foe.pokemon) |p| {
|
|
2316
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2317
2371
|
}
|
|
@@ -2378,7 +2432,7 @@ pub const Effects = struct {
|
|
|
2378
2432
|
volatiles.Thrashing = true;
|
|
2379
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2380
2434
|
|
|
2381
|
-
options.chance.observe(.
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2382
2436
|
}
|
|
2383
2437
|
|
|
2384
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2417,7 +2471,7 @@ pub const Effects = struct {
|
|
|
2417
2471
|
if (side.active.volatiles.Binding) return;
|
|
2418
2472
|
side.active.volatiles.Binding = true;
|
|
2419
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2420
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2421
2475
|
if (!showdown) {
|
|
2422
2476
|
foe.active.volatiles.Recharging = false;
|
|
2423
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
@@ -2717,10 +2771,6 @@ fn decrement(
|
|
|
2717
2771
|
} else n - 1;
|
|
2718
2772
|
}
|
|
2719
2773
|
|
|
2720
|
-
fn observation(n: anytype) optional.Optional(chance.Confusion) {
|
|
2721
|
-
return if (n == 0) .ended else .continuing;
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
2774
|
pub const Rolls = struct {
|
|
2725
2775
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2726
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2918,9 +2968,10 @@ pub const Rolls = struct {
|
|
|
2918
2968
|
}
|
|
2919
2969
|
|
|
2920
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2921
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2922
|
-
@intCast(val)
|
|
2923
|
-
else
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2924
2975
|
battle.rng.sleepDuration(player)
|
|
2925
2976
|
else if (showdown)
|
|
2926
2977
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2937,9 +2988,10 @@ pub const Rolls = struct {
|
|
|
2937
2988
|
}
|
|
2938
2989
|
|
|
2939
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2940
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2941
|
-
val
|
|
2942
|
-
else
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2943
2995
|
battle.rng.disableDuration(player)
|
|
2944
2996
|
else if (showdown)
|
|
2945
2997
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2952,9 +3004,10 @@ pub const Rolls = struct {
|
|
|
2952
3004
|
}
|
|
2953
3005
|
|
|
2954
3006
|
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2955
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2956
|
-
@intCast(val)
|
|
2957
|
-
else
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2958
3011
|
battle.rng.confusionDuration(player)
|
|
2959
3012
|
else if (showdown)
|
|
2960
3013
|
battle.rng.range(u3, 2, 6)
|
|
@@ -2967,9 +3020,10 @@ pub const Rolls = struct {
|
|
|
2967
3020
|
}
|
|
2968
3021
|
|
|
2969
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2970
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2971
|
-
@intCast(val)
|
|
2972
|
-
else
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2973
3027
|
battle.rng.attackingDuration(player)
|
|
2974
3028
|
else if (showdown)
|
|
2975
3029
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2990,9 +3044,10 @@ pub const Rolls = struct {
|
|
|
2990
3044
|
options: anytype,
|
|
2991
3045
|
) !u3 {
|
|
2992
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2993
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2994
|
-
@intCast(val)
|
|
2995
|
-
else
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2996
3051
|
battle.rng.distribution(player)
|
|
2997
3052
|
else if (showdown)
|
|
2998
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -3018,15 +3073,13 @@ pub const Rolls = struct {
|
|
|
3018
3073
|
options: anytype,
|
|
3019
3074
|
) !u4 {
|
|
3020
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
3021
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
3022
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
3023
3078
|
val
|
|
3024
3079
|
else
|
|
3025
3080
|
null
|
|
3026
3081
|
else
|
|
3027
|
-
null
|
|
3028
|
-
|
|
3029
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3030
3083
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
3031
3084
|
else slot: {
|
|
3032
3085
|
if (showdown) {
|
|
@@ -3184,7 +3237,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3184
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3185
3238
|
if (n == before) {
|
|
3186
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3187
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3188
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3189
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3190
3243
|
}
|