@pkmn/engine 0.1.0-dev.15f64810 → 0.1.0-dev.1d311928
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 +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 +13 -17
- package/build/pkg/gen1.js +6 -10
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +38 -90
- package/build/pkg/index.js +6 -63
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +35 -69
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +141 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +142 -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 +256 -77
- package/package.json +52 -134
- package/src/bin/install-pkmn-engine +9 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/node.zig +121 -0
- package/src/lib/bindings/wasm.zig +52 -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 +120 -27
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +50 -1
- package/src/lib/gen1/calc.zig +610 -186
- 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 -560
- package/src/lib/gen1/mechanics.zig +290 -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 +18 -9
- 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);
|
|
@@ -347,13 +342,13 @@ fn doTurn(
|
|
|
347
342
|
)) |r| return r;
|
|
348
343
|
if (!replace) {
|
|
349
344
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
346
|
}
|
|
352
347
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
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;
|
|
@@ -369,10 +364,10 @@ fn doTurn(
|
|
|
369
364
|
)) |r| return r;
|
|
370
365
|
if (!replace) {
|
|
371
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
368
|
}
|
|
374
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
371
|
}
|
|
377
372
|
|
|
378
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -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,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
|
-
|
|
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);
|
|
708
|
+
|
|
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);
|
|
683
715
|
|
|
684
|
-
|
|
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 and move.effect != .Explode) {
|
|
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,16 @@ 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
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1308
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1309
|
+
else
|
|
1310
|
+
battle.last_damage *| 2;
|
|
1237
1311
|
|
|
1238
1312
|
// Pokémon Showdown calls moveHit before Counter
|
|
1239
1313
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
@@ -1265,7 +1339,7 @@ fn applyDamage(
|
|
|
1265
1339
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1340
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1341
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1342
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1343
|
return true;
|
|
1270
1344
|
} else {
|
|
1271
1345
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1373,7 @@ fn mirrorMove(
|
|
|
1299
1373
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1374
|
return null;
|
|
1301
1375
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1376
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1377
|
}
|
|
1304
1378
|
|
|
1305
1379
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1394,7 @@ fn metronome(
|
|
|
1320
1394
|
var side = battle.side(player);
|
|
1321
1395
|
|
|
1322
1396
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1397
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1398
|
|
|
1325
1399
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1400
|
return null;
|
|
@@ -1421,13 +1495,17 @@ fn moveHit(
|
|
|
1421
1495
|
|
|
1422
1496
|
if (!miss) return true;
|
|
1423
1497
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1498
|
+
|
|
1424
1499
|
side.active.volatiles.Binding = false;
|
|
1500
|
+
options.chance.observe(.binding, player, .None);
|
|
1501
|
+
|
|
1425
1502
|
return false;
|
|
1426
1503
|
}
|
|
1427
1504
|
|
|
1428
1505
|
fn checkFaint(
|
|
1429
1506
|
battle: anytype,
|
|
1430
1507
|
player: Player,
|
|
1508
|
+
cap: bool,
|
|
1431
1509
|
options: anytype,
|
|
1432
1510
|
) @TypeOf(options.log).Error!?Result {
|
|
1433
1511
|
const side = battle.side(player);
|
|
@@ -1440,8 +1518,8 @@ fn checkFaint(
|
|
|
1440
1518
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1441
1519
|
const more = player_out or foe_out;
|
|
1442
1520
|
|
|
1443
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1444
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1521
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1522
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1445
1523
|
|
|
1446
1524
|
assert(!side.active.volatiles.MultiHit);
|
|
1447
1525
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1462,7 +1540,7 @@ fn checkFaint(
|
|
|
1462
1540
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1463
1541
|
}
|
|
1464
1542
|
|
|
1465
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1543
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1466
1544
|
var side = battle.side(player);
|
|
1467
1545
|
var foe = battle.foe(player);
|
|
1468
1546
|
assert(side.stored().hp == 0);
|
|
@@ -1471,7 +1549,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1471
1549
|
assert(!foe_volatiles.MultiHit);
|
|
1472
1550
|
if (foe_volatiles.Bide) {
|
|
1473
1551
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1474
|
-
foe_volatiles.state = if (
|
|
1552
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1475
1553
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1476
1554
|
}
|
|
1477
1555
|
|
|
@@ -1496,7 +1574,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1496
1574
|
}
|
|
1497
1575
|
|
|
1498
1576
|
try options.log.faint(.{ battle.active(player), done });
|
|
1499
|
-
|
|
1577
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1578
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1579
|
+
if (cap) options.calc.capped(player);
|
|
1500
1580
|
return null;
|
|
1501
1581
|
}
|
|
1502
1582
|
|
|
@@ -1560,12 +1640,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1560
1640
|
|
|
1561
1641
|
battle.turn += 1;
|
|
1562
1642
|
|
|
1563
|
-
if (
|
|
1643
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1564
1644
|
try options.log.tie(.{});
|
|
1565
1645
|
return Result.Tie;
|
|
1566
1646
|
}
|
|
1567
1647
|
|
|
1568
|
-
if (
|
|
1648
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1569
1649
|
try options.log.tie(.{});
|
|
1570
1650
|
return Result.Tie;
|
|
1571
1651
|
} else if (battle.turn >= 65535) {
|
|
@@ -1656,17 +1736,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1656
1736
|
}
|
|
1657
1737
|
}
|
|
1658
1738
|
|
|
1659
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1660
|
-
var volatiles = &active.volatiles;
|
|
1661
|
-
assert(volatiles.Thrashing);
|
|
1662
|
-
if (volatiles.attacks > 0) return false;
|
|
1663
|
-
|
|
1664
|
-
volatiles.Thrashing = false;
|
|
1665
|
-
volatiles.Confusion = true;
|
|
1666
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1667
|
-
return true;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1739
|
fn onBegin(
|
|
1671
1740
|
battle: anytype,
|
|
1672
1741
|
player: Player,
|
|
@@ -1753,8 +1822,10 @@ pub const Effects = struct {
|
|
|
1753
1822
|
|
|
1754
1823
|
side.active.volatiles.Bide = true;
|
|
1755
1824
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1825
|
+
|
|
1756
1826
|
side.active.volatiles.state = 0;
|
|
1757
1827
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1828
|
+
options.chance.observe(.attacking, player, .started);
|
|
1758
1829
|
|
|
1759
1830
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1760
1831
|
}
|
|
@@ -1820,7 +1891,10 @@ pub const Effects = struct {
|
|
|
1820
1891
|
|
|
1821
1892
|
if (foe.active.volatiles.Confusion) return;
|
|
1822
1893
|
foe.active.volatiles.Confusion = true;
|
|
1823
|
-
|
|
1894
|
+
|
|
1895
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1896
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1897
|
+
options.calc.confusion(player.foe());
|
|
1824
1898
|
|
|
1825
1899
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1826
1900
|
}
|
|
@@ -1866,18 +1940,18 @@ pub const Effects = struct {
|
|
|
1866
1940
|
|
|
1867
1941
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1868
1942
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1869
|
-
if (n == 0) {
|
|
1943
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1870
1944
|
try options.log.fail(.{ foe_ident, .None });
|
|
1871
1945
|
return null;
|
|
1872
1946
|
} else if (err) {
|
|
1873
1947
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1874
|
-
assert(!showdown);
|
|
1875
1948
|
return Result.Error;
|
|
1876
1949
|
}
|
|
1877
1950
|
|
|
1878
1951
|
volatiles.disable_move =
|
|
1879
1952
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1880
1953
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1954
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1881
1955
|
|
|
1882
1956
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1883
1957
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1948,7 +2022,7 @@ pub const Effects = struct {
|
|
|
1948
2022
|
if (foe.active.types.includes(move.type)) return;
|
|
1949
2023
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1950
2024
|
// Freeze Clause Mod
|
|
1951
|
-
if (
|
|
2025
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1952
2026
|
|
|
1953
2027
|
foe_stored.status = Status.init(.FRZ);
|
|
1954
2028
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1984,22 +2058,24 @@ pub const Effects = struct {
|
|
|
1984
2058
|
if (p != player and Status.any(s.stored().status)) {
|
|
1985
2059
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1986
2060
|
s.stored().status = 0;
|
|
2061
|
+
options.chance.observe(.sleep, p, .None);
|
|
1987
2062
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1988
2063
|
s.stored().status = Status.init(.PSN);
|
|
1989
2064
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1990
2065
|
}
|
|
1991
|
-
try clearVolatiles(battle, p,
|
|
2066
|
+
try clearVolatiles(battle, p, options);
|
|
1992
2067
|
}
|
|
1993
2068
|
} else {
|
|
1994
2069
|
if (Status.any(foe_stored.status)) {
|
|
1995
2070
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1996
2071
|
foe.last_selected_move = .SKIP_TURN;
|
|
2072
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1997
2073
|
}
|
|
1998
2074
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1999
2075
|
foe_stored.status = 0;
|
|
2000
2076
|
}
|
|
2001
|
-
try clearVolatiles(battle, player,
|
|
2002
|
-
try clearVolatiles(battle, player.foe(),
|
|
2077
|
+
try clearVolatiles(battle, player, options);
|
|
2078
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2003
2079
|
}
|
|
2004
2080
|
}
|
|
2005
2081
|
|
|
@@ -2287,7 +2363,7 @@ pub const Effects = struct {
|
|
|
2287
2363
|
}
|
|
2288
2364
|
|
|
2289
2365
|
// Sleep Clause Mod
|
|
2290
|
-
if (
|
|
2366
|
+
if (pkmn.options.mod) {
|
|
2291
2367
|
for (foe.pokemon) |p| {
|
|
2292
2368
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2293
2369
|
}
|
|
@@ -2295,6 +2371,8 @@ pub const Effects = struct {
|
|
|
2295
2371
|
foe.active.volatiles.Recharging = false;
|
|
2296
2372
|
|
|
2297
2373
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2374
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2375
|
+
|
|
2298
2376
|
const last = battle.side(player).last_selected_move;
|
|
2299
2377
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2300
2378
|
}
|
|
@@ -2351,6 +2429,8 @@ pub const Effects = struct {
|
|
|
2351
2429
|
|
|
2352
2430
|
volatiles.Thrashing = true;
|
|
2353
2431
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2432
|
+
|
|
2433
|
+
options.chance.observe(.attacking, player, .started);
|
|
2354
2434
|
}
|
|
2355
2435
|
|
|
2356
2436
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2389,13 +2469,14 @@ pub const Effects = struct {
|
|
|
2389
2469
|
if (side.active.volatiles.Binding) return;
|
|
2390
2470
|
side.active.volatiles.Binding = true;
|
|
2391
2471
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2392
|
-
// (Pokémon Showdown
|
|
2472
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2393
2473
|
if (!showdown) {
|
|
2394
2474
|
foe.active.volatiles.Recharging = false;
|
|
2395
2475
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2396
2476
|
|
|
2397
2477
|
side.active.volatiles.attacks =
|
|
2398
2478
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2479
|
+
options.chance.observe(.binding, player, .started);
|
|
2399
2480
|
}
|
|
2400
2481
|
|
|
2401
2482
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2625,22 +2706,22 @@ fn isForced(active: anytype) bool {
|
|
|
2625
2706
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2626
2707
|
}
|
|
2627
2708
|
|
|
2628
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2709
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2629
2710
|
var log = options.log;
|
|
2630
2711
|
var side = battle.side(who);
|
|
2631
2712
|
var volatiles = &side.active.volatiles;
|
|
2632
2713
|
const ident = battle.active(who);
|
|
2633
2714
|
|
|
2634
|
-
options.chance.clearDurations(who, clear);
|
|
2635
|
-
|
|
2636
2715
|
if (volatiles.disable_move != 0) {
|
|
2637
2716
|
volatiles.disable_move = 0;
|
|
2638
2717
|
volatiles.disable_duration = 0;
|
|
2718
|
+
options.chance.observe(.disable, who, .None);
|
|
2639
2719
|
try log.end(.{ ident, .DisableSilent });
|
|
2640
2720
|
}
|
|
2641
2721
|
if (volatiles.Confusion) {
|
|
2642
2722
|
// volatiles.confusion is left unchanged
|
|
2643
2723
|
volatiles.Confusion = false;
|
|
2724
|
+
options.chance.observe(.confusion, who, .None);
|
|
2644
2725
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2645
2726
|
}
|
|
2646
2727
|
if (volatiles.Mist) {
|
|
@@ -2675,6 +2756,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2675
2756
|
}
|
|
2676
2757
|
}
|
|
2677
2758
|
|
|
2759
|
+
fn decrement(
|
|
2760
|
+
comptime field: chance.Action.Field,
|
|
2761
|
+
player: Player,
|
|
2762
|
+
options: anytype,
|
|
2763
|
+
n: anytype,
|
|
2764
|
+
) @TypeOf(n) {
|
|
2765
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2766
|
+
.started, .ended => 0,
|
|
2767
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2768
|
+
else => unreachable,
|
|
2769
|
+
} else n - 1;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2678
2772
|
pub const Rolls = struct {
|
|
2679
2773
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2680
2774
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2872,9 +2966,10 @@ pub const Rolls = struct {
|
|
|
2872
2966
|
}
|
|
2873
2967
|
|
|
2874
2968
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2875
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2876
|
-
@intCast(val)
|
|
2877
|
-
else
|
|
2969
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2970
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2971
|
+
else
|
|
2972
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2878
2973
|
battle.rng.sleepDuration(player)
|
|
2879
2974
|
else if (showdown)
|
|
2880
2975
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2886,14 +2981,15 @@ pub const Rolls = struct {
|
|
|
2886
2981
|
};
|
|
2887
2982
|
|
|
2888
2983
|
assert(duration >= 1 and duration <= 7);
|
|
2889
|
-
options.chance.duration(
|
|
2984
|
+
options.chance.duration(player, duration);
|
|
2890
2985
|
return duration;
|
|
2891
2986
|
}
|
|
2892
2987
|
|
|
2893
2988
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2894
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2895
|
-
val
|
|
2896
|
-
else
|
|
2989
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2990
|
+
if (val >= 1 and val <= 8) val else null
|
|
2991
|
+
else
|
|
2992
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2897
2993
|
battle.rng.disableDuration(player)
|
|
2898
2994
|
else if (showdown)
|
|
2899
2995
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2901,29 +2997,31 @@ pub const Rolls = struct {
|
|
|
2901
2997
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2902
2998
|
|
|
2903
2999
|
assert(duration >= 1 and duration <= 8);
|
|
2904
|
-
options.chance.duration(
|
|
3000
|
+
options.chance.duration(player, duration);
|
|
2905
3001
|
return duration;
|
|
2906
3002
|
}
|
|
2907
3003
|
|
|
2908
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2909
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2910
|
-
@intCast(val)
|
|
2911
|
-
else
|
|
2912
|
-
battle.rng
|
|
3004
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3005
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3006
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3007
|
+
else
|
|
3008
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3009
|
+
battle.rng.confusionDuration(player)
|
|
2913
3010
|
else if (showdown)
|
|
2914
3011
|
battle.rng.range(u3, 2, 6)
|
|
2915
3012
|
else
|
|
2916
3013
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2917
3014
|
|
|
2918
3015
|
assert(duration >= 2 and duration <= 5);
|
|
2919
|
-
options.chance.duration(
|
|
3016
|
+
options.chance.duration(player, duration);
|
|
2920
3017
|
return duration;
|
|
2921
3018
|
}
|
|
2922
3019
|
|
|
2923
3020
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2924
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2925
|
-
@intCast(val)
|
|
2926
|
-
else
|
|
3021
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3022
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3023
|
+
else
|
|
3024
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2927
3025
|
battle.rng.attackingDuration(player)
|
|
2928
3026
|
else if (showdown)
|
|
2929
3027
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2931,7 +3029,7 @@ pub const Rolls = struct {
|
|
|
2931
3029
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2932
3030
|
|
|
2933
3031
|
assert(duration >= 2 and duration <= 3);
|
|
2934
|
-
options.chance.duration(
|
|
3032
|
+
options.chance.duration(player, duration);
|
|
2935
3033
|
return duration;
|
|
2936
3034
|
}
|
|
2937
3035
|
|
|
@@ -2944,9 +3042,10 @@ pub const Rolls = struct {
|
|
|
2944
3042
|
options: anytype,
|
|
2945
3043
|
) !u3 {
|
|
2946
3044
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2947
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2948
|
-
@intCast(val)
|
|
2949
|
-
else
|
|
3045
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3046
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3047
|
+
else
|
|
3048
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2950
3049
|
battle.rng.distribution(player)
|
|
2951
3050
|
else if (showdown)
|
|
2952
3051
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2957,7 +3056,7 @@ pub const Rolls = struct {
|
|
|
2957
3056
|
|
|
2958
3057
|
assert(n >= 2 and n <= 5);
|
|
2959
3058
|
if (effect == .Binding) {
|
|
2960
|
-
options.chance.duration(
|
|
3059
|
+
options.chance.duration(player, n);
|
|
2961
3060
|
} else {
|
|
2962
3061
|
try options.chance.multiHit(player, n);
|
|
2963
3062
|
}
|
|
@@ -2972,15 +3071,13 @@ pub const Rolls = struct {
|
|
|
2972
3071
|
options: anytype,
|
|
2973
3072
|
) !u4 {
|
|
2974
3073
|
// TODO: consider throwing error instead of rerolling?
|
|
2975
|
-
const
|
|
3074
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2976
3075
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2977
3076
|
val
|
|
2978
3077
|
else
|
|
2979
3078
|
null
|
|
2980
3079
|
else
|
|
2981
|
-
null
|
|
2982
|
-
|
|
2983
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3080
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2984
3081
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2985
3082
|
else slot: {
|
|
2986
3083
|
if (showdown) {
|
|
@@ -3138,7 +3235,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3138
3235
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3139
3236
|
if (n == before) {
|
|
3140
3237
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3141
|
-
if (!
|
|
3238
|
+
if (!pkmn.options.mod) {
|
|
3142
3239
|
while (slot <= 4) : (slot += 1) {
|
|
3143
3240
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3144
3241
|
}
|