@pkmn/engine 0.1.0-dev.8b192003 → 0.1.0-dev.8cf7e679
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/README.md +9 -6
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +44 -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 +97 -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 +44 -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 +13 -17
- package/build/pkg/gen1.js +6 -10
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +38 -90
- package/build/pkg/index.js +6 -63
- 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 +35 -69
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +141 -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 +254 -77
- package/package.json +50 -134
- package/src/bin/install-pkmn-engine +9 -7
- package/src/bin/pkmn-debug +1 -1
- 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 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +4 -0
- package/src/lib/common/protocol.zig +45 -44
- package/src/lib/common/rational.zig +53 -51
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +295 -154
- package/src/lib/gen1/chance.zig +477 -119
- 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 +1 -2
- package/src/lib/gen1/data.zig +35 -38
- package/src/lib/gen1/helpers.zig +17 -27
- package/src/lib/gen1/mechanics.zig +248 -148
- 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 +1 -2
- package/src/lib/gen2/data.zig +26 -29
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +9 -18
- package/src/lib/gen2/mechanics.zig +81 -81
- package/src/lib/node.zig +2 -4
- package/src/lib/pkmn.zig +11 -5
- package/src/lib/wasm.zig +36 -1
- package/src/tools/display/dom.ts +96 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- 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(battle.lastMove(player).index).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,11 +498,22 @@ 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) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
509
|
+
} else {
|
|
510
|
+
stored.status -= 1;
|
|
511
|
+
}
|
|
490
512
|
|
|
491
513
|
const duration = Status.duration(stored.status);
|
|
492
|
-
options.chance.
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
493
517
|
|
|
494
518
|
if (duration == 0) {
|
|
495
519
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -527,8 +551,12 @@ fn beforeMove(
|
|
|
527
551
|
}
|
|
528
552
|
|
|
529
553
|
if (volatiles.disable_duration > 0) {
|
|
530
|
-
volatiles.disable_duration
|
|
531
|
-
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
532
560
|
|
|
533
561
|
if (volatiles.disable_duration == 0) {
|
|
534
562
|
volatiles.disable_move = 0;
|
|
@@ -541,14 +569,24 @@ fn beforeMove(
|
|
|
541
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
542
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
543
571
|
if (side.last_selected_move == .None) {
|
|
544
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
545
573
|
return .err;
|
|
546
574
|
}
|
|
547
575
|
|
|
548
576
|
if (volatiles.Confusion) {
|
|
549
577
|
assert(volatiles.confusion > 0);
|
|
550
|
-
|
|
551
|
-
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
585
|
+
} else {
|
|
586
|
+
volatiles.confusion -= 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
552
590
|
|
|
553
591
|
if (volatiles.confusion == 0) {
|
|
554
592
|
volatiles.Confusion = false;
|
|
@@ -567,9 +605,8 @@ fn beforeMove(
|
|
|
567
605
|
volatiles.Binding = false;
|
|
568
606
|
volatiles.Invulnerable = false;
|
|
569
607
|
|
|
570
|
-
options.chance.
|
|
571
|
-
options.chance.
|
|
572
|
-
options.chance.durations(.binding, player, .None);
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
573
610
|
{
|
|
574
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
575
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -577,7 +614,9 @@ fn beforeMove(
|
|
|
577
614
|
const def = foe.active.stats.def;
|
|
578
615
|
foe.active.stats.def = active.stats.def;
|
|
579
616
|
defer foe.active.stats.def = def;
|
|
580
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
581
620
|
}
|
|
582
621
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
583
622
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -607,9 +646,8 @@ fn beforeMove(
|
|
|
607
646
|
volatiles.Charging = false;
|
|
608
647
|
volatiles.Binding = false;
|
|
609
648
|
|
|
610
|
-
options.chance.
|
|
611
|
-
options.chance.
|
|
612
|
-
options.chance.durations(.binding, player, .None);
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
650
|
+
options.chance.observe(.binding, player, .None);
|
|
613
651
|
|
|
614
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
615
653
|
try log.cant(.{ ident, .Paralysis });
|
|
@@ -628,8 +666,8 @@ fn beforeMove(
|
|
|
628
666
|
volatiles.state +%= battle.last_damage;
|
|
629
667
|
}
|
|
630
668
|
|
|
631
|
-
volatiles.attacks
|
|
632
|
-
options.chance.
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
633
671
|
|
|
634
672
|
if (volatiles.attacks != 0) {
|
|
635
673
|
try log.activate(.{ ident, .Bide });
|
|
@@ -665,14 +703,12 @@ fn beforeMove(
|
|
|
665
703
|
if (volatiles.Thrashing) {
|
|
666
704
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
667
705
|
|
|
668
|
-
volatiles.attacks
|
|
669
|
-
options.chance.
|
|
670
|
-
.thrashing,
|
|
671
|
-
player,
|
|
672
|
-
if (volatiles.attacks <= 1) .ended else .continuing,
|
|
673
|
-
);
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
674
708
|
|
|
675
709
|
if (volatiles.attacks == 0) {
|
|
710
|
+
const overwritten = volatiles.Confusion;
|
|
711
|
+
|
|
676
712
|
volatiles.Thrashing = false;
|
|
677
713
|
volatiles.Confusion = true;
|
|
678
714
|
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
@@ -682,6 +718,11 @@ fn beforeMove(
|
|
|
682
718
|
// can be expected to recognize this pattern and pass the information only to the
|
|
683
719
|
// correct player)
|
|
684
720
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
721
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
722
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
723
|
+
else
|
|
724
|
+
.started);
|
|
725
|
+
if (!overwritten) options.calc.confusion(player);
|
|
685
726
|
}
|
|
686
727
|
|
|
687
728
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -692,8 +733,8 @@ fn beforeMove(
|
|
|
692
733
|
}
|
|
693
734
|
|
|
694
735
|
if (volatiles.Binding) {
|
|
695
|
-
volatiles.attacks
|
|
696
|
-
options.chance.
|
|
736
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
737
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
697
738
|
|
|
698
739
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
699
740
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -747,9 +788,14 @@ fn canMove(
|
|
|
747
788
|
|
|
748
789
|
if (move.effect == .Thrashing) {
|
|
749
790
|
Effects.thrashing(battle, player, options);
|
|
750
|
-
} else if (
|
|
751
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
752
|
-
|
|
791
|
+
} else if (move.effect == .Binding) {
|
|
792
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
793
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
794
|
+
if (showdown) {
|
|
795
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
796
|
+
} else {
|
|
797
|
+
try Effects.binding(battle, player, false, options);
|
|
798
|
+
}
|
|
753
799
|
}
|
|
754
800
|
|
|
755
801
|
return true;
|
|
@@ -786,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
786
832
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
787
833
|
}
|
|
788
834
|
|
|
789
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
835
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
790
836
|
var active = &side.active;
|
|
791
837
|
const volatiles = &active.volatiles;
|
|
792
838
|
|
|
@@ -794,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
794
840
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
795
841
|
if (showdown and volatiles.Transform) return;
|
|
796
842
|
|
|
843
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
845
|
+
|
|
797
846
|
assert(mslot > 0 and mslot <= 4);
|
|
798
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
847
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
799
848
|
}
|
|
800
849
|
|
|
801
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -849,7 +898,7 @@ fn doMove(
|
|
|
849
898
|
|
|
850
899
|
var late = showdown and move.effect != .Explode;
|
|
851
900
|
const skip = status or immune;
|
|
852
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
853
902
|
if (showdown and move.effect.isMulti()) {
|
|
854
903
|
try Effects.multiHit(battle, player, move, options);
|
|
855
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -871,8 +920,9 @@ fn doMove(
|
|
|
871
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
872
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
873
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
874
|
-
if (showdown) break :
|
|
875
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
923
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
924
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
925
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
876
926
|
return @as(?Result, Result.Error);
|
|
877
927
|
}
|
|
878
928
|
if (battle.last_damage == 0) {
|
|
@@ -886,11 +936,12 @@ fn doMove(
|
|
|
886
936
|
}
|
|
887
937
|
}
|
|
888
938
|
|
|
939
|
+
var missed: bool = undefined;
|
|
889
940
|
const zero = battle.last_damage == 0;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
941
|
+
if (!showdown and !skip) {
|
|
942
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
943
|
+
miss = missed or zero;
|
|
944
|
+
}
|
|
894
945
|
|
|
895
946
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
896
947
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -917,11 +968,32 @@ fn doMove(
|
|
|
917
968
|
try log.immune(.{ foe_ident, .OHKO });
|
|
918
969
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
919
970
|
try log.immune(.{ foe_ident, .None });
|
|
971
|
+
if (!showdown) {
|
|
972
|
+
if (move.effect == .Binding) {
|
|
973
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
974
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
975
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
976
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
977
|
+
// (considered missing)
|
|
978
|
+
assert(zero);
|
|
979
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
980
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
981
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
982
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
983
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
984
|
+
// roll)
|
|
985
|
+
try options.chance.commit(player, .binding);
|
|
986
|
+
} else {
|
|
987
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
988
|
+
// division-by-zero freeze alternative timeline scenario
|
|
989
|
+
try options.chance.commit(player, .glitch);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
920
992
|
} else if (mist) {
|
|
921
993
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
922
994
|
try log.fail(.{ foe_ident, .None });
|
|
923
995
|
} else {
|
|
924
|
-
if (!showdown
|
|
996
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
925
997
|
try log.lastmiss(.{});
|
|
926
998
|
try log.miss(.{battle.active(player)});
|
|
927
999
|
}
|
|
@@ -983,22 +1055,20 @@ fn doMove(
|
|
|
983
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
984
1056
|
}
|
|
985
1057
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
986
|
-
} else if (showdown) {
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
987
1059
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
try buildRage(battle, player.foe(), options);
|
|
999
|
-
}
|
|
1000
|
-
return null;
|
|
1060
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1061
|
+
if (immune) {
|
|
1062
|
+
battle.last_damage = 0;
|
|
1063
|
+
assert(foe.stored().hp > 0);
|
|
1064
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1065
|
+
if (sub) {
|
|
1066
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1067
|
+
} else {
|
|
1068
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1069
|
+
try buildRage(battle, player.foe(), options);
|
|
1001
1070
|
}
|
|
1071
|
+
return null;
|
|
1002
1072
|
}
|
|
1003
1073
|
}
|
|
1004
1074
|
|
|
@@ -1058,6 +1128,7 @@ fn calcDamage(
|
|
|
1058
1128
|
target_player: Player,
|
|
1059
1129
|
m: ?Move.Data,
|
|
1060
1130
|
crit: bool,
|
|
1131
|
+
options: anytype,
|
|
1061
1132
|
) bool {
|
|
1062
1133
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1063
1134
|
const cfz = m == null;
|
|
@@ -1069,28 +1140,8 @@ fn calcDamage(
|
|
|
1069
1140
|
|
|
1070
1141
|
const special = move.type.special();
|
|
1071
1142
|
|
|
1072
|
-
|
|
1073
|
-
var
|
|
1074
|
-
if (crit)
|
|
1075
|
-
if (special) side.stored().stats.spc
|
|
1076
|
-
else side.stored().stats.atk
|
|
1077
|
-
else
|
|
1078
|
-
if (special) side.active.stats.spc
|
|
1079
|
-
else side.active.stats.atk;
|
|
1080
|
-
var def: u32 =
|
|
1081
|
-
if (crit)
|
|
1082
|
-
if (special) target.stored().stats.spc
|
|
1083
|
-
else target.stored().stats.def
|
|
1084
|
-
else
|
|
1085
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1086
|
-
if (special)
|
|
1087
|
-
target.active.stats.spc *
|
|
1088
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1089
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1090
|
-
else
|
|
1091
|
-
target.active.stats.def *
|
|
1092
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1093
|
-
// zig fmt: on
|
|
1143
|
+
var atk = attack(side, crit, special);
|
|
1144
|
+
var def = defense(target, crit, special, cfz);
|
|
1094
1145
|
|
|
1095
1146
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1096
1147
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1098,7 +1149,7 @@ fn calcDamage(
|
|
|
1098
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1099
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1100
1151
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1101
|
-
def = @max((def / 4) & 255, if (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1102
1153
|
}
|
|
1103
1154
|
|
|
1104
1155
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1115,11 +1166,41 @@ fn calcDamage(
|
|
|
1115
1166
|
d = @min(997, d);
|
|
1116
1167
|
d += 2;
|
|
1117
1168
|
|
|
1169
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1170
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1171
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1172
|
+
// that results in counter intuitive probabilities in the common case
|
|
1173
|
+
if (pkmn.options.chance and !pkmn.options.mod) {
|
|
1174
|
+
atk = attack(side, !crit, special);
|
|
1175
|
+
def = defense(target, !crit, special, false);
|
|
1176
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1118
1179
|
battle.last_damage = @intCast(d);
|
|
1119
1180
|
|
|
1120
1181
|
return true;
|
|
1121
1182
|
}
|
|
1122
1183
|
|
|
1184
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1185
|
+
return if (crit)
|
|
1186
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1187
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1191
|
+
return if (crit)
|
|
1192
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1193
|
+
else
|
|
1194
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1195
|
+
if (special)
|
|
1196
|
+
target.active.stats.spc *
|
|
1197
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1198
|
+
else
|
|
1199
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1200
|
+
target.active.stats.def *
|
|
1201
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1123
1204
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1124
1205
|
const side = battle.side(player);
|
|
1125
1206
|
const foe = battle.foe(player);
|
|
@@ -1174,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1174
1255
|
.Psywave => power: {
|
|
1175
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1176
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1177
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1178
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1179
1260
|
},
|
|
1180
1261
|
else => unreachable,
|
|
1181
1262
|
};
|
|
1182
1263
|
|
|
1183
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1184
1265
|
|
|
1185
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1186
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1217,12 +1298,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1217
1298
|
|
|
1218
1299
|
if (!used or !selected) {
|
|
1219
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1220
|
-
if (!
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1221
1302
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1222
1303
|
return null;
|
|
1223
1304
|
}
|
|
1224
1305
|
|
|
1225
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1226
1311
|
|
|
1227
1312
|
// Pokémon Showdown calls moveHit before Counter
|
|
1228
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1254,7 +1339,7 @@ fn applyDamage(
|
|
|
1254
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1255
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1256
1341
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1257
|
-
options.calc.capped(sub_player);
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1258
1343
|
return true;
|
|
1259
1344
|
} else {
|
|
1260
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1288,7 +1373,7 @@ fn mirrorMove(
|
|
|
1288
1373
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1289
1374
|
return null;
|
|
1290
1375
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1291
|
-
incrementPP(side, mslot);
|
|
1376
|
+
incrementPP(side, player, mslot, options);
|
|
1292
1377
|
}
|
|
1293
1378
|
|
|
1294
1379
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1309,7 +1394,7 @@ fn metronome(
|
|
|
1309
1394
|
var side = battle.side(player);
|
|
1310
1395
|
|
|
1311
1396
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1312
|
-
incrementPP(side, mslot);
|
|
1397
|
+
incrementPP(side, player, mslot, options);
|
|
1313
1398
|
|
|
1314
1399
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1315
1400
|
return null;
|
|
@@ -1410,7 +1495,10 @@ fn moveHit(
|
|
|
1410
1495
|
|
|
1411
1496
|
if (!miss) return true;
|
|
1412
1497
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1498
|
+
|
|
1413
1499
|
side.active.volatiles.Binding = false;
|
|
1500
|
+
options.chance.observe(.binding, player, .None);
|
|
1501
|
+
|
|
1414
1502
|
return false;
|
|
1415
1503
|
}
|
|
1416
1504
|
|
|
@@ -1461,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1461
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1462
1550
|
if (foe_volatiles.Bide) {
|
|
1463
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1464
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1465
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1466
1554
|
}
|
|
1467
1555
|
|
|
@@ -1552,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1552
1640
|
|
|
1553
1641
|
battle.turn += 1;
|
|
1554
1642
|
|
|
1555
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1556
1644
|
try options.log.tie(.{});
|
|
1557
1645
|
return Result.Tie;
|
|
1558
1646
|
}
|
|
1559
1647
|
|
|
1560
|
-
if (
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1561
1649
|
try options.log.tie(.{});
|
|
1562
1650
|
return Result.Tie;
|
|
1563
1651
|
} else if (battle.turn >= 65535) {
|
|
@@ -1737,7 +1825,7 @@ pub const Effects = struct {
|
|
|
1737
1825
|
|
|
1738
1826
|
side.active.volatiles.state = 0;
|
|
1739
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1740
|
-
options.chance.
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1741
1829
|
|
|
1742
1830
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1743
1831
|
}
|
|
@@ -1805,7 +1893,8 @@ pub const Effects = struct {
|
|
|
1805
1893
|
foe.active.volatiles.Confusion = true;
|
|
1806
1894
|
|
|
1807
1895
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1808
|
-
options.chance.
|
|
1896
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1809
1898
|
|
|
1810
1899
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1811
1900
|
}
|
|
@@ -1851,19 +1940,18 @@ pub const Effects = struct {
|
|
|
1851
1940
|
|
|
1852
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1853
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1854
|
-
if (n == 0) {
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1855
1944
|
try options.log.fail(.{ foe_ident, .None });
|
|
1856
1945
|
return null;
|
|
1857
1946
|
} else if (err) {
|
|
1858
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1859
|
-
assert(!showdown);
|
|
1860
1948
|
return Result.Error;
|
|
1861
1949
|
}
|
|
1862
1950
|
|
|
1863
1951
|
volatiles.disable_move =
|
|
1864
1952
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1865
1953
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1866
|
-
options.chance.
|
|
1954
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1867
1955
|
|
|
1868
1956
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1869
1957
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1934,7 +2022,7 @@ pub const Effects = struct {
|
|
|
1934
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
1935
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1936
2024
|
// Freeze Clause Mod
|
|
1937
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1938
2026
|
|
|
1939
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
1940
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1970,7 +2058,7 @@ pub const Effects = struct {
|
|
|
1970
2058
|
if (p != player and Status.any(s.stored().status)) {
|
|
1971
2059
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1972
2060
|
s.stored().status = 0;
|
|
1973
|
-
options.chance.
|
|
2061
|
+
options.chance.observe(.sleep, p, .None);
|
|
1974
2062
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1975
2063
|
s.stored().status = Status.init(.PSN);
|
|
1976
2064
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
@@ -1981,7 +2069,7 @@ pub const Effects = struct {
|
|
|
1981
2069
|
if (Status.any(foe_stored.status)) {
|
|
1982
2070
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1983
2071
|
foe.last_selected_move = .SKIP_TURN;
|
|
1984
|
-
options.chance.
|
|
2072
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1985
2073
|
}
|
|
1986
2074
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2075
|
foe_stored.status = 0;
|
|
@@ -2275,7 +2363,7 @@ pub const Effects = struct {
|
|
|
2275
2363
|
}
|
|
2276
2364
|
|
|
2277
2365
|
// Sleep Clause Mod
|
|
2278
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2279
2367
|
for (foe.pokemon) |p| {
|
|
2280
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2281
2369
|
}
|
|
@@ -2283,7 +2371,7 @@ pub const Effects = struct {
|
|
|
2283
2371
|
foe.active.volatiles.Recharging = false;
|
|
2284
2372
|
|
|
2285
2373
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2286
|
-
options.chance.
|
|
2374
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2287
2375
|
|
|
2288
2376
|
const last = battle.side(player).last_selected_move;
|
|
2289
2377
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
@@ -2342,7 +2430,7 @@ pub const Effects = struct {
|
|
|
2342
2430
|
volatiles.Thrashing = true;
|
|
2343
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2344
2432
|
|
|
2345
|
-
options.chance.
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2346
2434
|
}
|
|
2347
2435
|
|
|
2348
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2381,14 +2469,14 @@ pub const Effects = struct {
|
|
|
2381
2469
|
if (side.active.volatiles.Binding) return;
|
|
2382
2470
|
side.active.volatiles.Binding = true;
|
|
2383
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2384
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2385
2473
|
if (!showdown) {
|
|
2386
2474
|
foe.active.volatiles.Recharging = false;
|
|
2387
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2388
2476
|
|
|
2389
2477
|
side.active.volatiles.attacks =
|
|
2390
2478
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2391
|
-
options.chance.
|
|
2479
|
+
options.chance.observe(.binding, player, .started);
|
|
2392
2480
|
}
|
|
2393
2481
|
|
|
2394
2482
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2627,13 +2715,13 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
|
2627
2715
|
if (volatiles.disable_move != 0) {
|
|
2628
2716
|
volatiles.disable_move = 0;
|
|
2629
2717
|
volatiles.disable_duration = 0;
|
|
2630
|
-
options.chance.
|
|
2718
|
+
options.chance.observe(.disable, who, .None);
|
|
2631
2719
|
try log.end(.{ ident, .DisableSilent });
|
|
2632
2720
|
}
|
|
2633
2721
|
if (volatiles.Confusion) {
|
|
2634
2722
|
// volatiles.confusion is left unchanged
|
|
2635
2723
|
volatiles.Confusion = false;
|
|
2636
|
-
options.chance.
|
|
2724
|
+
options.chance.observe(.confusion, who, .None);
|
|
2637
2725
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2638
2726
|
}
|
|
2639
2727
|
if (volatiles.Mist) {
|
|
@@ -2668,8 +2756,17 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
|
2668
2756
|
}
|
|
2669
2757
|
}
|
|
2670
2758
|
|
|
2671
|
-
fn
|
|
2672
|
-
|
|
2759
|
+
fn decrement(
|
|
2760
|
+
comptime field: chance.Action.Field,
|
|
2761
|
+
player: Player,
|
|
2762
|
+
options: anytype,
|
|
2763
|
+
n: anytype,
|
|
2764
|
+
) @TypeOf(n) {
|
|
2765
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2766
|
+
.started, .ended => 0,
|
|
2767
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2768
|
+
else => unreachable,
|
|
2769
|
+
} else n - 1;
|
|
2673
2770
|
}
|
|
2674
2771
|
|
|
2675
2772
|
pub const Rolls = struct {
|
|
@@ -2869,9 +2966,10 @@ pub const Rolls = struct {
|
|
|
2869
2966
|
}
|
|
2870
2967
|
|
|
2871
2968
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2872
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2873
|
-
@intCast(val)
|
|
2874
|
-
else
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2875
2973
|
battle.rng.sleepDuration(player)
|
|
2876
2974
|
else if (showdown)
|
|
2877
2975
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,9 +2986,10 @@ pub const Rolls = struct {
|
|
|
2888
2986
|
}
|
|
2889
2987
|
|
|
2890
2988
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2891
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2892
|
-
val
|
|
2893
|
-
else
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2894
2993
|
battle.rng.disableDuration(player)
|
|
2895
2994
|
else if (showdown)
|
|
2896
2995
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,9 +3002,10 @@ pub const Rolls = struct {
|
|
|
2903
3002
|
}
|
|
2904
3003
|
|
|
2905
3004
|
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@intCast(val)
|
|
2908
|
-
else
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2909
3009
|
battle.rng.confusionDuration(player)
|
|
2910
3010
|
else if (showdown)
|
|
2911
3011
|
battle.rng.range(u3, 2, 6)
|
|
@@ -2918,9 +3018,10 @@ pub const Rolls = struct {
|
|
|
2918
3018
|
}
|
|
2919
3019
|
|
|
2920
3020
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2921
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2922
|
-
@intCast(val)
|
|
2923
|
-
else
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2924
3025
|
battle.rng.attackingDuration(player)
|
|
2925
3026
|
else if (showdown)
|
|
2926
3027
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2941,9 +3042,10 @@ pub const Rolls = struct {
|
|
|
2941
3042
|
options: anytype,
|
|
2942
3043
|
) !u3 {
|
|
2943
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2944
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2945
|
-
@intCast(val)
|
|
2946
|
-
else
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2947
3049
|
battle.rng.distribution(player)
|
|
2948
3050
|
else if (showdown)
|
|
2949
3051
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2969,15 +3071,13 @@ pub const Rolls = struct {
|
|
|
2969
3071
|
options: anytype,
|
|
2970
3072
|
) !u4 {
|
|
2971
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
2972
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2973
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2974
3076
|
val
|
|
2975
3077
|
else
|
|
2976
3078
|
null
|
|
2977
3079
|
else
|
|
2978
|
-
null
|
|
2979
|
-
|
|
2980
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2981
3081
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2982
3082
|
else slot: {
|
|
2983
3083
|
if (showdown) {
|
|
@@ -3135,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3135
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3136
3236
|
if (n == before) {
|
|
3137
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3138
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3139
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3140
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3141
3241
|
}
|