@pkmn/engine 0.1.0-dev.deda8593 → 0.1.0-dev.e0a786f8
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 +323 -157
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +60 -1
- package/src/lib/gen1/calc.zig +527 -161
- 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 -193
- 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 +88 -115
- 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,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(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
|
+
|
|
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,19 +614,14 @@ 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
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
595
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
596
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
597
|
-
player
|
|
598
|
-
else
|
|
599
|
-
player.foe();
|
|
600
621
|
|
|
601
622
|
const uncapped = battle.last_damage;
|
|
602
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
603
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
604
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
605
626
|
if (showdown) battle.last_damage = uncapped;
|
|
606
627
|
|
|
@@ -617,7 +638,10 @@ fn beforeMove(
|
|
|
617
638
|
volatiles.Thrashing = false;
|
|
618
639
|
volatiles.Charging = false;
|
|
619
640
|
volatiles.Binding = false;
|
|
620
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
621
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
646
|
try log.cant(.{ ident, .Paralysis });
|
|
623
647
|
return .done;
|
|
@@ -635,12 +659,8 @@ fn beforeMove(
|
|
|
635
659
|
volatiles.state +%= battle.last_damage;
|
|
636
660
|
}
|
|
637
661
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
664
|
|
|
645
665
|
if (volatiles.attacks != 0) {
|
|
646
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +694,40 @@ fn beforeMove(
|
|
|
674
694
|
}
|
|
675
695
|
|
|
676
696
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
698
|
+
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
701
|
+
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
683
708
|
|
|
684
|
-
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
685
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);
|
|
686
719
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
696
725
|
return .skip_can;
|
|
697
726
|
}
|
|
698
727
|
|
|
699
728
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
731
|
|
|
707
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -756,9 +781,14 @@ fn canMove(
|
|
|
756
781
|
|
|
757
782
|
if (move.effect == .Thrashing) {
|
|
758
783
|
Effects.thrashing(battle, player, options);
|
|
759
|
-
} else if (
|
|
760
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
761
|
-
|
|
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
|
+
}
|
|
762
792
|
}
|
|
763
793
|
|
|
764
794
|
return true;
|
|
@@ -795,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
826
|
}
|
|
797
827
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
829
|
var active = &side.active;
|
|
800
830
|
const volatiles = &active.volatiles;
|
|
801
831
|
|
|
@@ -803,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
834
|
if (showdown and volatiles.Transform) return;
|
|
805
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
806
839
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
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;
|
|
808
841
|
}
|
|
809
842
|
|
|
810
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,7 +891,7 @@ fn doMove(
|
|
|
858
891
|
|
|
859
892
|
var late = showdown and move.effect != .Explode;
|
|
860
893
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
895
|
if (showdown and move.effect.isMulti()) {
|
|
863
896
|
try Effects.multiHit(battle, player, move, options);
|
|
864
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +913,9 @@ fn doMove(
|
|
|
880
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} 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);
|
|
885
919
|
return @as(?Result, Result.Error);
|
|
886
920
|
}
|
|
887
921
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +929,12 @@ fn doMove(
|
|
|
895
929
|
}
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
var missed: bool = undefined;
|
|
898
933
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
903
938
|
|
|
904
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +961,32 @@ fn doMove(
|
|
|
926
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
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
|
+
}
|
|
929
985
|
} else if (mist) {
|
|
930
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
987
|
try log.fail(.{ foe_ident, .None });
|
|
932
988
|
} else {
|
|
933
|
-
if (!showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
990
|
try log.lastmiss(.{});
|
|
935
991
|
try log.miss(.{battle.active(player)});
|
|
936
992
|
}
|
|
@@ -945,8 +1001,6 @@ fn doMove(
|
|
|
945
1001
|
try buildRage(battle, player.foe(), options);
|
|
946
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
947
1003
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
1004
|
}
|
|
951
1005
|
return null;
|
|
952
1006
|
} else if (!showdown) {
|
|
@@ -994,22 +1048,20 @@ fn doMove(
|
|
|
994
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1049
|
}
|
|
996
1050
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1052
|
// 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;
|
|
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);
|
|
1012
1063
|
}
|
|
1064
|
+
return null;
|
|
1013
1065
|
}
|
|
1014
1066
|
}
|
|
1015
1067
|
|
|
@@ -1069,6 +1121,7 @@ fn calcDamage(
|
|
|
1069
1121
|
target_player: Player,
|
|
1070
1122
|
m: ?Move.Data,
|
|
1071
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1072
1125
|
) bool {
|
|
1073
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1074
1127
|
const cfz = m == null;
|
|
@@ -1080,28 +1133,8 @@ fn calcDamage(
|
|
|
1080
1133
|
|
|
1081
1134
|
const special = move.type.special();
|
|
1082
1135
|
|
|
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
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1105
1138
|
|
|
1106
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1107
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1109,7 +1142,7 @@ fn calcDamage(
|
|
|
1109
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1110
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1111
1144
|
// 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 (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1113
1146
|
}
|
|
1114
1147
|
|
|
1115
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1126,11 +1159,41 @@ fn calcDamage(
|
|
|
1126
1159
|
d = @min(997, d);
|
|
1127
1160
|
d += 2;
|
|
1128
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
|
+
|
|
1129
1172
|
battle.last_damage = @intCast(d);
|
|
1130
1173
|
|
|
1131
1174
|
return true;
|
|
1132
1175
|
}
|
|
1133
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
|
+
|
|
1134
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1135
1198
|
const side = battle.side(player);
|
|
1136
1199
|
const foe = battle.foe(player);
|
|
@@ -1185,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1185
1248
|
.Psywave => power: {
|
|
1186
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1187
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1188
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1189
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1190
1253
|
},
|
|
1191
1254
|
else => unreachable,
|
|
1192
1255
|
};
|
|
1193
1256
|
|
|
1194
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1195
1258
|
|
|
1196
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1197
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1228,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1228
1291
|
|
|
1229
1292
|
if (!used or !selected) {
|
|
1230
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1231
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1232
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1233
1296
|
return null;
|
|
1234
1297
|
}
|
|
1235
1298
|
|
|
1236
|
-
|
|
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;
|
|
1237
1304
|
|
|
1238
1305
|
// Pokémon Showdown calls moveHit before Counter
|
|
1239
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1265,7 +1332,7 @@ fn applyDamage(
|
|
|
1265
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(sub_player);
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1336
|
return true;
|
|
1270
1337
|
} else {
|
|
1271
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1366,7 @@ fn mirrorMove(
|
|
|
1299
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1367
|
return null;
|
|
1301
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1370
|
}
|
|
1304
1371
|
|
|
1305
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1387,7 @@ fn metronome(
|
|
|
1320
1387
|
var side = battle.side(player);
|
|
1321
1388
|
|
|
1322
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1391
|
|
|
1325
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1393
|
return null;
|
|
@@ -1421,7 +1488,10 @@ fn moveHit(
|
|
|
1421
1488
|
|
|
1422
1489
|
if (!miss) return true;
|
|
1423
1490
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1491
|
+
|
|
1424
1492
|
side.active.volatiles.Binding = false;
|
|
1493
|
+
options.chance.observe(.binding, player, .None);
|
|
1494
|
+
|
|
1425
1495
|
return false;
|
|
1426
1496
|
}
|
|
1427
1497
|
|
|
@@ -1472,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
|
|
|
1472
1542
|
assert(!foe_volatiles.MultiHit);
|
|
1473
1543
|
if (foe_volatiles.Bide) {
|
|
1474
1544
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1475
|
-
foe_volatiles.state = if (
|
|
1545
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1476
1546
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1477
1547
|
}
|
|
1478
1548
|
|
|
@@ -1563,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1563
1633
|
|
|
1564
1634
|
battle.turn += 1;
|
|
1565
1635
|
|
|
1566
|
-
if (
|
|
1636
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1567
1637
|
try options.log.tie(.{});
|
|
1568
1638
|
return Result.Tie;
|
|
1569
1639
|
}
|
|
1570
1640
|
|
|
1571
|
-
if (
|
|
1641
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1572
1642
|
try options.log.tie(.{});
|
|
1573
1643
|
return Result.Tie;
|
|
1574
1644
|
} else if (battle.turn >= 65535) {
|
|
@@ -1659,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1729
|
}
|
|
1660
1730
|
}
|
|
1661
1731
|
|
|
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
1732
|
fn onBegin(
|
|
1674
1733
|
battle: anytype,
|
|
1675
1734
|
player: Player,
|
|
@@ -1756,8 +1815,10 @@ pub const Effects = struct {
|
|
|
1756
1815
|
|
|
1757
1816
|
side.active.volatiles.Bide = true;
|
|
1758
1817
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1818
|
+
|
|
1759
1819
|
side.active.volatiles.state = 0;
|
|
1760
1820
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1821
|
+
options.chance.observe(.attacking, player, .started);
|
|
1761
1822
|
|
|
1762
1823
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1763
1824
|
}
|
|
@@ -1823,7 +1884,10 @@ pub const Effects = struct {
|
|
|
1823
1884
|
|
|
1824
1885
|
if (foe.active.volatiles.Confusion) return;
|
|
1825
1886
|
foe.active.volatiles.Confusion = true;
|
|
1826
|
-
|
|
1887
|
+
|
|
1888
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1889
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1890
|
+
options.calc.confusion(player.foe());
|
|
1827
1891
|
|
|
1828
1892
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1829
1893
|
}
|
|
@@ -1869,18 +1933,18 @@ pub const Effects = struct {
|
|
|
1869
1933
|
|
|
1870
1934
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1871
1935
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1872
|
-
if (n == 0) {
|
|
1936
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1873
1937
|
try options.log.fail(.{ foe_ident, .None });
|
|
1874
1938
|
return null;
|
|
1875
1939
|
} else if (err) {
|
|
1876
1940
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1877
|
-
assert(!showdown);
|
|
1878
1941
|
return Result.Error;
|
|
1879
1942
|
}
|
|
1880
1943
|
|
|
1881
1944
|
volatiles.disable_move =
|
|
1882
1945
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1883
1946
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1947
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1884
1948
|
|
|
1885
1949
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1886
1950
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1951,7 +2015,7 @@ pub const Effects = struct {
|
|
|
1951
2015
|
if (foe.active.types.includes(move.type)) return;
|
|
1952
2016
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1953
2017
|
// Freeze Clause Mod
|
|
1954
|
-
if (
|
|
2018
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1955
2019
|
|
|
1956
2020
|
foe_stored.status = Status.init(.FRZ);
|
|
1957
2021
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1987,22 +2051,24 @@ pub const Effects = struct {
|
|
|
1987
2051
|
if (p != player and Status.any(s.stored().status)) {
|
|
1988
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1989
2053
|
s.stored().status = 0;
|
|
2054
|
+
options.chance.observe(.sleep, p, .None);
|
|
1990
2055
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1991
2056
|
s.stored().status = Status.init(.PSN);
|
|
1992
2057
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1993
2058
|
}
|
|
1994
|
-
try clearVolatiles(battle, p,
|
|
2059
|
+
try clearVolatiles(battle, p, options);
|
|
1995
2060
|
}
|
|
1996
2061
|
} else {
|
|
1997
2062
|
if (Status.any(foe_stored.status)) {
|
|
1998
2063
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1999
2064
|
foe.last_selected_move = .SKIP_TURN;
|
|
2065
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2000
2066
|
}
|
|
2001
2067
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2002
2068
|
foe_stored.status = 0;
|
|
2003
2069
|
}
|
|
2004
|
-
try clearVolatiles(battle, player,
|
|
2005
|
-
try clearVolatiles(battle, player.foe(),
|
|
2070
|
+
try clearVolatiles(battle, player, options);
|
|
2071
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2006
2072
|
}
|
|
2007
2073
|
}
|
|
2008
2074
|
|
|
@@ -2290,7 +2356,7 @@ pub const Effects = struct {
|
|
|
2290
2356
|
}
|
|
2291
2357
|
|
|
2292
2358
|
// Sleep Clause Mod
|
|
2293
|
-
if (
|
|
2359
|
+
if (pkmn.options.mod) {
|
|
2294
2360
|
for (foe.pokemon) |p| {
|
|
2295
2361
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2296
2362
|
}
|
|
@@ -2298,6 +2364,8 @@ pub const Effects = struct {
|
|
|
2298
2364
|
foe.active.volatiles.Recharging = false;
|
|
2299
2365
|
|
|
2300
2366
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2367
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2368
|
+
|
|
2301
2369
|
const last = battle.side(player).last_selected_move;
|
|
2302
2370
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2303
2371
|
}
|
|
@@ -2354,6 +2422,8 @@ pub const Effects = struct {
|
|
|
2354
2422
|
|
|
2355
2423
|
volatiles.Thrashing = true;
|
|
2356
2424
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2425
|
+
|
|
2426
|
+
options.chance.observe(.attacking, player, .started);
|
|
2357
2427
|
}
|
|
2358
2428
|
|
|
2359
2429
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2392,13 +2462,14 @@ pub const Effects = struct {
|
|
|
2392
2462
|
if (side.active.volatiles.Binding) return;
|
|
2393
2463
|
side.active.volatiles.Binding = true;
|
|
2394
2464
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2395
|
-
// (Pokémon Showdown
|
|
2465
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2396
2466
|
if (!showdown) {
|
|
2397
2467
|
foe.active.volatiles.Recharging = false;
|
|
2398
2468
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2399
2469
|
|
|
2400
2470
|
side.active.volatiles.attacks =
|
|
2401
2471
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2472
|
+
options.chance.observe(.binding, player, .started);
|
|
2402
2473
|
}
|
|
2403
2474
|
|
|
2404
2475
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2628,22 +2699,22 @@ fn isForced(active: anytype) bool {
|
|
|
2628
2699
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2629
2700
|
}
|
|
2630
2701
|
|
|
2631
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2702
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2632
2703
|
var log = options.log;
|
|
2633
2704
|
var side = battle.side(who);
|
|
2634
2705
|
var volatiles = &side.active.volatiles;
|
|
2635
2706
|
const ident = battle.active(who);
|
|
2636
2707
|
|
|
2637
|
-
options.chance.clearDurations(who, clear);
|
|
2638
|
-
|
|
2639
2708
|
if (volatiles.disable_move != 0) {
|
|
2640
2709
|
volatiles.disable_move = 0;
|
|
2641
2710
|
volatiles.disable_duration = 0;
|
|
2711
|
+
options.chance.observe(.disable, who, .None);
|
|
2642
2712
|
try log.end(.{ ident, .DisableSilent });
|
|
2643
2713
|
}
|
|
2644
2714
|
if (volatiles.Confusion) {
|
|
2645
2715
|
// volatiles.confusion is left unchanged
|
|
2646
2716
|
volatiles.Confusion = false;
|
|
2717
|
+
options.chance.observe(.confusion, who, .None);
|
|
2647
2718
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2648
2719
|
}
|
|
2649
2720
|
if (volatiles.Mist) {
|
|
@@ -2678,6 +2749,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2678
2749
|
}
|
|
2679
2750
|
}
|
|
2680
2751
|
|
|
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;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2681
2765
|
pub const Rolls = struct {
|
|
2682
2766
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2683
2767
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2875,9 +2959,10 @@ pub const Rolls = struct {
|
|
|
2875
2959
|
}
|
|
2876
2960
|
|
|
2877
2961
|
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
|
|
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"))
|
|
2881
2966
|
battle.rng.sleepDuration(player)
|
|
2882
2967
|
else if (showdown)
|
|
2883
2968
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2889,14 +2974,15 @@ pub const Rolls = struct {
|
|
|
2889
2974
|
};
|
|
2890
2975
|
|
|
2891
2976
|
assert(duration >= 1 and duration <= 7);
|
|
2892
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2893
2978
|
return duration;
|
|
2894
2979
|
}
|
|
2895
2980
|
|
|
2896
2981
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2897
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2898
|
-
val
|
|
2899
|
-
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"))
|
|
2900
2986
|
battle.rng.disableDuration(player)
|
|
2901
2987
|
else if (showdown)
|
|
2902
2988
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2904,29 +2990,31 @@ pub const Rolls = struct {
|
|
|
2904
2990
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2905
2991
|
|
|
2906
2992
|
assert(duration >= 1 and duration <= 8);
|
|
2907
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2908
2994
|
return duration;
|
|
2909
2995
|
}
|
|
2910
2996
|
|
|
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
|
|
2997
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
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"))
|
|
3002
|
+
battle.rng.confusionDuration(player)
|
|
2916
3003
|
else if (showdown)
|
|
2917
3004
|
battle.rng.range(u3, 2, 6)
|
|
2918
3005
|
else
|
|
2919
3006
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2920
3007
|
|
|
2921
3008
|
assert(duration >= 2 and duration <= 5);
|
|
2922
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2923
3010
|
return duration;
|
|
2924
3011
|
}
|
|
2925
3012
|
|
|
2926
3013
|
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
|
|
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"))
|
|
2930
3018
|
battle.rng.attackingDuration(player)
|
|
2931
3019
|
else if (showdown)
|
|
2932
3020
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2934,7 +3022,7 @@ pub const Rolls = struct {
|
|
|
2934
3022
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2935
3023
|
|
|
2936
3024
|
assert(duration >= 2 and duration <= 3);
|
|
2937
|
-
options.chance.duration(
|
|
3025
|
+
options.chance.duration(player, duration);
|
|
2938
3026
|
return duration;
|
|
2939
3027
|
}
|
|
2940
3028
|
|
|
@@ -2947,9 +3035,10 @@ pub const Rolls = struct {
|
|
|
2947
3035
|
options: anytype,
|
|
2948
3036
|
) !u3 {
|
|
2949
3037
|
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
|
|
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"))
|
|
2953
3042
|
battle.rng.distribution(player)
|
|
2954
3043
|
else if (showdown)
|
|
2955
3044
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2960,7 +3049,7 @@ pub const Rolls = struct {
|
|
|
2960
3049
|
|
|
2961
3050
|
assert(n >= 2 and n <= 5);
|
|
2962
3051
|
if (effect == .Binding) {
|
|
2963
|
-
options.chance.duration(
|
|
3052
|
+
options.chance.duration(player, n);
|
|
2964
3053
|
} else {
|
|
2965
3054
|
try options.chance.multiHit(player, n);
|
|
2966
3055
|
}
|
|
@@ -2975,15 +3064,13 @@ pub const Rolls = struct {
|
|
|
2975
3064
|
options: anytype,
|
|
2976
3065
|
) !u4 {
|
|
2977
3066
|
// TODO: consider throwing error instead of rerolling?
|
|
2978
|
-
const
|
|
3067
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2979
3068
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2980
3069
|
val
|
|
2981
3070
|
else
|
|
2982
3071
|
null
|
|
2983
3072
|
else
|
|
2984
|
-
null
|
|
2985
|
-
|
|
2986
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3073
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2987
3074
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2988
3075
|
else slot: {
|
|
2989
3076
|
if (showdown) {
|
|
@@ -3141,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3141
3228
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3142
3229
|
if (n == before) {
|
|
3143
3230
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3144
|
-
if (!
|
|
3231
|
+
if (!pkmn.options.mod) {
|
|
3145
3232
|
while (slot <= 4) : (slot += 1) {
|
|
3146
3233
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3147
3234
|
}
|