@pkmn/engine 0.1.0-dev.0eb29f53 → 0.1.0-dev.207fa86a
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 +7 -35
- package/build/pkg/data.d.ts +2 -1
- package/build/pkg/data.js +5 -3
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/index.d.ts +1 -1
- package/build/pkg/index.js +1 -1
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.js +1 -1
- package/build/pkg/protocol.js.map +1 -1
- package/build.zig +27 -17
- package/package.json +26 -37
- package/src/bin/install-pkmn-engine +31 -58
- package/src/lib/binding/node.zig +4 -4
- package/src/lib/binding/wasm.zig +2 -2
- package/src/lib/gen1/README.md +24 -38
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +4 -0
- package/src/lib/gen1/mechanics.zig +268 -345
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +3 -3
- package/build/pkg/addon.d.ts +0 -7
- package/build/pkg/addon.js +0 -111
- package/build/pkg/addon.js.map +0 -1
- package/build/test/display.js +0 -383
- package/build/test/display.js.map +0 -1
- package/build/tools/debug.js +0 -133
- package/build/tools/debug.js.map +0 -1
- package/src/bin/pkmn-debug +0 -62
- package/src/lib/common/debug.zig +0 -64
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -56,8 +56,6 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
-
|
|
61
59
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
62
60
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
61
|
if (battle.turn == 0) return start(battle, log);
|
|
@@ -187,7 +185,6 @@ fn selectMove(
|
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
// move select
|
|
190
|
-
volatiles.state = 0;
|
|
191
188
|
if (choice.data == 0) {
|
|
192
189
|
const struggle = ok: {
|
|
193
190
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -304,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
304
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
305
302
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
306
303
|
|
|
307
|
-
const p1 =
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
+
|
|
308
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
309
310
|
|
|
310
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -314,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
314
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
315
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
316
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
317
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
318
|
+
// that out and enforce host ordering of events, so P1 just goes first reqgardless of who
|
|
318
319
|
// won the original coin flip
|
|
319
320
|
if (t1 and t2) return .P1;
|
|
320
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -346,7 +347,6 @@ fn doTurn(
|
|
|
346
347
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
347
348
|
} else if (foe_choice.type == .Pass) return null;
|
|
348
349
|
|
|
349
|
-
residual = true;
|
|
350
350
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
351
351
|
if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
|
|
352
352
|
if (!replace) {
|
|
@@ -434,7 +434,7 @@ fn executeMove(
|
|
|
434
434
|
|
|
435
435
|
var skip_can = false;
|
|
436
436
|
var skip_pp = false;
|
|
437
|
-
switch (try beforeMove(battle, player, from,
|
|
437
|
+
switch (try beforeMove(battle, player, from, log)) {
|
|
438
438
|
.done => return null,
|
|
439
439
|
.skip_can => skip_can = true,
|
|
440
440
|
.skip_pp => skip_pp = true,
|
|
@@ -442,22 +442,14 @@ fn executeMove(
|
|
|
442
442
|
.err => return @as(?Result, Result.Error),
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from,
|
|
446
|
-
return null;
|
|
447
|
-
}
|
|
445
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
|
|
448
446
|
|
|
449
447
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
450
448
|
}
|
|
451
449
|
|
|
452
450
|
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
453
451
|
|
|
454
|
-
fn beforeMove(
|
|
455
|
-
battle: anytype,
|
|
456
|
-
player: Player,
|
|
457
|
-
from: ?Move,
|
|
458
|
-
residual: *bool,
|
|
459
|
-
log: anytype,
|
|
460
|
-
) !BeforeMove {
|
|
452
|
+
fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
|
|
461
453
|
var side = battle.side(player);
|
|
462
454
|
const foe = battle.foe(player);
|
|
463
455
|
var active = &side.active;
|
|
@@ -536,9 +528,14 @@ fn beforeMove(
|
|
|
536
528
|
} else {
|
|
537
529
|
try log.activate(ident, .Confusion);
|
|
538
530
|
|
|
539
|
-
if (
|
|
531
|
+
const confused = if (showdown)
|
|
532
|
+
!battle.rng.chance(u8, 128, 256)
|
|
533
|
+
else
|
|
534
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
+
|
|
536
|
+
if (confused) {
|
|
540
537
|
assert(!volatiles.MultiHit);
|
|
541
|
-
|
|
538
|
+
volatiles.state = 0;
|
|
542
539
|
volatiles.Bide = false;
|
|
543
540
|
volatiles.Thrashing = false;
|
|
544
541
|
volatiles.MultiHit = false;
|
|
@@ -562,12 +559,8 @@ fn beforeMove(
|
|
|
562
559
|
player
|
|
563
560
|
else
|
|
564
561
|
player.foe();
|
|
565
|
-
|
|
566
|
-
const uncapped = battle.last_damage;
|
|
567
562
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
568
563
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
569
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
570
|
-
if (showdown) battle.last_damage = uncapped;
|
|
571
564
|
|
|
572
565
|
return .done;
|
|
573
566
|
}
|
|
@@ -576,15 +569,22 @@ fn beforeMove(
|
|
|
576
569
|
|
|
577
570
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
578
571
|
|
|
579
|
-
if (Status.is(stored.status, .PAR)
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
572
|
+
if (Status.is(stored.status, .PAR)) {
|
|
573
|
+
const paralyzed = if (showdown)
|
|
574
|
+
battle.rng.chance(u8, 63, 256)
|
|
575
|
+
else
|
|
576
|
+
battle.rng.next() < Gen12.percent(25);
|
|
577
|
+
|
|
578
|
+
if (paralyzed) {
|
|
579
|
+
volatiles.state = 0;
|
|
580
|
+
volatiles.Bide = false;
|
|
581
|
+
volatiles.Thrashing = false;
|
|
582
|
+
volatiles.Charging = false;
|
|
583
|
+
volatiles.Binding = false;
|
|
584
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
585
|
+
try log.cant(ident, .Paralysis);
|
|
586
|
+
return .done;
|
|
587
|
+
}
|
|
588
588
|
}
|
|
589
589
|
|
|
590
590
|
if (volatiles.Bide) {
|
|
@@ -611,18 +611,7 @@ fn beforeMove(
|
|
|
611
611
|
return .done;
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
615
614
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
616
|
-
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
617
|
-
|
|
618
|
-
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
619
|
-
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
620
|
-
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
621
|
-
if (showdown) {
|
|
622
|
-
residual.* = false;
|
|
623
|
-
try handleResidual(battle, player, log);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
615
|
return .done;
|
|
627
616
|
}
|
|
628
617
|
|
|
@@ -665,7 +654,6 @@ fn canMove(
|
|
|
665
654
|
auto: bool,
|
|
666
655
|
skip_pp: bool,
|
|
667
656
|
from: ?Move,
|
|
668
|
-
residual: *bool,
|
|
669
657
|
log: anytype,
|
|
670
658
|
) !bool {
|
|
671
659
|
var side = battle.side(player);
|
|
@@ -700,7 +688,7 @@ fn canMove(
|
|
|
700
688
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
701
689
|
|
|
702
690
|
if (move.effect.onBegin()) {
|
|
703
|
-
try onBegin(battle, player, move, mslot,
|
|
691
|
+
try onBegin(battle, player, move, mslot, log);
|
|
704
692
|
return false;
|
|
705
693
|
}
|
|
706
694
|
|
|
@@ -779,21 +767,16 @@ fn doMove(
|
|
|
779
767
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
780
768
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
781
769
|
if (immune and move.effect != .Binding) break :miss true;
|
|
782
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
783
|
-
battle.last_damage = 0;
|
|
784
|
-
break :miss true;
|
|
785
|
-
}
|
|
770
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
786
771
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
787
772
|
};
|
|
788
773
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
789
774
|
|
|
790
|
-
var late = showdown and move.effect != .Explode;
|
|
791
775
|
const skip = status or immune;
|
|
792
776
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
793
777
|
if (showdown and move.effect.isMulti()) {
|
|
794
778
|
Effects.multiHit(battle, player, move);
|
|
795
779
|
hits = side.active.volatiles.attacks;
|
|
796
|
-
late = false;
|
|
797
780
|
}
|
|
798
781
|
|
|
799
782
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -827,7 +810,7 @@ fn doMove(
|
|
|
827
810
|
}
|
|
828
811
|
|
|
829
812
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
830
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
813
|
+
if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
|
|
831
814
|
|
|
832
815
|
miss = if (showdown or skip)
|
|
833
816
|
miss
|
|
@@ -874,9 +857,15 @@ fn doMove(
|
|
|
874
857
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
875
858
|
} else if (move.effect == .Explode) {
|
|
876
859
|
try Effects.explode(battle, player);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
860
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
861
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
862
|
+
}
|
|
863
|
+
} else if (showdown) {
|
|
864
|
+
if (move.effect == .Disable) {
|
|
865
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
866
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
880
869
|
}
|
|
881
870
|
return null;
|
|
882
871
|
}
|
|
@@ -889,9 +878,6 @@ fn doMove(
|
|
|
889
878
|
hits = side.active.volatiles.attacks;
|
|
890
879
|
}
|
|
891
880
|
|
|
892
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
893
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
894
|
-
|
|
895
881
|
var nullified = false;
|
|
896
882
|
var hit: u4 = 0;
|
|
897
883
|
while (hit < hits) {
|
|
@@ -907,7 +893,9 @@ fn doMove(
|
|
|
907
893
|
if (hit == 0 and ohko) try log.ohko();
|
|
908
894
|
hit += 1;
|
|
909
895
|
if (foe.stored().hp == 0) break;
|
|
910
|
-
if (
|
|
896
|
+
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
897
|
+
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
+
}
|
|
911
899
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
912
900
|
if (nullified) break;
|
|
913
901
|
}
|
|
@@ -944,9 +932,6 @@ fn doMove(
|
|
|
944
932
|
|
|
945
933
|
if (foe.stored().hp == 0) return null;
|
|
946
934
|
|
|
947
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
948
|
-
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
949
|
-
|
|
950
935
|
if (!move.effect.isSpecial()) {
|
|
951
936
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
952
937
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -958,9 +943,7 @@ fn doMove(
|
|
|
958
943
|
if (move.effect == .Twineedle) {
|
|
959
944
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
960
945
|
} else if (move.effect == .Disable) {
|
|
961
|
-
|
|
962
|
-
if (showdown) try buildRage(battle, player.foe(), log);
|
|
963
|
-
return result;
|
|
946
|
+
return Effects.disable(battle, player, move, log);
|
|
964
947
|
} else {
|
|
965
948
|
try moveEffect(battle, player, move, log);
|
|
966
949
|
}
|
|
@@ -978,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
978
961
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
979
962
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
980
963
|
|
|
981
|
-
return
|
|
964
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
965
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
982
966
|
}
|
|
983
967
|
|
|
984
968
|
fn calcDamage(
|
|
@@ -1082,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1082
1066
|
|
|
1083
1067
|
fn randomizeDamage(battle: anytype) void {
|
|
1084
1068
|
if (battle.last_damage <= 1) return;
|
|
1085
|
-
|
|
1069
|
+
|
|
1070
|
+
const random = if (showdown)
|
|
1071
|
+
battle.rng.range(u8, 217, 256)
|
|
1072
|
+
else loop: {
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1075
|
+
if (r >= 217) break :loop r;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1086
1079
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1087
1080
|
}
|
|
1088
1081
|
|
|
1089
1082
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1090
1083
|
const side = battle.side(player);
|
|
1091
|
-
const foe = battle.foe(player);
|
|
1092
1084
|
|
|
1093
1085
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1094
1086
|
|
|
1095
1087
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1096
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1088
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1097
1089
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1098
1090
|
.SonicBoom => 20,
|
|
1099
1091
|
.DragonRage => 40,
|
|
1100
1092
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1101
1093
|
.Psywave => power: {
|
|
1102
1094
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1095
|
+
if (showdown) {
|
|
1096
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1097
|
+
} else {
|
|
1098
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1099
|
+
if (max <= 1) return Result.Error;
|
|
1100
|
+
while (true) {
|
|
1101
|
+
const r = battle.rng.next();
|
|
1102
|
+
if (r < max) break :power r;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1106
1105
|
},
|
|
1107
1106
|
else => unreachable,
|
|
1108
1107
|
};
|
|
1109
1108
|
|
|
1110
1109
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1111
1110
|
|
|
1112
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1113
1111
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1114
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1115
|
-
|
|
1116
1112
|
return null;
|
|
1117
1113
|
}
|
|
1118
1114
|
|
|
@@ -1220,7 +1216,7 @@ fn mirrorMove(
|
|
|
1220
1216
|
|
|
1221
1217
|
incrementPP(side, mslot);
|
|
1222
1218
|
|
|
1223
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove,
|
|
1219
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1224
1220
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1225
1221
|
}
|
|
1226
1222
|
|
|
@@ -1234,10 +1230,22 @@ fn metronome(
|
|
|
1234
1230
|
) !?Result {
|
|
1235
1231
|
var side = battle.side(player);
|
|
1236
1232
|
|
|
1237
|
-
side.last_selected_move =
|
|
1233
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1234
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1235
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1236
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1237
|
+
} else loop: {
|
|
1238
|
+
while (true) {
|
|
1239
|
+
const r = battle.rng.next();
|
|
1240
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1241
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1242
|
+
break :loop @intToEnum(Move, r);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1238
1246
|
incrementPP(side, mslot);
|
|
1239
1247
|
|
|
1240
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome,
|
|
1248
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1241
1249
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1242
1250
|
}
|
|
1243
1251
|
|
|
@@ -1270,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1270
1278
|
if (move.effect == .Swift) return true;
|
|
1271
1279
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1272
1280
|
}
|
|
1273
|
-
if (move.effect == .DreamEater and
|
|
1274
|
-
(showdown and foe.active.volatiles.Substitute)))
|
|
1275
|
-
{
|
|
1281
|
+
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
|
|
1276
1282
|
immune.* = true;
|
|
1277
1283
|
if (showdown) return false;
|
|
1278
1284
|
break :miss true;
|
|
@@ -1292,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1292
1298
|
}
|
|
1293
1299
|
|
|
1294
1300
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1301
|
+
var state = &side.active.volatiles.state;
|
|
1302
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1303
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1304
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1301
1305
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1302
1306
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1303
1307
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1304
1308
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1305
1309
|
accuracy = @min(255, @max(1, accuracy));
|
|
1306
1310
|
|
|
1307
|
-
|
|
1308
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1309
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1311
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1310
1312
|
|
|
1311
1313
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1312
|
-
break :miss
|
|
1314
|
+
break :miss if (showdown)
|
|
1315
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1316
|
+
else
|
|
1317
|
+
battle.rng.next() >= accuracy;
|
|
1313
1318
|
};
|
|
1314
1319
|
|
|
1315
1320
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1424,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1424
1429
|
}
|
|
1425
1430
|
|
|
1426
1431
|
if (volatiles.LeechSeed) {
|
|
1427
|
-
var foe = battle.foe(player);
|
|
1428
|
-
var foe_stored = foe.stored();
|
|
1429
|
-
const foe_ident = battle.active(player.foe());
|
|
1430
|
-
|
|
1431
|
-
if (foe_stored.hp == 0) {
|
|
1432
|
-
assert(showdown);
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
1432
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1437
1433
|
|
|
1438
1434
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1444,12 +1440,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1444
1440
|
const amount = @min(damage, stored.hp);
|
|
1445
1441
|
stored.hp -= amount;
|
|
1446
1442
|
|
|
1443
|
+
var foe = battle.foe(player);
|
|
1444
|
+
var foe_stored = foe.stored();
|
|
1445
|
+
const foe_ident = battle.active(player.foe());
|
|
1446
|
+
|
|
1447
1447
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
|
-
if (amount > 0)
|
|
1449
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1450
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1451
|
-
if (showdown) battle.last_damage = damage;
|
|
1452
|
-
}
|
|
1448
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1453
1449
|
|
|
1454
1450
|
const before = foe_stored.hp;
|
|
1455
1451
|
// Uncapped damage is added back to the foe
|
|
@@ -1484,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1484
1480
|
|
|
1485
1481
|
fn checkEBC(battle: anytype) bool {
|
|
1486
1482
|
for (battle.sides, 0..) |side, i| {
|
|
1487
|
-
const foe = battle.sides[~@intCast(u1, i)];
|
|
1488
|
-
|
|
1489
1483
|
var foe_all_ghosts = true;
|
|
1490
1484
|
var foe_all_transform = true;
|
|
1491
|
-
for (foe.order, 0..) |id, j| {
|
|
1492
|
-
if (id == 0) break;
|
|
1493
|
-
const active = j == 0;
|
|
1494
|
-
const pokemon = foe.pokemon[id - 1];
|
|
1495
1485
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1486
|
+
for (battle.sides[~@intCast(u1, i)].pokemon) |pokemon| {
|
|
1487
|
+
if (pokemon.species == .None) continue;
|
|
1488
|
+
|
|
1489
|
+
const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
|
|
1498
1490
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1499
1491
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1500
|
-
for (
|
|
1492
|
+
for (pokemon.moves) |m| {
|
|
1501
1493
|
if (m.id == .None) break :transform true;
|
|
1502
1494
|
if (m.id != .Transform) break :transform false;
|
|
1503
1495
|
}
|
|
@@ -1505,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1505
1497
|
};
|
|
1506
1498
|
}
|
|
1507
1499
|
|
|
1508
|
-
for (side.
|
|
1509
|
-
if (id == 0) break;
|
|
1510
|
-
const active = j == 0;
|
|
1511
|
-
const pokemon = side.pokemon[id - 1];
|
|
1512
|
-
|
|
1500
|
+
for (side.pokemon) |pokemon| {
|
|
1513
1501
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1514
1502
|
const transform = foe_all_transform and transform: {
|
|
1515
|
-
for (
|
|
1503
|
+
for (pokemon.moves) |m| {
|
|
1516
1504
|
if (m.id == .None) break :transform true;
|
|
1517
1505
|
if (m.id != .Transform) break :transform false;
|
|
1518
1506
|
}
|
|
@@ -1520,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1520
1508
|
};
|
|
1521
1509
|
if (transform) continue;
|
|
1522
1510
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1523
|
-
for (
|
|
1511
|
+
for (pokemon.moves) |m| {
|
|
1524
1512
|
if (m.pp != 0) break :no_pp false;
|
|
1525
1513
|
}
|
|
1526
1514
|
break :no_pp true;
|
|
@@ -1536,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1536
1524
|
return false;
|
|
1537
1525
|
}
|
|
1538
1526
|
|
|
1539
|
-
fn
|
|
1540
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1541
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1542
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1543
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1544
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1545
|
-
m
|
|
1546
|
-
else
|
|
1547
|
-
side.last_selected_move;
|
|
1548
|
-
if (m != .None and m == last) {
|
|
1549
|
-
side.active.volatiles.Charging = false;
|
|
1550
|
-
try log.disabled(ident, last);
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1558
|
-
const side = battle.side(who);
|
|
1559
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1560
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1565
|
-
var volatiles = &active.volatiles;
|
|
1566
|
-
assert(volatiles.Thrashing);
|
|
1567
|
-
if (volatiles.attacks > 0) return false;
|
|
1568
|
-
|
|
1569
|
-
volatiles.Thrashing = false;
|
|
1570
|
-
volatiles.Confusion = true;
|
|
1571
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1572
|
-
return true;
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
inline fn onBegin(
|
|
1576
|
-
battle: anytype,
|
|
1577
|
-
player: Player,
|
|
1578
|
-
move: Move.Data,
|
|
1579
|
-
mslot: u8,
|
|
1580
|
-
residual: *bool,
|
|
1581
|
-
log: anytype,
|
|
1582
|
-
) !void {
|
|
1527
|
+
inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
|
|
1583
1528
|
assert(move.effect.onBegin());
|
|
1584
1529
|
return switch (move.effect) {
|
|
1585
1530
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1595,7 +1540,7 @@ inline fn onBegin(
|
|
|
1595
1540
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1596
1541
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1597
1542
|
.Splash => Effects.splash(battle, player, log),
|
|
1598
|
-
.Substitute => Effects.substitute(battle, player,
|
|
1543
|
+
.Substitute => Effects.substitute(battle, player, log),
|
|
1599
1544
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1600
1545
|
.Transform => Effects.transform(battle, player, log),
|
|
1601
1546
|
else => unreachable,
|
|
@@ -1662,7 +1607,10 @@ pub const Effects = struct {
|
|
|
1662
1607
|
side.active.volatiles.Bide = true;
|
|
1663
1608
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1664
1609
|
side.active.volatiles.state = 0;
|
|
1665
|
-
side.active.volatiles.attacks =
|
|
1610
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1611
|
+
battle.rng.range(u4, 2, 4)
|
|
1612
|
+
else
|
|
1613
|
+
(battle.rng.next() & 1) + 2);
|
|
1666
1614
|
|
|
1667
1615
|
try log.start(battle.active(player), .Bide);
|
|
1668
1616
|
}
|
|
@@ -1684,8 +1632,14 @@ pub const Effects = struct {
|
|
|
1684
1632
|
return;
|
|
1685
1633
|
}
|
|
1686
1634
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1635
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1636
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1637
|
+
else
|
|
1638
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1639
|
+
Gen12.percent(10)
|
|
1640
|
+
else
|
|
1641
|
+
Gen12.percent(30));
|
|
1642
|
+
if (!chance) return;
|
|
1689
1643
|
|
|
1690
1644
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1645
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1709,7 +1663,11 @@ pub const Effects = struct {
|
|
|
1709
1663
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1664
|
|
|
1711
1665
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
if (
|
|
1666
|
+
const chance = if (showdown)
|
|
1667
|
+
battle.rng.chance(u8, 25, 256)
|
|
1668
|
+
else
|
|
1669
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1670
|
+
if (!chance) return;
|
|
1713
1671
|
} else {
|
|
1714
1672
|
if (showdown) {
|
|
1715
1673
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1728,7 +1686,10 @@ pub const Effects = struct {
|
|
|
1728
1686
|
|
|
1729
1687
|
if (foe.active.volatiles.Confusion) return;
|
|
1730
1688
|
foe.active.volatiles.Confusion = true;
|
|
1731
|
-
foe.active.volatiles.confusion =
|
|
1689
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1690
|
+
battle.rng.range(u8, 2, 6)
|
|
1691
|
+
else
|
|
1692
|
+
(battle.rng.next() & 3) + 2);
|
|
1732
1693
|
|
|
1733
1694
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1734
1695
|
}
|
|
@@ -1778,8 +1739,11 @@ pub const Effects = struct {
|
|
|
1778
1739
|
return Result.Error;
|
|
1779
1740
|
}
|
|
1780
1741
|
|
|
1781
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1782
|
-
volatiles.disabled_duration =
|
|
1742
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1743
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1744
|
+
battle.rng.range(u8, 1, 9)
|
|
1745
|
+
else
|
|
1746
|
+
(battle.rng.next() & 7) + 1);
|
|
1783
1747
|
|
|
1784
1748
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1785
1749
|
return null;
|
|
@@ -1789,11 +1753,6 @@ pub const Effects = struct {
|
|
|
1789
1753
|
var side = battle.side(player);
|
|
1790
1754
|
var stored = side.stored();
|
|
1791
1755
|
|
|
1792
|
-
if (battle.last_damage == 0) {
|
|
1793
|
-
assert(showdown);
|
|
1794
|
-
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
1756
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1798
1757
|
battle.last_damage = drain;
|
|
1799
1758
|
|
|
@@ -1818,7 +1777,15 @@ pub const Effects = struct {
|
|
|
1818
1777
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1819
1778
|
|
|
1820
1779
|
if (volatiles.Substitute) return;
|
|
1821
|
-
|
|
1780
|
+
|
|
1781
|
+
const chance = if (showdown)
|
|
1782
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1783
|
+
else
|
|
1784
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1785
|
+
Gen12.percent(10)
|
|
1786
|
+
else
|
|
1787
|
+
Gen12.percent(30));
|
|
1788
|
+
if (!chance) return;
|
|
1822
1789
|
|
|
1823
1790
|
volatiles.Flinch = true;
|
|
1824
1791
|
volatiles.Recharging = false;
|
|
@@ -1844,8 +1811,12 @@ pub const Effects = struct {
|
|
|
1844
1811
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1845
1812
|
}
|
|
1846
1813
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1814
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1815
|
+
battle.rng.chance(u8, 26, 256)
|
|
1816
|
+
else
|
|
1817
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1818
|
+
if (!chance) return;
|
|
1819
|
+
|
|
1849
1820
|
// Freeze Clause Mod
|
|
1850
1821
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1851
1822
|
|
|
@@ -1990,7 +1961,7 @@ pub const Effects = struct {
|
|
|
1990
1961
|
}
|
|
1991
1962
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1992
1963
|
|
|
1993
|
-
const rslot =
|
|
1964
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
1994
1965
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1995
1966
|
|
|
1996
1967
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2011,8 +1982,7 @@ pub const Effects = struct {
|
|
|
2011
1982
|
assert(!side.active.volatiles.MultiHit);
|
|
2012
1983
|
side.active.volatiles.MultiHit = true;
|
|
2013
1984
|
|
|
2014
|
-
side.active.volatiles.attacks =
|
|
2015
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1985
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2016
1986
|
}
|
|
2017
1987
|
|
|
2018
1988
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2056,8 +2026,14 @@ pub const Effects = struct {
|
|
|
2056
2026
|
}
|
|
2057
2027
|
|
|
2058
2028
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2059
|
-
|
|
2060
|
-
|
|
2029
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2030
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2031
|
+
else
|
|
2032
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2033
|
+
Gen12.percent(10)
|
|
2034
|
+
else
|
|
2035
|
+
Gen12.percent(30));
|
|
2036
|
+
if (!chance) return;
|
|
2061
2037
|
|
|
2062
2038
|
foe_stored.status = Status.init(.PAR);
|
|
2063
2039
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2087,7 +2063,7 @@ pub const Effects = struct {
|
|
|
2087
2063
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2088
2064
|
if (!showdown)
|
|
2089
2065
|
.Poison
|
|
2090
|
-
else if (toxic ==
|
|
2066
|
+
else if (toxic == foe.active.volatiles.Toxic)
|
|
2091
2067
|
if (toxic) .Toxic else .Poison
|
|
2092
2068
|
else
|
|
2093
2069
|
.None
|
|
@@ -2100,7 +2076,16 @@ pub const Effects = struct {
|
|
|
2100
2076
|
|
|
2101
2077
|
if (move.effect == .Poison) {
|
|
2102
2078
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2103
|
-
} else
|
|
2079
|
+
} else {
|
|
2080
|
+
const chance = if (showdown)
|
|
2081
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2082
|
+
else
|
|
2083
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2084
|
+
Gen12.percent(20)
|
|
2085
|
+
else
|
|
2086
|
+
Gen12.percent(40));
|
|
2087
|
+
if (!chance) return;
|
|
2088
|
+
}
|
|
2104
2089
|
|
|
2105
2090
|
foe_stored.status = Status.init(.PSN);
|
|
2106
2091
|
if (toxic) {
|
|
@@ -2114,8 +2099,10 @@ pub const Effects = struct {
|
|
|
2114
2099
|
|
|
2115
2100
|
fn rage(battle: anytype, player: Player) !void {
|
|
2116
2101
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2117
|
-
|
|
2102
|
+
if (volatiles.Rage) return;
|
|
2118
2103
|
volatiles.Rage = true;
|
|
2104
|
+
assert(!volatiles.Bide);
|
|
2105
|
+
volatiles.state = 0;
|
|
2119
2106
|
}
|
|
2120
2107
|
|
|
2121
2108
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2167,7 +2154,16 @@ pub const Effects = struct {
|
|
|
2167
2154
|
}
|
|
2168
2155
|
foe.active.volatiles.Recharging = false;
|
|
2169
2156
|
|
|
2170
|
-
|
|
2157
|
+
const duration = @intCast(u3, if (showdown)
|
|
2158
|
+
battle.rng.range(u8, 1, 8)
|
|
2159
|
+
else loop: {
|
|
2160
|
+
while (true) {
|
|
2161
|
+
const r = battle.rng.next() & 7;
|
|
2162
|
+
if (r != 0) break :loop r;
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
foe_stored.status = Status.slp(duration);
|
|
2171
2167
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2172
2168
|
}
|
|
2173
2169
|
|
|
@@ -2175,7 +2171,7 @@ pub const Effects = struct {
|
|
|
2175
2171
|
try log.activate(battle.active(player), .Splash);
|
|
2176
2172
|
}
|
|
2177
2173
|
|
|
2178
|
-
fn substitute(battle: anytype, player: Player,
|
|
2174
|
+
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2179
2175
|
var side = battle.side(player);
|
|
2180
2176
|
if (side.active.volatiles.Substitute) {
|
|
2181
2177
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2200,18 +2196,14 @@ pub const Effects = struct {
|
|
|
2200
2196
|
side.active.volatiles.substitute = hp + 1;
|
|
2201
2197
|
side.active.volatiles.Substitute = true;
|
|
2202
2198
|
try log.start(battle.active(player), .Substitute);
|
|
2203
|
-
if (hp > 0)
|
|
2204
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2205
|
-
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2206
|
-
}
|
|
2199
|
+
if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
|
|
2207
2200
|
}
|
|
2208
2201
|
|
|
2209
2202
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2210
2203
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2211
2204
|
|
|
2212
|
-
// Whirlwind/Roar should not roll to hit
|
|
2205
|
+
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2213
2206
|
_ = try checkHit(battle, player, move, log);
|
|
2214
|
-
battle.last_damage = 0;
|
|
2215
2207
|
}
|
|
2216
2208
|
|
|
2217
2209
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2220,7 +2212,11 @@ pub const Effects = struct {
|
|
|
2220
2212
|
assert(!volatiles.Bide);
|
|
2221
2213
|
|
|
2222
2214
|
volatiles.Thrashing = true;
|
|
2223
|
-
volatiles.
|
|
2215
|
+
volatiles.state = 0;
|
|
2216
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2217
|
+
battle.rng.range(u8, 2, 4)
|
|
2218
|
+
else
|
|
2219
|
+
(battle.rng.next() & 1) + 2);
|
|
2224
2220
|
}
|
|
2225
2221
|
|
|
2226
2222
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2262,7 +2258,7 @@ pub const Effects = struct {
|
|
|
2262
2258
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2263
2259
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2264
2260
|
|
|
2265
|
-
side.active.volatiles.attacks =
|
|
2261
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2266
2262
|
}
|
|
2267
2263
|
|
|
2268
2264
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2289,11 +2285,9 @@ pub const Effects = struct {
|
|
|
2289
2285
|
return try log.fail(ident, .None);
|
|
2290
2286
|
}
|
|
2291
2287
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2292
|
-
const stat = unmodifiedStats(battle,
|
|
2288
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2293
2289
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2294
2290
|
try log.boost(ident, reason, n);
|
|
2295
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2296
|
-
if (showdown and move.effect == .Rage) return;
|
|
2297
2291
|
},
|
|
2298
2292
|
.DefenseUp1, .DefenseUp2 => {
|
|
2299
2293
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2309,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2309
2303
|
return try log.fail(ident, .None);
|
|
2310
2304
|
}
|
|
2311
2305
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2312
|
-
const stat = unmodifiedStats(battle,
|
|
2306
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2313
2307
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2314
2308
|
try log.boost(ident, .Defense, n);
|
|
2315
2309
|
},
|
|
@@ -2326,7 +2320,7 @@ pub const Effects = struct {
|
|
|
2326
2320
|
return try log.fail(ident, .None);
|
|
2327
2321
|
}
|
|
2328
2322
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2329
|
-
const stat = unmodifiedStats(battle,
|
|
2323
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2330
2324
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2331
2325
|
try log.boost(ident, .Speed, 2);
|
|
2332
2326
|
},
|
|
@@ -2346,7 +2340,7 @@ pub const Effects = struct {
|
|
|
2346
2340
|
return try log.fail(ident, .None);
|
|
2347
2341
|
}
|
|
2348
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2349
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2350
2344
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2351
2345
|
try log.boost(ident, .SpecialAttack, n);
|
|
2352
2346
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2373,7 +2367,11 @@ pub const Effects = struct {
|
|
|
2373
2367
|
}
|
|
2374
2368
|
|
|
2375
2369
|
if (move.effect.isStatDownChance()) {
|
|
2376
|
-
if (
|
|
2370
|
+
const chance = if (showdown)
|
|
2371
|
+
battle.rng.chance(u8, 85, 256)
|
|
2372
|
+
else
|
|
2373
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2374
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2377
2375
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2378
2376
|
return; // checkHit already checks for Invulnerable
|
|
2379
2377
|
}
|
|
@@ -2395,7 +2393,7 @@ pub const Effects = struct {
|
|
|
2395
2393
|
return try log.fail(foe_ident, .None);
|
|
2396
2394
|
}
|
|
2397
2395
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2398
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2396
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2399
2397
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2400
2398
|
try log.boost(foe_ident, .Attack, -1);
|
|
2401
2399
|
},
|
|
@@ -2413,7 +2411,7 @@ pub const Effects = struct {
|
|
|
2413
2411
|
return try log.fail(foe_ident, .None);
|
|
2414
2412
|
}
|
|
2415
2413
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2416
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2414
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2417
2415
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2418
2416
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2419
2417
|
},
|
|
@@ -2430,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2430
2428
|
return try log.fail(foe_ident, .None);
|
|
2431
2429
|
}
|
|
2432
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2433
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2431
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2434
2432
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2435
2433
|
try log.boost(foe_ident, .Speed, -1);
|
|
2436
2434
|
assert(boosts.spe >= -6);
|
|
@@ -2450,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2450
2448
|
return try log.fail(foe_ident, .None);
|
|
2451
2449
|
}
|
|
2452
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2453
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2454
2452
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2455
2453
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2456
2454
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2469,7 +2467,8 @@ pub const Effects = struct {
|
|
|
2469
2467
|
}
|
|
2470
2468
|
};
|
|
2471
2469
|
|
|
2472
|
-
fn unmodifiedStats(battle: anytype,
|
|
2470
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2471
|
+
const side = battle.side(who);
|
|
2473
2472
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2474
2473
|
const id = ID.from(side.active.volatiles.transform);
|
|
2475
2474
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2483,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2483
2482
|
}
|
|
2484
2483
|
}
|
|
2485
2484
|
|
|
2486
|
-
|
|
2485
|
+
fn isForced(active: ActivePokemon) bool {
|
|
2487
2486
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2488
2487
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2489
2488
|
}
|
|
@@ -2515,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2515
2514
|
volatiles.LeechSeed = false;
|
|
2516
2515
|
try log.end(ident, .LeechSeed);
|
|
2517
2516
|
}
|
|
2518
|
-
if (
|
|
2517
|
+
if (volatiles.Toxic) {
|
|
2519
2518
|
volatiles.Toxic = false;
|
|
2520
|
-
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
|
|
2519
|
+
// volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
|
|
2520
|
+
if (showdown) {
|
|
2521
|
+
volatiles.toxic = 0;
|
|
2522
|
+
try log.end(ident, .Toxic);
|
|
2523
|
+
}
|
|
2521
2524
|
}
|
|
2522
2525
|
if (volatiles.LightScreen) {
|
|
2523
2526
|
volatiles.LightScreen = false;
|
|
@@ -2527,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2527
2530
|
volatiles.Reflect = false;
|
|
2528
2531
|
try log.end(ident, .Reflect);
|
|
2529
2532
|
}
|
|
2530
|
-
if (showdown and volatiles.Toxic) {
|
|
2531
|
-
volatiles.Toxic = false;
|
|
2532
|
-
// Pokémon Showdown erroneously clears the toxic counter
|
|
2533
|
-
volatiles.toxic = 0;
|
|
2534
|
-
try log.end(ident, .Toxic);
|
|
2535
|
-
}
|
|
2536
2533
|
}
|
|
2537
2534
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
inline fn damage(battle: anytype) u8 {
|
|
2550
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2551
|
-
while (true) {
|
|
2552
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2553
|
-
if (r >= 217) return r;
|
|
2535
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2536
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2537
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2538
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2539
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2540
|
+
side.active.volatiles.Charging = false;
|
|
2541
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2542
|
+
return true;
|
|
2554
2543
|
}
|
|
2555
2544
|
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2556
2547
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2563
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2564
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
inline fn confused(battle: anytype) bool {
|
|
2568
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2569
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2573
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2574
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
inline fn confusionChance(battle: anytype) bool {
|
|
2578
|
-
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2579
|
-
return battle.rng.next() < Gen12.percent(10);
|
|
2580
|
-
}
|
|
2548
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2549
|
+
var volatiles = &active.volatiles;
|
|
2550
|
+
assert(volatiles.Thrashing);
|
|
2551
|
+
if (volatiles.attacks > 0) return false;
|
|
2581
2552
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2553
|
+
volatiles.Thrashing = false;
|
|
2554
|
+
volatiles.Confusion = true;
|
|
2555
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2556
|
+
battle.rng.range(u8, 2, 6)
|
|
2557
|
+
else
|
|
2558
|
+
(battle.rng.next() & 3) + 2);
|
|
2586
2559
|
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2590
|
-
}
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2591
2562
|
|
|
2592
|
-
|
|
2593
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2594
|
-
while (true) {
|
|
2595
|
-
const r = battle.rng.next() & 7;
|
|
2596
|
-
if (r != 0) return @intCast(u3, r);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2563
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2599
2564
|
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2565
|
+
fn distribution(battle: anytype) u3 {
|
|
2566
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2567
|
+
const r = (battle.rng.next() & 3);
|
|
2568
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2569
|
+
}
|
|
2604
2570
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
if (
|
|
2620
|
-
r -= 1;
|
|
2621
|
-
if (r == 0) break;
|
|
2622
|
-
}
|
|
2571
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2572
|
+
if (showdown) {
|
|
2573
|
+
if (check_pp == 0) {
|
|
2574
|
+
var i: usize = moves.len;
|
|
2575
|
+
while (i > 0) {
|
|
2576
|
+
i -= 1;
|
|
2577
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2581
|
+
var i: usize = 0;
|
|
2582
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2583
|
+
if (moves[i].pp > 0) {
|
|
2584
|
+
r -= 1;
|
|
2585
|
+
if (r == 0) break;
|
|
2623
2586
|
}
|
|
2624
|
-
return @intCast(u4, i + 1);
|
|
2625
2587
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
while (true) {
|
|
2629
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2630
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2588
|
+
return @intCast(u4, i + 1);
|
|
2631
2589
|
}
|
|
2632
2590
|
}
|
|
2633
2591
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2592
|
+
while (true) {
|
|
2593
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2594
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2637
2595
|
}
|
|
2638
|
-
|
|
2639
|
-
inline fn metronome(battle: anytype) Move {
|
|
2640
|
-
if (showdown) {
|
|
2641
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2642
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2643
|
-
return @intToEnum(Move, r + mod);
|
|
2644
|
-
}
|
|
2645
|
-
while (true) {
|
|
2646
|
-
const r = battle.rng.next();
|
|
2647
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2648
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2649
|
-
return @intToEnum(Move, r);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2654
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2655
|
-
while (true) {
|
|
2656
|
-
const r = battle.rng.next();
|
|
2657
|
-
if (r < max) return r;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2662
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2663
|
-
const r = (battle.rng.next() & 3);
|
|
2664
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
inline fn unboost(battle: anytype) bool {
|
|
2668
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2669
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2670
|
-
}
|
|
2671
|
-
};
|
|
2596
|
+
}
|
|
2672
2597
|
|
|
2673
2598
|
test "RNG agreement" {
|
|
2674
2599
|
if (!showdown) return;
|
|
@@ -2768,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2768
2693
|
while (slot <= 4) : (slot += 1) {
|
|
2769
2694
|
const m = active.moves[slot - 1];
|
|
2770
2695
|
if (m.id == .None) break;
|
|
2771
|
-
if (m.id ==
|
|
2772
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2773
|
-
const
|
|
2774
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2775
|
-
const s = if (struggle) 0 else slot;
|
|
2696
|
+
if (m.id == side.last_selected_move) {
|
|
2697
|
+
// Pokémon Showdown displays Struggle if limited to Bide when out of PP
|
|
2698
|
+
const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
|
|
2776
2699
|
out[n] = .{ .type = .Move, .data = s };
|
|
2777
2700
|
n += 1;
|
|
2778
2701
|
return n;
|