@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.683fba11
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 -68
- 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 +52 -31
- package/src/bin/install-pkmn-engine +8 -6
- 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 +509 -152
- package/src/lib/gen1/chance.zig +366 -240
- 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 -554
- package/src/lib/gen1/mechanics.zig +280 -185
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +90 -503
- 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 +69 -107
- 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,37 +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
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
11
11
|
|
|
12
12
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
24
15
|
const Damage = protocol.Damage;
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
18
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
19
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
32
22
|
const Move = data.Move;
|
|
33
23
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
35
27
|
const Side = data.Side;
|
|
36
28
|
const Species = data.Species;
|
|
37
29
|
const Stats = data.Stats;
|
|
@@ -174,7 +166,10 @@ fn selectMove(
|
|
|
174
166
|
if (foe_choice.type == .Switch) {
|
|
175
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
176
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
177
|
-
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
|
+
}
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
175
|
return null;
|
|
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
248
|
active.moves = incoming.moves;
|
|
254
249
|
|
|
255
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
256
252
|
|
|
257
253
|
foe.active.volatiles.Binding = false;
|
|
258
254
|
|
|
259
255
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
256
|
|
|
262
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
258
|
incoming.status = Status.init(.PSN);
|
|
@@ -353,7 +348,7 @@ fn doTurn(
|
|
|
353
348
|
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
350
|
|
|
356
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
357
352
|
|
|
358
353
|
residual = true;
|
|
359
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -425,7 +420,7 @@ fn executeMove(
|
|
|
425
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
426
421
|
const stored = side.stored();
|
|
427
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
428
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
429
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
430
425
|
side.last_selected_move == .HyperBeam or
|
|
431
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
@@ -458,6 +453,25 @@ fn executeMove(
|
|
|
458
453
|
.err => return @as(?Result, Result.Error),
|
|
459
454
|
}
|
|
460
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
|
+
|
|
461
475
|
const can = skip_can or
|
|
462
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
463
477
|
if (!can) return null;
|
|
@@ -484,17 +498,22 @@ fn beforeMove(
|
|
|
484
498
|
|
|
485
499
|
if (Status.is(stored.status, .SLP)) {
|
|
486
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
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,
|
|
490
509
|
} else {
|
|
491
510
|
stored.status -= 1;
|
|
492
511
|
}
|
|
493
512
|
|
|
494
513
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
517
|
|
|
499
518
|
if (duration == 0) {
|
|
500
519
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +551,12 @@ fn beforeMove(
|
|
|
532
551
|
}
|
|
533
552
|
|
|
534
553
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
);
|
|
541
560
|
|
|
542
561
|
if (volatiles.disable_duration == 0) {
|
|
543
562
|
volatiles.disable_move = 0;
|
|
@@ -550,19 +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
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
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,
|
|
562
585
|
} else {
|
|
563
586
|
volatiles.confusion -= 1;
|
|
564
587
|
}
|
|
565
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
590
|
|
|
567
591
|
if (volatiles.confusion == 0) {
|
|
568
592
|
volatiles.Confusion = false;
|
|
@@ -580,7 +604,9 @@ fn beforeMove(
|
|
|
580
604
|
volatiles.Charging = false;
|
|
581
605
|
volatiles.Binding = false;
|
|
582
606
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
584
610
|
{
|
|
585
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,7 +614,9 @@ fn beforeMove(
|
|
|
588
614
|
const def = foe.active.stats.def;
|
|
589
615
|
foe.active.stats.def = active.stats.def;
|
|
590
616
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
592
620
|
}
|
|
593
621
|
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
622
|
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
@@ -617,7 +645,10 @@ fn beforeMove(
|
|
|
617
645
|
volatiles.Thrashing = false;
|
|
618
646
|
volatiles.Charging = false;
|
|
619
647
|
volatiles.Binding = false;
|
|
620
|
-
|
|
648
|
+
|
|
649
|
+
options.chance.observe(.attacking, player, .None);
|
|
650
|
+
options.chance.observe(.binding, player, .None);
|
|
651
|
+
|
|
621
652
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
653
|
try log.cant(.{ ident, .Paralysis });
|
|
623
654
|
return .done;
|
|
@@ -635,12 +666,8 @@ fn beforeMove(
|
|
|
635
666
|
volatiles.state +%= battle.last_damage;
|
|
636
667
|
}
|
|
637
668
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
669
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
670
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
671
|
|
|
645
672
|
if (volatiles.attacks != 0) {
|
|
646
673
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +701,40 @@ fn beforeMove(
|
|
|
674
701
|
}
|
|
675
702
|
|
|
676
703
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
704
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
705
|
+
|
|
706
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
707
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
683
708
|
|
|
684
|
-
if (
|
|
709
|
+
if (volatiles.attacks == 0) {
|
|
710
|
+
const overwritten = volatiles.Confusion;
|
|
711
|
+
|
|
712
|
+
volatiles.Thrashing = false;
|
|
713
|
+
volatiles.Confusion = true;
|
|
714
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
715
|
+
|
|
716
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
717
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
718
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
719
|
+
// correct player)
|
|
685
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);
|
|
686
726
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
727
|
+
|
|
728
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
729
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
730
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
731
|
+
|
|
696
732
|
return .skip_can;
|
|
697
733
|
}
|
|
698
734
|
|
|
699
735
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
736
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
737
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
738
|
|
|
707
739
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
740
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -756,9 +788,14 @@ fn canMove(
|
|
|
756
788
|
|
|
757
789
|
if (move.effect == .Thrashing) {
|
|
758
790
|
Effects.thrashing(battle, player, options);
|
|
759
|
-
} else if (
|
|
760
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
761
|
-
|
|
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
|
+
}
|
|
762
799
|
}
|
|
763
800
|
|
|
764
801
|
return true;
|
|
@@ -795,7 +832,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
832
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
833
|
}
|
|
797
834
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
835
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
836
|
var active = &side.active;
|
|
800
837
|
const volatiles = &active.volatiles;
|
|
801
838
|
|
|
@@ -803,8 +840,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
840
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
841
|
if (showdown and volatiles.Transform) return;
|
|
805
842
|
|
|
843
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
844
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
845
|
+
|
|
806
846
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
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;
|
|
808
848
|
}
|
|
809
849
|
|
|
810
850
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,7 +898,7 @@ fn doMove(
|
|
|
858
898
|
|
|
859
899
|
var late = showdown and move.effect != .Explode;
|
|
860
900
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
901
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
902
|
if (showdown and move.effect.isMulti()) {
|
|
863
903
|
try Effects.multiHit(battle, player, move, options);
|
|
864
904
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +920,9 @@ fn doMove(
|
|
|
880
920
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
921
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
922
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} 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);
|
|
885
926
|
return @as(?Result, Result.Error);
|
|
886
927
|
}
|
|
887
928
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +936,12 @@ fn doMove(
|
|
|
895
936
|
}
|
|
896
937
|
}
|
|
897
938
|
|
|
939
|
+
var missed: bool = undefined;
|
|
898
940
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
941
|
+
if (!showdown and !skip) {
|
|
942
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
943
|
+
miss = missed or zero;
|
|
944
|
+
}
|
|
903
945
|
|
|
904
946
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
947
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +968,32 @@ fn doMove(
|
|
|
926
968
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
969
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
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
|
+
}
|
|
929
992
|
} else if (mist) {
|
|
930
993
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
994
|
try log.fail(.{ foe_ident, .None });
|
|
932
995
|
} else {
|
|
933
|
-
if (!showdown
|
|
996
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
997
|
try log.lastmiss(.{});
|
|
935
998
|
try log.miss(.{battle.active(player)});
|
|
936
999
|
}
|
|
@@ -945,8 +1008,6 @@ fn doMove(
|
|
|
945
1008
|
try buildRage(battle, player.foe(), options);
|
|
946
1009
|
} else if (showdown and move.effect == .Disable) {
|
|
947
1010
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
1011
|
}
|
|
951
1012
|
return null;
|
|
952
1013
|
} else if (!showdown) {
|
|
@@ -994,22 +1055,20 @@ fn doMove(
|
|
|
994
1055
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1056
|
}
|
|
996
1057
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1058
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1059
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try buildRage(battle, player.foe(), options);
|
|
1010
|
-
}
|
|
1011
|
-
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);
|
|
1012
1070
|
}
|
|
1071
|
+
return null;
|
|
1013
1072
|
}
|
|
1014
1073
|
}
|
|
1015
1074
|
|
|
@@ -1069,6 +1128,7 @@ fn calcDamage(
|
|
|
1069
1128
|
target_player: Player,
|
|
1070
1129
|
m: ?Move.Data,
|
|
1071
1130
|
crit: bool,
|
|
1131
|
+
options: anytype,
|
|
1072
1132
|
) bool {
|
|
1073
1133
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1134
|
const cfz = m == null;
|
|
@@ -1080,28 +1140,8 @@ fn calcDamage(
|
|
|
1080
1140
|
|
|
1081
1141
|
const special = move.type.special();
|
|
1082
1142
|
|
|
1083
|
-
|
|
1084
|
-
var
|
|
1085
|
-
if (crit)
|
|
1086
|
-
if (special) side.stored().stats.spc
|
|
1087
|
-
else side.stored().stats.atk
|
|
1088
|
-
else
|
|
1089
|
-
if (special) side.active.stats.spc
|
|
1090
|
-
else side.active.stats.atk;
|
|
1091
|
-
var def: u32 =
|
|
1092
|
-
if (crit)
|
|
1093
|
-
if (special) target.stored().stats.spc
|
|
1094
|
-
else target.stored().stats.def
|
|
1095
|
-
else
|
|
1096
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1097
|
-
if (special)
|
|
1098
|
-
target.active.stats.spc *
|
|
1099
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1100
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1101
|
-
else
|
|
1102
|
-
target.active.stats.def *
|
|
1103
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1104
|
-
// zig fmt: on
|
|
1143
|
+
var atk = attack(side, crit, special);
|
|
1144
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1145
|
|
|
1106
1146
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1147
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1109,7 +1149,7 @@ fn calcDamage(
|
|
|
1109
1149
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1110
1150
|
atk = @max((atk / 4) & 255, 1);
|
|
1111
1151
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1112
|
-
def = @max((def / 4) & 255, if (
|
|
1152
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1113
1153
|
}
|
|
1114
1154
|
|
|
1115
1155
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1126,11 +1166,41 @@ fn calcDamage(
|
|
|
1126
1166
|
d = @min(997, d);
|
|
1127
1167
|
d += 2;
|
|
1128
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
|
+
|
|
1129
1179
|
battle.last_damage = @intCast(d);
|
|
1130
1180
|
|
|
1131
1181
|
return true;
|
|
1132
1182
|
}
|
|
1133
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
|
+
|
|
1134
1204
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1205
|
const side = battle.side(player);
|
|
1136
1206
|
const foe = battle.foe(player);
|
|
@@ -1185,13 +1255,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1185
1255
|
.Psywave => power: {
|
|
1186
1256
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1187
1257
|
// GLITCH: Psywave infinite glitch loop
|
|
1188
|
-
if (!
|
|
1258
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1189
1259
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1190
1260
|
},
|
|
1191
1261
|
else => unreachable,
|
|
1192
1262
|
};
|
|
1193
1263
|
|
|
1194
|
-
if (battle.last_damage == 0) return if (
|
|
1264
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1195
1265
|
|
|
1196
1266
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1197
1267
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1228,12 +1298,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1228
1298
|
|
|
1229
1299
|
if (!used or !selected) {
|
|
1230
1300
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1231
|
-
if (!
|
|
1301
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1232
1302
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1233
1303
|
return null;
|
|
1234
1304
|
}
|
|
1235
1305
|
|
|
1236
|
-
|
|
1306
|
+
// NOTE: llvm/llvm-project#58557
|
|
1307
|
+
if (comptime builtin.target.isWasm()) {
|
|
1308
|
+
battle.last_damage = @max(battle.last_damage *% 2, std.math.maxInt(u16));
|
|
1309
|
+
} else {
|
|
1310
|
+
battle.last_damage *|= 2;
|
|
1311
|
+
}
|
|
1237
1312
|
|
|
1238
1313
|
// Pokémon Showdown calls moveHit before Counter
|
|
1239
1314
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1265,7 +1340,7 @@ fn applyDamage(
|
|
|
1265
1340
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1341
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1342
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(sub_player);
|
|
1343
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1344
|
return true;
|
|
1270
1345
|
} else {
|
|
1271
1346
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1374,7 @@ fn mirrorMove(
|
|
|
1299
1374
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1375
|
return null;
|
|
1301
1376
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1377
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1378
|
}
|
|
1304
1379
|
|
|
1305
1380
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1395,7 @@ fn metronome(
|
|
|
1320
1395
|
var side = battle.side(player);
|
|
1321
1396
|
|
|
1322
1397
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1398
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1399
|
|
|
1325
1400
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1401
|
return null;
|
|
@@ -1421,7 +1496,10 @@ fn moveHit(
|
|
|
1421
1496
|
|
|
1422
1497
|
if (!miss) return true;
|
|
1423
1498
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1499
|
+
|
|
1424
1500
|
side.active.volatiles.Binding = false;
|
|
1501
|
+
options.chance.observe(.binding, player, .None);
|
|
1502
|
+
|
|
1425
1503
|
return false;
|
|
1426
1504
|
}
|
|
1427
1505
|
|
|
@@ -1472,7 +1550,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1472
1550
|
assert(!foe_volatiles.MultiHit);
|
|
1473
1551
|
if (foe_volatiles.Bide) {
|
|
1474
1552
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1475
|
-
foe_volatiles.state = if (
|
|
1553
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1476
1554
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1477
1555
|
}
|
|
1478
1556
|
|
|
@@ -1563,12 +1641,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1563
1641
|
|
|
1564
1642
|
battle.turn += 1;
|
|
1565
1643
|
|
|
1566
|
-
if (
|
|
1644
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1567
1645
|
try options.log.tie(.{});
|
|
1568
1646
|
return Result.Tie;
|
|
1569
1647
|
}
|
|
1570
1648
|
|
|
1571
|
-
if (
|
|
1649
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1572
1650
|
try options.log.tie(.{});
|
|
1573
1651
|
return Result.Tie;
|
|
1574
1652
|
} else if (battle.turn >= 65535) {
|
|
@@ -1659,17 +1737,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1737
|
}
|
|
1660
1738
|
}
|
|
1661
1739
|
|
|
1662
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1663
|
-
var volatiles = &active.volatiles;
|
|
1664
|
-
assert(volatiles.Thrashing);
|
|
1665
|
-
if (volatiles.attacks > 0) return false;
|
|
1666
|
-
|
|
1667
|
-
volatiles.Thrashing = false;
|
|
1668
|
-
volatiles.Confusion = true;
|
|
1669
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1670
|
-
return true;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
1740
|
fn onBegin(
|
|
1674
1741
|
battle: anytype,
|
|
1675
1742
|
player: Player,
|
|
@@ -1756,8 +1823,10 @@ pub const Effects = struct {
|
|
|
1756
1823
|
|
|
1757
1824
|
side.active.volatiles.Bide = true;
|
|
1758
1825
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1826
|
+
|
|
1759
1827
|
side.active.volatiles.state = 0;
|
|
1760
1828
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1829
|
+
options.chance.observe(.attacking, player, .started);
|
|
1761
1830
|
|
|
1762
1831
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1763
1832
|
}
|
|
@@ -1823,7 +1892,10 @@ pub const Effects = struct {
|
|
|
1823
1892
|
|
|
1824
1893
|
if (foe.active.volatiles.Confusion) return;
|
|
1825
1894
|
foe.active.volatiles.Confusion = true;
|
|
1826
|
-
|
|
1895
|
+
|
|
1896
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1897
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1898
|
+
options.calc.confusion(player.foe());
|
|
1827
1899
|
|
|
1828
1900
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1829
1901
|
}
|
|
@@ -1869,18 +1941,18 @@ pub const Effects = struct {
|
|
|
1869
1941
|
|
|
1870
1942
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1871
1943
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1872
|
-
if (n == 0) {
|
|
1944
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1873
1945
|
try options.log.fail(.{ foe_ident, .None });
|
|
1874
1946
|
return null;
|
|
1875
1947
|
} else if (err) {
|
|
1876
1948
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1877
|
-
assert(!showdown);
|
|
1878
1949
|
return Result.Error;
|
|
1879
1950
|
}
|
|
1880
1951
|
|
|
1881
1952
|
volatiles.disable_move =
|
|
1882
1953
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1883
1954
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1955
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1884
1956
|
|
|
1885
1957
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1886
1958
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1951,7 +2023,7 @@ pub const Effects = struct {
|
|
|
1951
2023
|
if (foe.active.types.includes(move.type)) return;
|
|
1952
2024
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1953
2025
|
// Freeze Clause Mod
|
|
1954
|
-
if (
|
|
2026
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1955
2027
|
|
|
1956
2028
|
foe_stored.status = Status.init(.FRZ);
|
|
1957
2029
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1987,22 +2059,24 @@ pub const Effects = struct {
|
|
|
1987
2059
|
if (p != player and Status.any(s.stored().status)) {
|
|
1988
2060
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1989
2061
|
s.stored().status = 0;
|
|
2062
|
+
options.chance.observe(.sleep, p, .None);
|
|
1990
2063
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1991
2064
|
s.stored().status = Status.init(.PSN);
|
|
1992
2065
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1993
2066
|
}
|
|
1994
|
-
try clearVolatiles(battle, p,
|
|
2067
|
+
try clearVolatiles(battle, p, options);
|
|
1995
2068
|
}
|
|
1996
2069
|
} else {
|
|
1997
2070
|
if (Status.any(foe_stored.status)) {
|
|
1998
2071
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1999
2072
|
foe.last_selected_move = .SKIP_TURN;
|
|
2073
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2000
2074
|
}
|
|
2001
2075
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2002
2076
|
foe_stored.status = 0;
|
|
2003
2077
|
}
|
|
2004
|
-
try clearVolatiles(battle, player,
|
|
2005
|
-
try clearVolatiles(battle, player.foe(),
|
|
2078
|
+
try clearVolatiles(battle, player, options);
|
|
2079
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2006
2080
|
}
|
|
2007
2081
|
}
|
|
2008
2082
|
|
|
@@ -2290,7 +2364,7 @@ pub const Effects = struct {
|
|
|
2290
2364
|
}
|
|
2291
2365
|
|
|
2292
2366
|
// Sleep Clause Mod
|
|
2293
|
-
if (
|
|
2367
|
+
if (pkmn.options.mod) {
|
|
2294
2368
|
for (foe.pokemon) |p| {
|
|
2295
2369
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2296
2370
|
}
|
|
@@ -2298,6 +2372,8 @@ pub const Effects = struct {
|
|
|
2298
2372
|
foe.active.volatiles.Recharging = false;
|
|
2299
2373
|
|
|
2300
2374
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2375
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2376
|
+
|
|
2301
2377
|
const last = battle.side(player).last_selected_move;
|
|
2302
2378
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2303
2379
|
}
|
|
@@ -2354,6 +2430,8 @@ pub const Effects = struct {
|
|
|
2354
2430
|
|
|
2355
2431
|
volatiles.Thrashing = true;
|
|
2356
2432
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2433
|
+
|
|
2434
|
+
options.chance.observe(.attacking, player, .started);
|
|
2357
2435
|
}
|
|
2358
2436
|
|
|
2359
2437
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2392,13 +2470,14 @@ pub const Effects = struct {
|
|
|
2392
2470
|
if (side.active.volatiles.Binding) return;
|
|
2393
2471
|
side.active.volatiles.Binding = true;
|
|
2394
2472
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2395
|
-
// (Pokémon Showdown
|
|
2473
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2396
2474
|
if (!showdown) {
|
|
2397
2475
|
foe.active.volatiles.Recharging = false;
|
|
2398
2476
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2399
2477
|
|
|
2400
2478
|
side.active.volatiles.attacks =
|
|
2401
2479
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2480
|
+
options.chance.observe(.binding, player, .started);
|
|
2402
2481
|
}
|
|
2403
2482
|
|
|
2404
2483
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2628,22 +2707,22 @@ fn isForced(active: anytype) bool {
|
|
|
2628
2707
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2629
2708
|
}
|
|
2630
2709
|
|
|
2631
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2710
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2632
2711
|
var log = options.log;
|
|
2633
2712
|
var side = battle.side(who);
|
|
2634
2713
|
var volatiles = &side.active.volatiles;
|
|
2635
2714
|
const ident = battle.active(who);
|
|
2636
2715
|
|
|
2637
|
-
options.chance.clearDurations(who, clear);
|
|
2638
|
-
|
|
2639
2716
|
if (volatiles.disable_move != 0) {
|
|
2640
2717
|
volatiles.disable_move = 0;
|
|
2641
2718
|
volatiles.disable_duration = 0;
|
|
2719
|
+
options.chance.observe(.disable, who, .None);
|
|
2642
2720
|
try log.end(.{ ident, .DisableSilent });
|
|
2643
2721
|
}
|
|
2644
2722
|
if (volatiles.Confusion) {
|
|
2645
2723
|
// volatiles.confusion is left unchanged
|
|
2646
2724
|
volatiles.Confusion = false;
|
|
2725
|
+
options.chance.observe(.confusion, who, .None);
|
|
2647
2726
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2648
2727
|
}
|
|
2649
2728
|
if (volatiles.Mist) {
|
|
@@ -2678,6 +2757,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2678
2757
|
}
|
|
2679
2758
|
}
|
|
2680
2759
|
|
|
2760
|
+
fn decrement(
|
|
2761
|
+
comptime field: chance.Action.Field,
|
|
2762
|
+
player: Player,
|
|
2763
|
+
options: anytype,
|
|
2764
|
+
n: anytype,
|
|
2765
|
+
) @TypeOf(n) {
|
|
2766
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2767
|
+
.started, .ended => 0,
|
|
2768
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2769
|
+
else => unreachable,
|
|
2770
|
+
} else n - 1;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2681
2773
|
pub const Rolls = struct {
|
|
2682
2774
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2683
2775
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2875,9 +2967,10 @@ pub const Rolls = struct {
|
|
|
2875
2967
|
}
|
|
2876
2968
|
|
|
2877
2969
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2878
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2879
|
-
@intCast(val)
|
|
2880
|
-
else
|
|
2970
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2971
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2972
|
+
else
|
|
2973
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2881
2974
|
battle.rng.sleepDuration(player)
|
|
2882
2975
|
else if (showdown)
|
|
2883
2976
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2889,14 +2982,15 @@ pub const Rolls = struct {
|
|
|
2889
2982
|
};
|
|
2890
2983
|
|
|
2891
2984
|
assert(duration >= 1 and duration <= 7);
|
|
2892
|
-
options.chance.duration(
|
|
2985
|
+
options.chance.duration(player, duration);
|
|
2893
2986
|
return duration;
|
|
2894
2987
|
}
|
|
2895
2988
|
|
|
2896
2989
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2897
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2898
|
-
val
|
|
2899
|
-
else
|
|
2990
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2991
|
+
if (val >= 1 and val <= 8) val else null
|
|
2992
|
+
else
|
|
2993
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2900
2994
|
battle.rng.disableDuration(player)
|
|
2901
2995
|
else if (showdown)
|
|
2902
2996
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2904,29 +2998,31 @@ pub const Rolls = struct {
|
|
|
2904
2998
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2905
2999
|
|
|
2906
3000
|
assert(duration >= 1 and duration <= 8);
|
|
2907
|
-
options.chance.duration(
|
|
3001
|
+
options.chance.duration(player, duration);
|
|
2908
3002
|
return duration;
|
|
2909
3003
|
}
|
|
2910
3004
|
|
|
2911
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2912
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2913
|
-
@intCast(val)
|
|
2914
|
-
else
|
|
2915
|
-
battle.rng
|
|
3005
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3006
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3007
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3008
|
+
else
|
|
3009
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3010
|
+
battle.rng.confusionDuration(player)
|
|
2916
3011
|
else if (showdown)
|
|
2917
3012
|
battle.rng.range(u3, 2, 6)
|
|
2918
3013
|
else
|
|
2919
3014
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2920
3015
|
|
|
2921
3016
|
assert(duration >= 2 and duration <= 5);
|
|
2922
|
-
options.chance.duration(
|
|
3017
|
+
options.chance.duration(player, duration);
|
|
2923
3018
|
return duration;
|
|
2924
3019
|
}
|
|
2925
3020
|
|
|
2926
3021
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2927
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2928
|
-
@intCast(val)
|
|
2929
|
-
else
|
|
3022
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3023
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3024
|
+
else
|
|
3025
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2930
3026
|
battle.rng.attackingDuration(player)
|
|
2931
3027
|
else if (showdown)
|
|
2932
3028
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2934,7 +3030,7 @@ pub const Rolls = struct {
|
|
|
2934
3030
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2935
3031
|
|
|
2936
3032
|
assert(duration >= 2 and duration <= 3);
|
|
2937
|
-
options.chance.duration(
|
|
3033
|
+
options.chance.duration(player, duration);
|
|
2938
3034
|
return duration;
|
|
2939
3035
|
}
|
|
2940
3036
|
|
|
@@ -2947,9 +3043,10 @@ pub const Rolls = struct {
|
|
|
2947
3043
|
options: anytype,
|
|
2948
3044
|
) !u3 {
|
|
2949
3045
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2950
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2951
|
-
@intCast(val)
|
|
2952
|
-
else
|
|
3046
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3047
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3048
|
+
else
|
|
3049
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2953
3050
|
battle.rng.distribution(player)
|
|
2954
3051
|
else if (showdown)
|
|
2955
3052
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2960,7 +3057,7 @@ pub const Rolls = struct {
|
|
|
2960
3057
|
|
|
2961
3058
|
assert(n >= 2 and n <= 5);
|
|
2962
3059
|
if (effect == .Binding) {
|
|
2963
|
-
options.chance.duration(
|
|
3060
|
+
options.chance.duration(player, n);
|
|
2964
3061
|
} else {
|
|
2965
3062
|
try options.chance.multiHit(player, n);
|
|
2966
3063
|
}
|
|
@@ -2975,15 +3072,13 @@ pub const Rolls = struct {
|
|
|
2975
3072
|
options: anytype,
|
|
2976
3073
|
) !u4 {
|
|
2977
3074
|
// TODO: consider throwing error instead of rerolling?
|
|
2978
|
-
const
|
|
3075
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2979
3076
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2980
3077
|
val
|
|
2981
3078
|
else
|
|
2982
3079
|
null
|
|
2983
3080
|
else
|
|
2984
|
-
null
|
|
2985
|
-
|
|
2986
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3081
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2987
3082
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2988
3083
|
else slot: {
|
|
2989
3084
|
if (showdown) {
|
|
@@ -3141,7 +3236,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3141
3236
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3142
3237
|
if (n == before) {
|
|
3143
3238
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3144
|
-
if (!
|
|
3239
|
+
if (!pkmn.options.mod) {
|
|
3145
3240
|
while (slot <= 4) : (slot += 1) {
|
|
3146
3241
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3147
3242
|
}
|