@pkmn/engine 0.1.0-dev.8b192003 → 0.1.0-dev.8c935a85
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 +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 +330 -85
- package/package.json +54 -135
- 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 +121 -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 -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 +256 -181
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +60 -1
- package/src/lib/gen1/calc.zig +314 -164
- 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 +249 -156
- 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 +3 -116
- 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 -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(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,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,19 +614,14 @@ 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
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
583
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
584
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
585
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
586
|
-
player
|
|
587
|
-
else
|
|
588
|
-
player.foe();
|
|
589
621
|
|
|
590
622
|
const uncapped = battle.last_damage;
|
|
591
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
592
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
593
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
594
626
|
if (showdown) battle.last_damage = uncapped;
|
|
595
627
|
|
|
@@ -607,9 +639,8 @@ fn beforeMove(
|
|
|
607
639
|
volatiles.Charging = false;
|
|
608
640
|
volatiles.Binding = false;
|
|
609
641
|
|
|
610
|
-
options.chance.
|
|
611
|
-
options.chance.
|
|
612
|
-
options.chance.durations(.binding, player, .None);
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
613
644
|
|
|
614
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
615
646
|
try log.cant(.{ ident, .Paralysis });
|
|
@@ -628,8 +659,8 @@ fn beforeMove(
|
|
|
628
659
|
volatiles.state +%= battle.last_damage;
|
|
629
660
|
}
|
|
630
661
|
|
|
631
|
-
volatiles.attacks
|
|
632
|
-
options.chance.
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
633
664
|
|
|
634
665
|
if (volatiles.attacks != 0) {
|
|
635
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -665,14 +696,12 @@ fn beforeMove(
|
|
|
665
696
|
if (volatiles.Thrashing) {
|
|
666
697
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
667
698
|
|
|
668
|
-
volatiles.attacks
|
|
669
|
-
options.chance.
|
|
670
|
-
.thrashing,
|
|
671
|
-
player,
|
|
672
|
-
if (volatiles.attacks <= 1) .ended else .continuing,
|
|
673
|
-
);
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
674
701
|
|
|
675
702
|
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
676
705
|
volatiles.Thrashing = false;
|
|
677
706
|
volatiles.Confusion = true;
|
|
678
707
|
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
@@ -682,6 +711,11 @@ fn beforeMove(
|
|
|
682
711
|
// can be expected to recognize this pattern and pass the information only to the
|
|
683
712
|
// correct player)
|
|
684
713
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
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);
|
|
685
719
|
}
|
|
686
720
|
|
|
687
721
|
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
@@ -692,8 +726,8 @@ fn beforeMove(
|
|
|
692
726
|
}
|
|
693
727
|
|
|
694
728
|
if (volatiles.Binding) {
|
|
695
|
-
volatiles.attacks
|
|
696
|
-
options.chance.
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
697
731
|
|
|
698
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
699
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -747,9 +781,14 @@ fn canMove(
|
|
|
747
781
|
|
|
748
782
|
if (move.effect == .Thrashing) {
|
|
749
783
|
Effects.thrashing(battle, player, options);
|
|
750
|
-
} else if (
|
|
751
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
752
|
-
|
|
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
|
+
}
|
|
753
792
|
}
|
|
754
793
|
|
|
755
794
|
return true;
|
|
@@ -786,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
786
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
787
826
|
}
|
|
788
827
|
|
|
789
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
790
829
|
var active = &side.active;
|
|
791
830
|
const volatiles = &active.volatiles;
|
|
792
831
|
|
|
@@ -794,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
794
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
795
834
|
if (showdown and volatiles.Transform) return;
|
|
796
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
797
839
|
assert(mslot > 0 and mslot <= 4);
|
|
798
|
-
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;
|
|
799
841
|
}
|
|
800
842
|
|
|
801
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -849,7 +891,7 @@ fn doMove(
|
|
|
849
891
|
|
|
850
892
|
var late = showdown and move.effect != .Explode;
|
|
851
893
|
const skip = status or immune;
|
|
852
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
853
895
|
if (showdown and move.effect.isMulti()) {
|
|
854
896
|
try Effects.multiHit(battle, player, move, options);
|
|
855
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -871,8 +913,9 @@ fn doMove(
|
|
|
871
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
872
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
873
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
874
|
-
if (showdown) break :
|
|
875
|
-
} 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);
|
|
876
919
|
return @as(?Result, Result.Error);
|
|
877
920
|
}
|
|
878
921
|
if (battle.last_damage == 0) {
|
|
@@ -886,11 +929,12 @@ fn doMove(
|
|
|
886
929
|
}
|
|
887
930
|
}
|
|
888
931
|
|
|
932
|
+
var missed: bool = undefined;
|
|
889
933
|
const zero = battle.last_damage == 0;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
894
938
|
|
|
895
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
896
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -917,11 +961,32 @@ fn doMove(
|
|
|
917
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
918
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
919
963
|
try log.immune(.{ foe_ident, .None });
|
|
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
|
+
}
|
|
984
|
+
}
|
|
920
985
|
} else if (mist) {
|
|
921
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
922
987
|
try log.fail(.{ foe_ident, .None });
|
|
923
988
|
} else {
|
|
924
|
-
if (!showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
925
990
|
try log.lastmiss(.{});
|
|
926
991
|
try log.miss(.{battle.active(player)});
|
|
927
992
|
}
|
|
@@ -983,22 +1048,20 @@ fn doMove(
|
|
|
983
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
984
1049
|
}
|
|
985
1050
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
986
|
-
} else if (showdown) {
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
987
1052
|
// 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;
|
|
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);
|
|
1001
1063
|
}
|
|
1064
|
+
return null;
|
|
1002
1065
|
}
|
|
1003
1066
|
}
|
|
1004
1067
|
|
|
@@ -1058,6 +1121,7 @@ fn calcDamage(
|
|
|
1058
1121
|
target_player: Player,
|
|
1059
1122
|
m: ?Move.Data,
|
|
1060
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1061
1125
|
) bool {
|
|
1062
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1063
1127
|
const cfz = m == null;
|
|
@@ -1069,28 +1133,8 @@ fn calcDamage(
|
|
|
1069
1133
|
|
|
1070
1134
|
const special = move.type.special();
|
|
1071
1135
|
|
|
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
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1094
1138
|
|
|
1095
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1096
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1098,7 +1142,7 @@ fn calcDamage(
|
|
|
1098
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1099
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1100
1144
|
// 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 (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1102
1146
|
}
|
|
1103
1147
|
|
|
1104
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1115,11 +1159,41 @@ fn calcDamage(
|
|
|
1115
1159
|
d = @min(997, d);
|
|
1116
1160
|
d += 2;
|
|
1117
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
|
+
|
|
1118
1172
|
battle.last_damage = @intCast(d);
|
|
1119
1173
|
|
|
1120
1174
|
return true;
|
|
1121
1175
|
}
|
|
1122
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
|
+
|
|
1123
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1124
1198
|
const side = battle.side(player);
|
|
1125
1199
|
const foe = battle.foe(player);
|
|
@@ -1174,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1174
1248
|
.Psywave => power: {
|
|
1175
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1176
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1177
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1178
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1179
1253
|
},
|
|
1180
1254
|
else => unreachable,
|
|
1181
1255
|
};
|
|
1182
1256
|
|
|
1183
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1184
1258
|
|
|
1185
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1186
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1217,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1217
1291
|
|
|
1218
1292
|
if (!used or !selected) {
|
|
1219
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1220
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1221
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1222
1296
|
return null;
|
|
1223
1297
|
}
|
|
1224
1298
|
|
|
1225
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1226
1304
|
|
|
1227
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1228
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1254,7 +1332,7 @@ fn applyDamage(
|
|
|
1254
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1255
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1256
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1257
|
-
options.calc.capped(sub_player);
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1258
1336
|
return true;
|
|
1259
1337
|
} else {
|
|
1260
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1288,7 +1366,7 @@ fn mirrorMove(
|
|
|
1288
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1289
1367
|
return null;
|
|
1290
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1291
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1292
1370
|
}
|
|
1293
1371
|
|
|
1294
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1309,7 +1387,7 @@ fn metronome(
|
|
|
1309
1387
|
var side = battle.side(player);
|
|
1310
1388
|
|
|
1311
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1312
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1313
1391
|
|
|
1314
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1315
1393
|
return null;
|
|
@@ -1410,7 +1488,10 @@ fn moveHit(
|
|
|
1410
1488
|
|
|
1411
1489
|
if (!miss) return true;
|
|
1412
1490
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1491
|
+
|
|
1413
1492
|
side.active.volatiles.Binding = false;
|
|
1493
|
+
options.chance.observe(.binding, player, .None);
|
|
1494
|
+
|
|
1414
1495
|
return false;
|
|
1415
1496
|
}
|
|
1416
1497
|
|
|
@@ -1461,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1461
1542
|
assert(!foe_volatiles.MultiHit);
|
|
1462
1543
|
if (foe_volatiles.Bide) {
|
|
1463
1544
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1464
|
-
foe_volatiles.state = if (
|
|
1545
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1465
1546
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1466
1547
|
}
|
|
1467
1548
|
|
|
@@ -1552,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1552
1633
|
|
|
1553
1634
|
battle.turn += 1;
|
|
1554
1635
|
|
|
1555
|
-
if (
|
|
1636
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1556
1637
|
try options.log.tie(.{});
|
|
1557
1638
|
return Result.Tie;
|
|
1558
1639
|
}
|
|
1559
1640
|
|
|
1560
|
-
if (
|
|
1641
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1561
1642
|
try options.log.tie(.{});
|
|
1562
1643
|
return Result.Tie;
|
|
1563
1644
|
} else if (battle.turn >= 65535) {
|
|
@@ -1737,7 +1818,7 @@ pub const Effects = struct {
|
|
|
1737
1818
|
|
|
1738
1819
|
side.active.volatiles.state = 0;
|
|
1739
1820
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1740
|
-
options.chance.
|
|
1821
|
+
options.chance.observe(.attacking, player, .started);
|
|
1741
1822
|
|
|
1742
1823
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1743
1824
|
}
|
|
@@ -1805,7 +1886,8 @@ pub const Effects = struct {
|
|
|
1805
1886
|
foe.active.volatiles.Confusion = true;
|
|
1806
1887
|
|
|
1807
1888
|
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1808
|
-
options.chance.
|
|
1889
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1890
|
+
options.calc.confusion(player.foe());
|
|
1809
1891
|
|
|
1810
1892
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1811
1893
|
}
|
|
@@ -1851,19 +1933,18 @@ pub const Effects = struct {
|
|
|
1851
1933
|
|
|
1852
1934
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1853
1935
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1854
|
-
if (n == 0) {
|
|
1936
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1855
1937
|
try options.log.fail(.{ foe_ident, .None });
|
|
1856
1938
|
return null;
|
|
1857
1939
|
} else if (err) {
|
|
1858
1940
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1859
|
-
assert(!showdown);
|
|
1860
1941
|
return Result.Error;
|
|
1861
1942
|
}
|
|
1862
1943
|
|
|
1863
1944
|
volatiles.disable_move =
|
|
1864
1945
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1865
1946
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1866
|
-
options.chance.
|
|
1947
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1867
1948
|
|
|
1868
1949
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1869
1950
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1934,7 +2015,7 @@ pub const Effects = struct {
|
|
|
1934
2015
|
if (foe.active.types.includes(move.type)) return;
|
|
1935
2016
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1936
2017
|
// Freeze Clause Mod
|
|
1937
|
-
if (
|
|
2018
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1938
2019
|
|
|
1939
2020
|
foe_stored.status = Status.init(.FRZ);
|
|
1940
2021
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1970,7 +2051,7 @@ pub const Effects = struct {
|
|
|
1970
2051
|
if (p != player and Status.any(s.stored().status)) {
|
|
1971
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1972
2053
|
s.stored().status = 0;
|
|
1973
|
-
options.chance.
|
|
2054
|
+
options.chance.observe(.sleep, p, .None);
|
|
1974
2055
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1975
2056
|
s.stored().status = Status.init(.PSN);
|
|
1976
2057
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
@@ -1981,7 +2062,7 @@ pub const Effects = struct {
|
|
|
1981
2062
|
if (Status.any(foe_stored.status)) {
|
|
1982
2063
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1983
2064
|
foe.last_selected_move = .SKIP_TURN;
|
|
1984
|
-
options.chance.
|
|
2065
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1985
2066
|
}
|
|
1986
2067
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2068
|
foe_stored.status = 0;
|
|
@@ -2275,7 +2356,7 @@ pub const Effects = struct {
|
|
|
2275
2356
|
}
|
|
2276
2357
|
|
|
2277
2358
|
// Sleep Clause Mod
|
|
2278
|
-
if (
|
|
2359
|
+
if (pkmn.options.mod) {
|
|
2279
2360
|
for (foe.pokemon) |p| {
|
|
2280
2361
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2281
2362
|
}
|
|
@@ -2283,7 +2364,7 @@ pub const Effects = struct {
|
|
|
2283
2364
|
foe.active.volatiles.Recharging = false;
|
|
2284
2365
|
|
|
2285
2366
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2286
|
-
options.chance.
|
|
2367
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2287
2368
|
|
|
2288
2369
|
const last = battle.side(player).last_selected_move;
|
|
2289
2370
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
@@ -2342,7 +2423,7 @@ pub const Effects = struct {
|
|
|
2342
2423
|
volatiles.Thrashing = true;
|
|
2343
2424
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2344
2425
|
|
|
2345
|
-
options.chance.
|
|
2426
|
+
options.chance.observe(.attacking, player, .started);
|
|
2346
2427
|
}
|
|
2347
2428
|
|
|
2348
2429
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2381,14 +2462,14 @@ pub const Effects = struct {
|
|
|
2381
2462
|
if (side.active.volatiles.Binding) return;
|
|
2382
2463
|
side.active.volatiles.Binding = true;
|
|
2383
2464
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2384
|
-
// (Pokémon Showdown
|
|
2465
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2385
2466
|
if (!showdown) {
|
|
2386
2467
|
foe.active.volatiles.Recharging = false;
|
|
2387
2468
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2388
2469
|
|
|
2389
2470
|
side.active.volatiles.attacks =
|
|
2390
2471
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2391
|
-
options.chance.
|
|
2472
|
+
options.chance.observe(.binding, player, .started);
|
|
2392
2473
|
}
|
|
2393
2474
|
|
|
2394
2475
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2627,13 +2708,13 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
|
2627
2708
|
if (volatiles.disable_move != 0) {
|
|
2628
2709
|
volatiles.disable_move = 0;
|
|
2629
2710
|
volatiles.disable_duration = 0;
|
|
2630
|
-
options.chance.
|
|
2711
|
+
options.chance.observe(.disable, who, .None);
|
|
2631
2712
|
try log.end(.{ ident, .DisableSilent });
|
|
2632
2713
|
}
|
|
2633
2714
|
if (volatiles.Confusion) {
|
|
2634
2715
|
// volatiles.confusion is left unchanged
|
|
2635
2716
|
volatiles.Confusion = false;
|
|
2636
|
-
options.chance.
|
|
2717
|
+
options.chance.observe(.confusion, who, .None);
|
|
2637
2718
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2638
2719
|
}
|
|
2639
2720
|
if (volatiles.Mist) {
|
|
@@ -2668,8 +2749,17 @@ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
|
2668
2749
|
}
|
|
2669
2750
|
}
|
|
2670
2751
|
|
|
2671
|
-
fn
|
|
2672
|
-
|
|
2752
|
+
fn decrement(
|
|
2753
|
+
comptime field: chance.Action.Field,
|
|
2754
|
+
player: Player,
|
|
2755
|
+
options: anytype,
|
|
2756
|
+
n: anytype,
|
|
2757
|
+
) @TypeOf(n) {
|
|
2758
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2759
|
+
.started, .ended => 0,
|
|
2760
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2761
|
+
else => unreachable,
|
|
2762
|
+
} else n - 1;
|
|
2673
2763
|
}
|
|
2674
2764
|
|
|
2675
2765
|
pub const Rolls = struct {
|
|
@@ -2869,9 +2959,10 @@ pub const Rolls = struct {
|
|
|
2869
2959
|
}
|
|
2870
2960
|
|
|
2871
2961
|
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
|
|
2962
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2963
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2964
|
+
else
|
|
2965
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2875
2966
|
battle.rng.sleepDuration(player)
|
|
2876
2967
|
else if (showdown)
|
|
2877
2968
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,9 +2979,10 @@ pub const Rolls = struct {
|
|
|
2888
2979
|
}
|
|
2889
2980
|
|
|
2890
2981
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2891
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2892
|
-
val
|
|
2893
|
-
else
|
|
2982
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2983
|
+
if (val >= 1 and val <= 8) val else null
|
|
2984
|
+
else
|
|
2985
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2894
2986
|
battle.rng.disableDuration(player)
|
|
2895
2987
|
else if (showdown)
|
|
2896
2988
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,9 +2995,10 @@ pub const Rolls = struct {
|
|
|
2903
2995
|
}
|
|
2904
2996
|
|
|
2905
2997
|
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
|
|
2998
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3000
|
+
else
|
|
3001
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
2909
3002
|
battle.rng.confusionDuration(player)
|
|
2910
3003
|
else if (showdown)
|
|
2911
3004
|
battle.rng.range(u3, 2, 6)
|
|
@@ -2918,9 +3011,10 @@ pub const Rolls = struct {
|
|
|
2918
3011
|
}
|
|
2919
3012
|
|
|
2920
3013
|
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
|
|
3014
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3015
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3016
|
+
else
|
|
3017
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2924
3018
|
battle.rng.attackingDuration(player)
|
|
2925
3019
|
else if (showdown)
|
|
2926
3020
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2941,9 +3035,10 @@ pub const Rolls = struct {
|
|
|
2941
3035
|
options: anytype,
|
|
2942
3036
|
) !u3 {
|
|
2943
3037
|
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
|
|
3038
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3039
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3040
|
+
else
|
|
3041
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2947
3042
|
battle.rng.distribution(player)
|
|
2948
3043
|
else if (showdown)
|
|
2949
3044
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2969,15 +3064,13 @@ pub const Rolls = struct {
|
|
|
2969
3064
|
options: anytype,
|
|
2970
3065
|
) !u4 {
|
|
2971
3066
|
// TODO: consider throwing error instead of rerolling?
|
|
2972
|
-
const
|
|
3067
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2973
3068
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2974
3069
|
val
|
|
2975
3070
|
else
|
|
2976
3071
|
null
|
|
2977
3072
|
else
|
|
2978
|
-
null
|
|
2979
|
-
|
|
2980
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3073
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2981
3074
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2982
3075
|
else slot: {
|
|
2983
3076
|
if (showdown) {
|
|
@@ -3135,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3135
3228
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3136
3229
|
if (n == before) {
|
|
3137
3230
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3138
|
-
if (!
|
|
3231
|
+
if (!pkmn.options.mod) {
|
|
3139
3232
|
while (slot <= 4) : (slot += 1) {
|
|
3140
3233
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3141
3234
|
}
|