@pkmn/engine 0.1.0-dev.7cd30bd9 → 0.1.0-dev.7f37a47c
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 +29 -39
- 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 +271 -349
- 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/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
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
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;
|
|
@@ -512,10 +504,9 @@ fn beforeMove(
|
|
|
512
504
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
513
505
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
514
506
|
|
|
515
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
516
|
-
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
517
|
-
// from the same original hit
|
|
507
|
+
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
518
508
|
if (showdown and volatiles.Flinch) {
|
|
509
|
+
volatiles.Flinch = false;
|
|
519
510
|
try log.cant(ident, .Flinch);
|
|
520
511
|
return .done;
|
|
521
512
|
}
|
|
@@ -537,9 +528,14 @@ fn beforeMove(
|
|
|
537
528
|
} else {
|
|
538
529
|
try log.activate(ident, .Confusion);
|
|
539
530
|
|
|
540
|
-
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) {
|
|
541
537
|
assert(!volatiles.MultiHit);
|
|
542
|
-
|
|
538
|
+
volatiles.state = 0;
|
|
543
539
|
volatiles.Bide = false;
|
|
544
540
|
volatiles.Thrashing = false;
|
|
545
541
|
volatiles.MultiHit = false;
|
|
@@ -563,12 +559,8 @@ fn beforeMove(
|
|
|
563
559
|
player
|
|
564
560
|
else
|
|
565
561
|
player.foe();
|
|
566
|
-
|
|
567
|
-
const uncapped = battle.last_damage;
|
|
568
562
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
569
563
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
570
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
571
|
-
if (showdown) battle.last_damage = uncapped;
|
|
572
564
|
|
|
573
565
|
return .done;
|
|
574
566
|
}
|
|
@@ -577,15 +569,22 @@ fn beforeMove(
|
|
|
577
569
|
|
|
578
570
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
579
571
|
|
|
580
|
-
if (Status.is(stored.status, .PAR)
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
588
|
}
|
|
590
589
|
|
|
591
590
|
if (volatiles.Bide) {
|
|
@@ -612,18 +611,7 @@ fn beforeMove(
|
|
|
612
611
|
return .done;
|
|
613
612
|
}
|
|
614
613
|
|
|
615
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
616
614
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
617
|
-
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
618
|
-
|
|
619
|
-
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
620
|
-
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
621
|
-
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
622
|
-
if (showdown) {
|
|
623
|
-
residual.* = false;
|
|
624
|
-
try handleResidual(battle, player, log);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
615
|
return .done;
|
|
628
616
|
}
|
|
629
617
|
|
|
@@ -666,7 +654,6 @@ fn canMove(
|
|
|
666
654
|
auto: bool,
|
|
667
655
|
skip_pp: bool,
|
|
668
656
|
from: ?Move,
|
|
669
|
-
residual: *bool,
|
|
670
657
|
log: anytype,
|
|
671
658
|
) !bool {
|
|
672
659
|
var side = battle.side(player);
|
|
@@ -701,7 +688,7 @@ fn canMove(
|
|
|
701
688
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
702
689
|
|
|
703
690
|
if (move.effect.onBegin()) {
|
|
704
|
-
try onBegin(battle, player, move, mslot,
|
|
691
|
+
try onBegin(battle, player, move, mslot, log);
|
|
705
692
|
return false;
|
|
706
693
|
}
|
|
707
694
|
|
|
@@ -780,21 +767,16 @@ fn doMove(
|
|
|
780
767
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
781
768
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
782
769
|
if (immune and move.effect != .Binding) break :miss true;
|
|
783
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
784
|
-
battle.last_damage = 0;
|
|
785
|
-
break :miss true;
|
|
786
|
-
}
|
|
770
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
787
771
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
788
772
|
};
|
|
789
773
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
790
774
|
|
|
791
|
-
var late = showdown and move.effect != .Explode;
|
|
792
775
|
const skip = status or immune;
|
|
793
776
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
794
777
|
if (showdown and move.effect.isMulti()) {
|
|
795
778
|
Effects.multiHit(battle, player, move);
|
|
796
779
|
hits = side.active.volatiles.attacks;
|
|
797
|
-
late = false;
|
|
798
780
|
}
|
|
799
781
|
|
|
800
782
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -828,7 +810,7 @@ fn doMove(
|
|
|
828
810
|
}
|
|
829
811
|
|
|
830
812
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
831
|
-
if (showdown and skip) battle.last_damage = 0;
|
|
813
|
+
if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
|
|
832
814
|
|
|
833
815
|
miss = if (showdown or skip)
|
|
834
816
|
miss
|
|
@@ -875,9 +857,15 @@ fn doMove(
|
|
|
875
857
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
876
858
|
} else if (move.effect == .Explode) {
|
|
877
859
|
try Effects.explode(battle, player);
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
+
}
|
|
881
869
|
}
|
|
882
870
|
return null;
|
|
883
871
|
}
|
|
@@ -890,9 +878,6 @@ fn doMove(
|
|
|
890
878
|
hits = side.active.volatiles.attacks;
|
|
891
879
|
}
|
|
892
880
|
|
|
893
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
894
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
895
|
-
|
|
896
881
|
var nullified = false;
|
|
897
882
|
var hit: u4 = 0;
|
|
898
883
|
while (hit < hits) {
|
|
@@ -908,7 +893,9 @@ fn doMove(
|
|
|
908
893
|
if (hit == 0 and ohko) try log.ohko();
|
|
909
894
|
hit += 1;
|
|
910
895
|
if (foe.stored().hp == 0) break;
|
|
911
|
-
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
|
+
}
|
|
912
899
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
913
900
|
if (nullified) break;
|
|
914
901
|
}
|
|
@@ -945,9 +932,6 @@ fn doMove(
|
|
|
945
932
|
|
|
946
933
|
if (foe.stored().hp == 0) return null;
|
|
947
934
|
|
|
948
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
949
|
-
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
950
|
-
|
|
951
935
|
if (!move.effect.isSpecial()) {
|
|
952
936
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
953
937
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -959,9 +943,7 @@ fn doMove(
|
|
|
959
943
|
if (move.effect == .Twineedle) {
|
|
960
944
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
961
945
|
} else if (move.effect == .Disable) {
|
|
962
|
-
|
|
963
|
-
if (showdown) try buildRage(battle, player.foe(), log);
|
|
964
|
-
return result;
|
|
946
|
+
return Effects.disable(battle, player, move, log);
|
|
965
947
|
} else {
|
|
966
948
|
try moveEffect(battle, player, move, log);
|
|
967
949
|
}
|
|
@@ -979,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
979
961
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
980
962
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
981
963
|
|
|
982
|
-
return
|
|
964
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
965
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
983
966
|
}
|
|
984
967
|
|
|
985
968
|
fn calcDamage(
|
|
@@ -1083,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1083
1066
|
|
|
1084
1067
|
fn randomizeDamage(battle: anytype) void {
|
|
1085
1068
|
if (battle.last_damage <= 1) return;
|
|
1086
|
-
|
|
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
|
+
|
|
1087
1079
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1088
1080
|
}
|
|
1089
1081
|
|
|
1090
1082
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1091
1083
|
const side = battle.side(player);
|
|
1092
|
-
const foe = battle.foe(player);
|
|
1093
1084
|
|
|
1094
1085
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1095
1086
|
|
|
1096
1087
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1097
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1088
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1098
1089
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1099
1090
|
.SonicBoom => 20,
|
|
1100
1091
|
.DragonRage => 40,
|
|
1101
1092
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1102
1093
|
.Psywave => power: {
|
|
1103
1094
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
+
}
|
|
1107
1105
|
},
|
|
1108
1106
|
else => unreachable,
|
|
1109
1107
|
};
|
|
1110
1108
|
|
|
1111
1109
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1112
1110
|
|
|
1113
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1114
1111
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1115
|
-
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1116
|
-
|
|
1117
1112
|
return null;
|
|
1118
1113
|
}
|
|
1119
1114
|
|
|
@@ -1221,7 +1216,7 @@ fn mirrorMove(
|
|
|
1221
1216
|
|
|
1222
1217
|
incrementPP(side, mslot);
|
|
1223
1218
|
|
|
1224
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove,
|
|
1219
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1225
1220
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1226
1221
|
}
|
|
1227
1222
|
|
|
@@ -1235,10 +1230,22 @@ fn metronome(
|
|
|
1235
1230
|
) !?Result {
|
|
1236
1231
|
var side = battle.side(player);
|
|
1237
1232
|
|
|
1238
|
-
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
|
+
|
|
1239
1246
|
incrementPP(side, mslot);
|
|
1240
1247
|
|
|
1241
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome,
|
|
1248
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1242
1249
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1243
1250
|
}
|
|
1244
1251
|
|
|
@@ -1271,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1271
1278
|
if (move.effect == .Swift) return true;
|
|
1272
1279
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1273
1280
|
}
|
|
1274
|
-
if (move.effect == .DreamEater and
|
|
1275
|
-
(showdown and foe.active.volatiles.Substitute)))
|
|
1276
|
-
{
|
|
1281
|
+
if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
|
|
1277
1282
|
immune.* = true;
|
|
1278
1283
|
if (showdown) return false;
|
|
1279
1284
|
break :miss true;
|
|
@@ -1293,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1293
1298
|
}
|
|
1294
1299
|
|
|
1295
1300
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
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));
|
|
1302
1305
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1303
1306
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1304
1307
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1305
1308
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1306
1309
|
accuracy = @min(255, @max(1, accuracy));
|
|
1307
1310
|
|
|
1308
|
-
|
|
1309
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1310
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1311
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1311
1312
|
|
|
1312
1313
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1313
|
-
break :miss
|
|
1314
|
+
break :miss if (showdown)
|
|
1315
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1316
|
+
else
|
|
1317
|
+
battle.rng.next() >= accuracy;
|
|
1314
1318
|
};
|
|
1315
1319
|
|
|
1316
1320
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1425,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1425
1429
|
}
|
|
1426
1430
|
|
|
1427
1431
|
if (volatiles.LeechSeed) {
|
|
1428
|
-
var foe = battle.foe(player);
|
|
1429
|
-
var foe_stored = foe.stored();
|
|
1430
|
-
const foe_ident = battle.active(player.foe());
|
|
1431
|
-
|
|
1432
|
-
if (foe_stored.hp == 0) {
|
|
1433
|
-
assert(showdown);
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
1432
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1438
1433
|
|
|
1439
1434
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1445,12 +1440,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1445
1440
|
const amount = @min(damage, stored.hp);
|
|
1446
1441
|
stored.hp -= amount;
|
|
1447
1442
|
|
|
1443
|
+
var foe = battle.foe(player);
|
|
1444
|
+
var foe_stored = foe.stored();
|
|
1445
|
+
const foe_ident = battle.active(player.foe());
|
|
1446
|
+
|
|
1448
1447
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1449
|
-
if (amount > 0)
|
|
1450
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1451
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1452
|
-
if (showdown) battle.last_damage = damage;
|
|
1453
|
-
}
|
|
1448
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1454
1449
|
|
|
1455
1450
|
const before = foe_stored.hp;
|
|
1456
1451
|
// Uncapped damage is added back to the foe
|
|
@@ -1485,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
|
|
|
1485
1480
|
|
|
1486
1481
|
fn checkEBC(battle: anytype) bool {
|
|
1487
1482
|
for (battle.sides, 0..) |side, i| {
|
|
1488
|
-
const foe = battle.sides[~@intCast(u1, i)];
|
|
1489
|
-
|
|
1490
1483
|
var foe_all_ghosts = true;
|
|
1491
1484
|
var foe_all_transform = true;
|
|
1492
|
-
for (foe.order, 0..) |id, j| {
|
|
1493
|
-
if (id == 0) break;
|
|
1494
|
-
const active = j == 0;
|
|
1495
|
-
const pokemon = foe.pokemon[id - 1];
|
|
1496
1485
|
|
|
1497
|
-
|
|
1498
|
-
|
|
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);
|
|
1499
1490
|
foe_all_ghosts = foe_all_ghosts and ghost;
|
|
1500
1491
|
foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
|
|
1501
|
-
for (
|
|
1492
|
+
for (pokemon.moves) |m| {
|
|
1502
1493
|
if (m.id == .None) break :transform true;
|
|
1503
1494
|
if (m.id != .Transform) break :transform false;
|
|
1504
1495
|
}
|
|
@@ -1506,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1506
1497
|
};
|
|
1507
1498
|
}
|
|
1508
1499
|
|
|
1509
|
-
for (side.
|
|
1510
|
-
if (id == 0) break;
|
|
1511
|
-
const active = j == 0;
|
|
1512
|
-
const pokemon = side.pokemon[id - 1];
|
|
1513
|
-
|
|
1500
|
+
for (side.pokemon) |pokemon| {
|
|
1514
1501
|
if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
|
|
1515
1502
|
const transform = foe_all_transform and transform: {
|
|
1516
|
-
for (
|
|
1503
|
+
for (pokemon.moves) |m| {
|
|
1517
1504
|
if (m.id == .None) break :transform true;
|
|
1518
1505
|
if (m.id != .Transform) break :transform false;
|
|
1519
1506
|
}
|
|
@@ -1521,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1521
1508
|
};
|
|
1522
1509
|
if (transform) continue;
|
|
1523
1510
|
const no_pp = foe_all_ghosts and no_pp: {
|
|
1524
|
-
for (
|
|
1511
|
+
for (pokemon.moves) |m| {
|
|
1525
1512
|
if (m.pp != 0) break :no_pp false;
|
|
1526
1513
|
}
|
|
1527
1514
|
break :no_pp true;
|
|
@@ -1537,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1537
1524
|
return false;
|
|
1538
1525
|
}
|
|
1539
1526
|
|
|
1540
|
-
fn
|
|
1541
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1542
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1543
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1544
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1545
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1546
|
-
m
|
|
1547
|
-
else
|
|
1548
|
-
side.last_selected_move;
|
|
1549
|
-
if (m != .None and m == last) {
|
|
1550
|
-
side.active.volatiles.Charging = false;
|
|
1551
|
-
try log.disabled(ident, last);
|
|
1552
|
-
return true;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
return false;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1559
|
-
const side = battle.side(who);
|
|
1560
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1561
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1566
|
-
var volatiles = &active.volatiles;
|
|
1567
|
-
assert(volatiles.Thrashing);
|
|
1568
|
-
if (volatiles.attacks > 0) return false;
|
|
1569
|
-
|
|
1570
|
-
volatiles.Thrashing = false;
|
|
1571
|
-
volatiles.Confusion = true;
|
|
1572
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1573
|
-
return true;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
inline fn onBegin(
|
|
1577
|
-
battle: anytype,
|
|
1578
|
-
player: Player,
|
|
1579
|
-
move: Move.Data,
|
|
1580
|
-
mslot: u8,
|
|
1581
|
-
residual: *bool,
|
|
1582
|
-
log: anytype,
|
|
1583
|
-
) !void {
|
|
1527
|
+
inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
|
|
1584
1528
|
assert(move.effect.onBegin());
|
|
1585
1529
|
return switch (move.effect) {
|
|
1586
1530
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1596,7 +1540,7 @@ inline fn onBegin(
|
|
|
1596
1540
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1597
1541
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1598
1542
|
.Splash => Effects.splash(battle, player, log),
|
|
1599
|
-
.Substitute => Effects.substitute(battle, player,
|
|
1543
|
+
.Substitute => Effects.substitute(battle, player, log),
|
|
1600
1544
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1601
1545
|
.Transform => Effects.transform(battle, player, log),
|
|
1602
1546
|
else => unreachable,
|
|
@@ -1663,7 +1607,10 @@ pub const Effects = struct {
|
|
|
1663
1607
|
side.active.volatiles.Bide = true;
|
|
1664
1608
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1665
1609
|
side.active.volatiles.state = 0;
|
|
1666
|
-
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);
|
|
1667
1614
|
|
|
1668
1615
|
try log.start(battle.active(player), .Bide);
|
|
1669
1616
|
}
|
|
@@ -1685,8 +1632,14 @@ pub const Effects = struct {
|
|
|
1685
1632
|
return;
|
|
1686
1633
|
}
|
|
1687
1634
|
|
|
1688
|
-
|
|
1689
|
-
|
|
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;
|
|
1690
1643
|
|
|
1691
1644
|
foe_stored.status = Status.init(.BRN);
|
|
1692
1645
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1710,7 +1663,11 @@ pub const Effects = struct {
|
|
|
1710
1663
|
const sub = foe.active.volatiles.Substitute;
|
|
1711
1664
|
|
|
1712
1665
|
if (move.effect == .ConfusionChance) {
|
|
1713
|
-
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;
|
|
1714
1671
|
} else {
|
|
1715
1672
|
if (showdown) {
|
|
1716
1673
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1729,7 +1686,10 @@ pub const Effects = struct {
|
|
|
1729
1686
|
|
|
1730
1687
|
if (foe.active.volatiles.Confusion) return;
|
|
1731
1688
|
foe.active.volatiles.Confusion = true;
|
|
1732
|
-
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);
|
|
1733
1693
|
|
|
1734
1694
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1735
1695
|
}
|
|
@@ -1779,8 +1739,11 @@ pub const Effects = struct {
|
|
|
1779
1739
|
return Result.Error;
|
|
1780
1740
|
}
|
|
1781
1741
|
|
|
1782
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1783
|
-
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);
|
|
1784
1747
|
|
|
1785
1748
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1786
1749
|
return null;
|
|
@@ -1790,11 +1753,6 @@ pub const Effects = struct {
|
|
|
1790
1753
|
var side = battle.side(player);
|
|
1791
1754
|
var stored = side.stored();
|
|
1792
1755
|
|
|
1793
|
-
if (battle.last_damage == 0) {
|
|
1794
|
-
assert(showdown);
|
|
1795
|
-
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
1756
|
const drain = @max(battle.last_damage / 2, 1);
|
|
1799
1757
|
battle.last_damage = drain;
|
|
1800
1758
|
|
|
@@ -1819,7 +1777,15 @@ pub const Effects = struct {
|
|
|
1819
1777
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1820
1778
|
|
|
1821
1779
|
if (volatiles.Substitute) return;
|
|
1822
|
-
|
|
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;
|
|
1823
1789
|
|
|
1824
1790
|
volatiles.Flinch = true;
|
|
1825
1791
|
volatiles.Recharging = false;
|
|
@@ -1845,8 +1811,12 @@ pub const Effects = struct {
|
|
|
1845
1811
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1846
1812
|
}
|
|
1847
1813
|
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
+
|
|
1850
1820
|
// Freeze Clause Mod
|
|
1851
1821
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1852
1822
|
|
|
@@ -1991,7 +1961,7 @@ pub const Effects = struct {
|
|
|
1991
1961
|
}
|
|
1992
1962
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1993
1963
|
|
|
1994
|
-
const rslot =
|
|
1964
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
1995
1965
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1996
1966
|
|
|
1997
1967
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2012,8 +1982,7 @@ pub const Effects = struct {
|
|
|
2012
1982
|
assert(!side.active.volatiles.MultiHit);
|
|
2013
1983
|
side.active.volatiles.MultiHit = true;
|
|
2014
1984
|
|
|
2015
|
-
side.active.volatiles.attacks =
|
|
2016
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
1985
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2017
1986
|
}
|
|
2018
1987
|
|
|
2019
1988
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2057,8 +2026,14 @@ pub const Effects = struct {
|
|
|
2057
2026
|
}
|
|
2058
2027
|
|
|
2059
2028
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2060
|
-
|
|
2061
|
-
|
|
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;
|
|
2062
2037
|
|
|
2063
2038
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2039
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2088,7 +2063,7 @@ pub const Effects = struct {
|
|
|
2088
2063
|
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2089
2064
|
if (!showdown)
|
|
2090
2065
|
.Poison
|
|
2091
|
-
else if (toxic ==
|
|
2066
|
+
else if (toxic == foe.active.volatiles.Toxic)
|
|
2092
2067
|
if (toxic) .Toxic else .Poison
|
|
2093
2068
|
else
|
|
2094
2069
|
.None
|
|
@@ -2101,7 +2076,16 @@ pub const Effects = struct {
|
|
|
2101
2076
|
|
|
2102
2077
|
if (move.effect == .Poison) {
|
|
2103
2078
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2104
|
-
} 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
|
+
}
|
|
2105
2089
|
|
|
2106
2090
|
foe_stored.status = Status.init(.PSN);
|
|
2107
2091
|
if (toxic) {
|
|
@@ -2115,8 +2099,10 @@ pub const Effects = struct {
|
|
|
2115
2099
|
|
|
2116
2100
|
fn rage(battle: anytype, player: Player) !void {
|
|
2117
2101
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2118
|
-
|
|
2102
|
+
if (volatiles.Rage) return;
|
|
2119
2103
|
volatiles.Rage = true;
|
|
2104
|
+
assert(!volatiles.Bide);
|
|
2105
|
+
volatiles.state = 0;
|
|
2120
2106
|
}
|
|
2121
2107
|
|
|
2122
2108
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2168,7 +2154,16 @@ pub const Effects = struct {
|
|
|
2168
2154
|
}
|
|
2169
2155
|
foe.active.volatiles.Recharging = false;
|
|
2170
2156
|
|
|
2171
|
-
|
|
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);
|
|
2172
2167
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2173
2168
|
}
|
|
2174
2169
|
|
|
@@ -2176,7 +2171,7 @@ pub const Effects = struct {
|
|
|
2176
2171
|
try log.activate(battle.active(player), .Splash);
|
|
2177
2172
|
}
|
|
2178
2173
|
|
|
2179
|
-
fn substitute(battle: anytype, player: Player,
|
|
2174
|
+
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2180
2175
|
var side = battle.side(player);
|
|
2181
2176
|
if (side.active.volatiles.Substitute) {
|
|
2182
2177
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2201,18 +2196,14 @@ pub const Effects = struct {
|
|
|
2201
2196
|
side.active.volatiles.substitute = hp + 1;
|
|
2202
2197
|
side.active.volatiles.Substitute = true;
|
|
2203
2198
|
try log.start(battle.active(player), .Substitute);
|
|
2204
|
-
if (hp > 0)
|
|
2205
|
-
try log.damage(battle.active(player), side.stored(), .None);
|
|
2206
|
-
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2207
|
-
}
|
|
2199
|
+
if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
|
|
2208
2200
|
}
|
|
2209
2201
|
|
|
2210
2202
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2211
2203
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2212
2204
|
|
|
2213
|
-
// Whirlwind/Roar should not roll to hit
|
|
2205
|
+
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2214
2206
|
_ = try checkHit(battle, player, move, log);
|
|
2215
|
-
battle.last_damage = 0;
|
|
2216
2207
|
}
|
|
2217
2208
|
|
|
2218
2209
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2221,7 +2212,11 @@ pub const Effects = struct {
|
|
|
2221
2212
|
assert(!volatiles.Bide);
|
|
2222
2213
|
|
|
2223
2214
|
volatiles.Thrashing = true;
|
|
2224
|
-
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);
|
|
2225
2220
|
}
|
|
2226
2221
|
|
|
2227
2222
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2263,7 +2258,7 @@ pub const Effects = struct {
|
|
|
2263
2258
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2264
2259
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2265
2260
|
|
|
2266
|
-
side.active.volatiles.attacks =
|
|
2261
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2267
2262
|
}
|
|
2268
2263
|
|
|
2269
2264
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2290,11 +2285,9 @@ pub const Effects = struct {
|
|
|
2290
2285
|
return try log.fail(ident, .None);
|
|
2291
2286
|
}
|
|
2292
2287
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2293
|
-
const stat = unmodifiedStats(battle,
|
|
2288
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2294
2289
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2295
2290
|
try log.boost(ident, reason, n);
|
|
2296
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2297
|
-
if (showdown and move.effect == .Rage) return;
|
|
2298
2291
|
},
|
|
2299
2292
|
.DefenseUp1, .DefenseUp2 => {
|
|
2300
2293
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2310,7 +2303,7 @@ pub const Effects = struct {
|
|
|
2310
2303
|
return try log.fail(ident, .None);
|
|
2311
2304
|
}
|
|
2312
2305
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2313
|
-
const stat = unmodifiedStats(battle,
|
|
2306
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2314
2307
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2315
2308
|
try log.boost(ident, .Defense, n);
|
|
2316
2309
|
},
|
|
@@ -2327,7 +2320,7 @@ pub const Effects = struct {
|
|
|
2327
2320
|
return try log.fail(ident, .None);
|
|
2328
2321
|
}
|
|
2329
2322
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2330
|
-
const stat = unmodifiedStats(battle,
|
|
2323
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2331
2324
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2332
2325
|
try log.boost(ident, .Speed, 2);
|
|
2333
2326
|
},
|
|
@@ -2347,7 +2340,7 @@ pub const Effects = struct {
|
|
|
2347
2340
|
return try log.fail(ident, .None);
|
|
2348
2341
|
}
|
|
2349
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2350
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2351
2344
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2352
2345
|
try log.boost(ident, .SpecialAttack, n);
|
|
2353
2346
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2374,7 +2367,11 @@ pub const Effects = struct {
|
|
|
2374
2367
|
}
|
|
2375
2368
|
|
|
2376
2369
|
if (move.effect.isStatDownChance()) {
|
|
2377
|
-
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;
|
|
2378
2375
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2379
2376
|
return; // checkHit already checks for Invulnerable
|
|
2380
2377
|
}
|
|
@@ -2396,7 +2393,7 @@ pub const Effects = struct {
|
|
|
2396
2393
|
return try log.fail(foe_ident, .None);
|
|
2397
2394
|
}
|
|
2398
2395
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2399
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2396
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2400
2397
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2401
2398
|
try log.boost(foe_ident, .Attack, -1);
|
|
2402
2399
|
},
|
|
@@ -2414,7 +2411,7 @@ pub const Effects = struct {
|
|
|
2414
2411
|
return try log.fail(foe_ident, .None);
|
|
2415
2412
|
}
|
|
2416
2413
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2417
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2414
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2418
2415
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2419
2416
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2420
2417
|
},
|
|
@@ -2431,7 +2428,7 @@ pub const Effects = struct {
|
|
|
2431
2428
|
return try log.fail(foe_ident, .None);
|
|
2432
2429
|
}
|
|
2433
2430
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2434
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2431
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2435
2432
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2436
2433
|
try log.boost(foe_ident, .Speed, -1);
|
|
2437
2434
|
assert(boosts.spe >= -6);
|
|
@@ -2451,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2451
2448
|
return try log.fail(foe_ident, .None);
|
|
2452
2449
|
}
|
|
2453
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2454
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2455
2452
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2456
2453
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2457
2454
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2470,7 +2467,8 @@ pub const Effects = struct {
|
|
|
2470
2467
|
}
|
|
2471
2468
|
};
|
|
2472
2469
|
|
|
2473
|
-
fn unmodifiedStats(battle: anytype,
|
|
2470
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2471
|
+
const side = battle.side(who);
|
|
2474
2472
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2475
2473
|
const id = ID.from(side.active.volatiles.transform);
|
|
2476
2474
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2484,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2484
2482
|
}
|
|
2485
2483
|
}
|
|
2486
2484
|
|
|
2487
|
-
|
|
2485
|
+
fn isForced(active: ActivePokemon) bool {
|
|
2488
2486
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2489
2487
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2490
2488
|
}
|
|
@@ -2516,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2516
2514
|
volatiles.LeechSeed = false;
|
|
2517
2515
|
try log.end(ident, .LeechSeed);
|
|
2518
2516
|
}
|
|
2519
|
-
if (
|
|
2517
|
+
if (volatiles.Toxic) {
|
|
2520
2518
|
volatiles.Toxic = false;
|
|
2521
|
-
// 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
|
+
}
|
|
2522
2524
|
}
|
|
2523
2525
|
if (volatiles.LightScreen) {
|
|
2524
2526
|
volatiles.LightScreen = false;
|
|
@@ -2528,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2528
2530
|
volatiles.Reflect = false;
|
|
2529
2531
|
try log.end(ident, .Reflect);
|
|
2530
2532
|
}
|
|
2531
|
-
if (showdown and volatiles.Toxic) {
|
|
2532
|
-
volatiles.Toxic = false;
|
|
2533
|
-
// Pokémon Showdown erroneously clears the toxic counter
|
|
2534
|
-
volatiles.toxic = 0;
|
|
2535
|
-
try log.end(ident, .Toxic);
|
|
2536
|
-
}
|
|
2537
2533
|
}
|
|
2538
2534
|
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
inline fn damage(battle: anytype) u8 {
|
|
2551
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2552
|
-
while (true) {
|
|
2553
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2554
|
-
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;
|
|
2555
2543
|
}
|
|
2556
2544
|
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2557
2547
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2564
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2565
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
inline fn confused(battle: anytype) bool {
|
|
2569
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2570
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2574
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2575
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
inline fn confusionChance(battle: anytype) bool {
|
|
2579
|
-
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2580
|
-
return battle.rng.next() < Gen12.percent(10);
|
|
2581
|
-
}
|
|
2548
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2549
|
+
var volatiles = &active.volatiles;
|
|
2550
|
+
assert(volatiles.Thrashing);
|
|
2551
|
+
if (volatiles.attacks > 0) return false;
|
|
2582
2552
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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);
|
|
2587
2559
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2591
|
-
}
|
|
2560
|
+
return true;
|
|
2561
|
+
}
|
|
2592
2562
|
|
|
2593
|
-
|
|
2594
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2595
|
-
while (true) {
|
|
2596
|
-
const r = battle.rng.next() & 7;
|
|
2597
|
-
if (r != 0) return @intCast(u3, r);
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2563
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2600
2564
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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
|
+
}
|
|
2605
2570
|
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
if (
|
|
2621
|
-
r -= 1;
|
|
2622
|
-
if (r == 0) break;
|
|
2623
|
-
}
|
|
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;
|
|
2624
2586
|
}
|
|
2625
|
-
return @intCast(u4, i + 1);
|
|
2626
2587
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
while (true) {
|
|
2630
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2631
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
inline fn disableDuration(battle: anytype) u4 {
|
|
2636
|
-
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2637
|
-
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
inline fn metronome(battle: anytype) Move {
|
|
2641
|
-
if (showdown) {
|
|
2642
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2643
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2644
|
-
return @intToEnum(Move, r + mod);
|
|
2645
|
-
}
|
|
2646
|
-
while (true) {
|
|
2647
|
-
const r = battle.rng.next();
|
|
2648
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2649
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2650
|
-
return @intToEnum(Move, r);
|
|
2588
|
+
return @intCast(u4, i + 1);
|
|
2651
2589
|
}
|
|
2652
2590
|
}
|
|
2653
2591
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const r = battle.rng.next();
|
|
2658
|
-
if (r < max) return r;
|
|
2659
|
-
}
|
|
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;
|
|
2660
2595
|
}
|
|
2661
|
-
|
|
2662
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2663
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2664
|
-
const r = (battle.rng.next() & 3);
|
|
2665
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
inline fn unboost(battle: anytype) bool {
|
|
2669
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2670
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2671
|
-
}
|
|
2672
|
-
};
|
|
2596
|
+
}
|
|
2673
2597
|
|
|
2674
2598
|
test "RNG agreement" {
|
|
2675
2599
|
if (!showdown) return;
|
|
@@ -2769,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2769
2693
|
while (slot <= 4) : (slot += 1) {
|
|
2770
2694
|
const m = active.moves[slot - 1];
|
|
2771
2695
|
if (m.id == .None) break;
|
|
2772
|
-
if (m.id ==
|
|
2773
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2774
|
-
const
|
|
2775
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2776
|
-
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;
|
|
2777
2699
|
out[n] = .{ .type = .Move, .data = s };
|
|
2778
2700
|
n += 1;
|
|
2779
2701
|
return n;
|