@pkmn/engine 0.1.0-dev.729b1e87 → 0.1.0-dev.7cd30bd9
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 +35 -7
- package/build/pkg/addon.d.ts +7 -0
- package/build/pkg/addon.js +111 -0
- package/build/pkg/addon.js.map +1 -0
- package/build/pkg/data.d.ts +1 -2
- package/build/pkg/data.js +3 -5
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/protocol.js +1 -1
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +383 -0
- package/build/test/display.js.map +1 -0
- package/build/tools/debug.js +133 -0
- package/build/tools/debug.js.map +1 -0
- package/build.zig +17 -27
- package/package.json +39 -29
- package/src/bin/install-pkmn-engine +58 -31
- package/src/bin/pkmn-debug +62 -0
- package/src/lib/binding/node.zig +4 -4
- package/src/lib/binding/wasm.zig +2 -2
- package/src/lib/gen1/README.md +37 -24
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/helpers.zig +0 -4
- package/src/lib/gen1/mechanics.zig +317 -256
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +3 -3
- package/src/test/display/pkmn.html.tmpl +368 -0
|
@@ -56,6 +56,8 @@ 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
|
+
|
|
59
61
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
60
62
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
61
63
|
if (battle.turn == 0) return start(battle, log);
|
|
@@ -185,6 +187,7 @@ fn selectMove(
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
// move select
|
|
190
|
+
volatiles.state = 0;
|
|
188
191
|
if (choice.data == 0) {
|
|
189
192
|
const struggle = ok: {
|
|
190
193
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -301,11 +304,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
301
304
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
302
305
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
303
306
|
|
|
304
|
-
const p1 =
|
|
305
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
-
else
|
|
307
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
-
|
|
307
|
+
const p1 = Rolls.speedTie(battle);
|
|
309
308
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
310
309
|
|
|
311
310
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -315,7 +314,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
315
314
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
316
315
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
317
316
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
318
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
317
|
+
// that out and enforce host ordering of events, so P1 just goes first regardless of who
|
|
319
318
|
// won the original coin flip
|
|
320
319
|
if (t1 and t2) return .P1;
|
|
321
320
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -347,6 +346,7 @@ fn doTurn(
|
|
|
347
346
|
if (try checkFaint(battle, player, log)) |r| return r;
|
|
348
347
|
} else if (foe_choice.type == .Pass) return null;
|
|
349
348
|
|
|
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, log)) {
|
|
437
|
+
switch (try beforeMove(battle, player, from, residual, log)) {
|
|
438
438
|
.done => return null,
|
|
439
439
|
.skip_can => skip_can = true,
|
|
440
440
|
.skip_pp => skip_pp = true,
|
|
@@ -442,14 +442,22 @@ 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, log))
|
|
445
|
+
if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
446
448
|
|
|
447
449
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
448
450
|
}
|
|
449
451
|
|
|
450
|
-
const BeforeMove =
|
|
452
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
451
453
|
|
|
452
|
-
fn beforeMove(
|
|
454
|
+
fn beforeMove(
|
|
455
|
+
battle: anytype,
|
|
456
|
+
player: Player,
|
|
457
|
+
from: ?Move,
|
|
458
|
+
residual: *bool,
|
|
459
|
+
log: anytype,
|
|
460
|
+
) !BeforeMove {
|
|
453
461
|
var side = battle.side(player);
|
|
454
462
|
const foe = battle.foe(player);
|
|
455
463
|
var active = &side.active;
|
|
@@ -504,9 +512,10 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
504
512
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
505
513
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
506
514
|
|
|
507
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
515
|
+
// Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
|
|
516
|
+
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
517
|
+
// from the same original hit
|
|
508
518
|
if (showdown and volatiles.Flinch) {
|
|
509
|
-
volatiles.Flinch = false;
|
|
510
519
|
try log.cant(ident, .Flinch);
|
|
511
520
|
return .done;
|
|
512
521
|
}
|
|
@@ -528,14 +537,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
528
537
|
} else {
|
|
529
538
|
try log.activate(ident, .Confusion);
|
|
530
539
|
|
|
531
|
-
|
|
532
|
-
!battle.rng.chance(u8, 128, 256)
|
|
533
|
-
else
|
|
534
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
535
|
-
|
|
536
|
-
if (confused) {
|
|
540
|
+
if (Rolls.confused(battle)) {
|
|
537
541
|
assert(!volatiles.MultiHit);
|
|
538
|
-
volatiles.state = 0;
|
|
542
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
539
543
|
volatiles.Bide = false;
|
|
540
544
|
volatiles.Thrashing = false;
|
|
541
545
|
volatiles.MultiHit = false;
|
|
@@ -559,8 +563,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
559
563
|
player
|
|
560
564
|
else
|
|
561
565
|
player.foe();
|
|
566
|
+
|
|
567
|
+
const uncapped = battle.last_damage;
|
|
562
568
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
563
569
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
570
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
571
|
+
if (showdown) battle.last_damage = uncapped;
|
|
564
572
|
|
|
565
573
|
return .done;
|
|
566
574
|
}
|
|
@@ -569,22 +577,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
569
577
|
|
|
570
578
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
571
579
|
|
|
572
|
-
if (Status.is(stored.status, .PAR)) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
}
|
|
580
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
581
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
582
|
+
volatiles.Bide = false;
|
|
583
|
+
volatiles.Thrashing = false;
|
|
584
|
+
volatiles.Charging = false;
|
|
585
|
+
volatiles.Binding = false;
|
|
586
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
587
|
+
try log.cant(ident, .Paralysis);
|
|
588
|
+
return .done;
|
|
588
589
|
}
|
|
589
590
|
|
|
590
591
|
if (volatiles.Bide) {
|
|
@@ -611,7 +612,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
611
612
|
return .done;
|
|
612
613
|
}
|
|
613
614
|
|
|
615
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
614
616
|
_ = 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
|
+
|
|
615
627
|
return .done;
|
|
616
628
|
}
|
|
617
629
|
|
|
@@ -654,6 +666,7 @@ fn canMove(
|
|
|
654
666
|
auto: bool,
|
|
655
667
|
skip_pp: bool,
|
|
656
668
|
from: ?Move,
|
|
669
|
+
residual: *bool,
|
|
657
670
|
log: anytype,
|
|
658
671
|
) !bool {
|
|
659
672
|
var side = battle.side(player);
|
|
@@ -688,7 +701,7 @@ fn canMove(
|
|
|
688
701
|
try log.move(player_ident, side.last_selected_move, battle.active(target), f);
|
|
689
702
|
|
|
690
703
|
if (move.effect.onBegin()) {
|
|
691
|
-
try onBegin(battle, player, move, mslot, log);
|
|
704
|
+
try onBegin(battle, player, move, mslot, residual, log);
|
|
692
705
|
return false;
|
|
693
706
|
}
|
|
694
707
|
|
|
@@ -767,16 +780,21 @@ fn doMove(
|
|
|
767
780
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
768
781
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
769
782
|
if (immune and move.effect != .Binding) break :miss true;
|
|
770
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
783
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
784
|
+
battle.last_damage = 0;
|
|
785
|
+
break :miss true;
|
|
786
|
+
}
|
|
771
787
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
772
788
|
};
|
|
773
789
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
774
790
|
|
|
791
|
+
var late = showdown and move.effect != .Explode;
|
|
775
792
|
const skip = status or immune;
|
|
776
793
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
777
794
|
if (showdown and move.effect.isMulti()) {
|
|
778
795
|
Effects.multiHit(battle, player, move);
|
|
779
796
|
hits = side.active.volatiles.attacks;
|
|
797
|
+
late = false;
|
|
780
798
|
}
|
|
781
799
|
|
|
782
800
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -810,7 +828,7 @@ fn doMove(
|
|
|
810
828
|
}
|
|
811
829
|
|
|
812
830
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
813
|
-
if (showdown and skip
|
|
831
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
814
832
|
|
|
815
833
|
miss = if (showdown or skip)
|
|
816
834
|
miss
|
|
@@ -857,15 +875,9 @@ fn doMove(
|
|
|
857
875
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
858
876
|
} else if (move.effect == .Explode) {
|
|
859
877
|
try Effects.explode(battle, player);
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
}
|
|
878
|
+
try buildRage(battle, player.foe(), log);
|
|
879
|
+
} else if (showdown and move.effect == .Disable) {
|
|
880
|
+
try buildRage(battle, player.foe(), log);
|
|
869
881
|
}
|
|
870
882
|
return null;
|
|
871
883
|
}
|
|
@@ -878,6 +890,9 @@ fn doMove(
|
|
|
878
890
|
hits = side.active.volatiles.attacks;
|
|
879
891
|
}
|
|
880
892
|
|
|
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
|
+
|
|
881
896
|
var nullified = false;
|
|
882
897
|
var hit: u4 = 0;
|
|
883
898
|
while (hit < hits) {
|
|
@@ -893,9 +908,7 @@ fn doMove(
|
|
|
893
908
|
if (hit == 0 and ohko) try log.ohko();
|
|
894
909
|
hit += 1;
|
|
895
910
|
if (foe.stored().hp == 0) break;
|
|
896
|
-
if (
|
|
897
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
898
|
-
}
|
|
911
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
899
912
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
900
913
|
if (nullified) break;
|
|
901
914
|
}
|
|
@@ -932,6 +945,9 @@ fn doMove(
|
|
|
932
945
|
|
|
933
946
|
if (foe.stored().hp == 0) return null;
|
|
934
947
|
|
|
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
|
+
|
|
935
951
|
if (!move.effect.isSpecial()) {
|
|
936
952
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
937
953
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -943,7 +959,9 @@ fn doMove(
|
|
|
943
959
|
if (move.effect == .Twineedle) {
|
|
944
960
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
945
961
|
} else if (move.effect == .Disable) {
|
|
946
|
-
|
|
962
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
963
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
964
|
+
return result;
|
|
947
965
|
} else {
|
|
948
966
|
try moveEffect(battle, player, move, log);
|
|
949
967
|
}
|
|
@@ -961,8 +979,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
961
979
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
962
980
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
963
981
|
|
|
964
|
-
|
|
965
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
982
|
+
return Rolls.criticalHit(battle, chance);
|
|
966
983
|
}
|
|
967
984
|
|
|
968
985
|
fn calcDamage(
|
|
@@ -1066,49 +1083,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1066
1083
|
|
|
1067
1084
|
fn randomizeDamage(battle: anytype) void {
|
|
1068
1085
|
if (battle.last_damage <= 1) return;
|
|
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
|
+
const random = Rolls.damage(battle);
|
|
1079
1087
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1080
1088
|
}
|
|
1081
1089
|
|
|
1082
1090
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1083
1091
|
const side = battle.side(player);
|
|
1092
|
+
const foe = battle.foe(player);
|
|
1084
1093
|
|
|
1085
1094
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1086
1095
|
|
|
1087
1096
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1088
|
-
.SuperFang => @max(
|
|
1097
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1089
1098
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1090
1099
|
.SonicBoom => 20,
|
|
1091
1100
|
.DragonRage => 40,
|
|
1092
1101
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1093
1102
|
.Psywave => power: {
|
|
1094
1103
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
}
|
|
1104
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1105
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1106
|
+
break :power Rolls.psywave(battle, max);
|
|
1105
1107
|
},
|
|
1106
1108
|
else => unreachable,
|
|
1107
1109
|
};
|
|
1108
1110
|
|
|
1109
1111
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1110
1112
|
|
|
1113
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1111
1114
|
_ = 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
|
+
|
|
1112
1117
|
return null;
|
|
1113
1118
|
}
|
|
1114
1119
|
|
|
@@ -1216,7 +1221,7 @@ fn mirrorMove(
|
|
|
1216
1221
|
|
|
1217
1222
|
incrementPP(side, mslot);
|
|
1218
1223
|
|
|
1219
|
-
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
|
|
1224
|
+
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
|
|
1220
1225
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1221
1226
|
}
|
|
1222
1227
|
|
|
@@ -1230,22 +1235,10 @@ fn metronome(
|
|
|
1230
1235
|
) !?Result {
|
|
1231
1236
|
var side = battle.side(player);
|
|
1232
1237
|
|
|
1233
|
-
side.last_selected_move =
|
|
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
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1246
1239
|
incrementPP(side, mslot);
|
|
1247
1240
|
|
|
1248
|
-
if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
|
|
1241
|
+
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
1249
1242
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
1250
1243
|
}
|
|
1251
1244
|
|
|
@@ -1300,23 +1293,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1300
1293
|
}
|
|
1301
1294
|
|
|
1302
1295
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1296
|
+
const state = side.active.volatiles.state;
|
|
1297
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1298
|
+
const overwritten = overwrite and state > 0;
|
|
1299
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1300
|
+
|
|
1301
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1307
1302
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1308
1303
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1309
1304
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1310
1305
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1311
1306
|
accuracy = @min(255, @max(1, accuracy));
|
|
1312
1307
|
|
|
1313
|
-
|
|
1308
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1309
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1310
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1314
1311
|
|
|
1315
1312
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1316
|
-
break :miss
|
|
1317
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1318
|
-
else
|
|
1319
|
-
battle.rng.next() >= accuracy;
|
|
1313
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1320
1314
|
};
|
|
1321
1315
|
|
|
1322
1316
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1431,6 +1425,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1431
1425
|
}
|
|
1432
1426
|
|
|
1433
1427
|
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
|
+
|
|
1434
1437
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1435
1438
|
|
|
1436
1439
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1442,12 +1445,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1442
1445
|
const amount = @min(damage, stored.hp);
|
|
1443
1446
|
stored.hp -= amount;
|
|
1444
1447
|
|
|
1445
|
-
var foe = battle.foe(player);
|
|
1446
|
-
var foe_stored = foe.stored();
|
|
1447
|
-
const foe_ident = battle.active(player.foe());
|
|
1448
|
-
|
|
1449
1448
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1450
|
-
if (amount > 0)
|
|
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
|
+
}
|
|
1451
1454
|
|
|
1452
1455
|
const before = foe_stored.hp;
|
|
1453
1456
|
// Uncapped damage is added back to the foe
|
|
@@ -1534,7 +1537,50 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1534
1537
|
return false;
|
|
1535
1538
|
}
|
|
1536
1539
|
|
|
1537
|
-
|
|
1540
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
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 {
|
|
1538
1584
|
assert(move.effect.onBegin());
|
|
1539
1585
|
return switch (move.effect) {
|
|
1540
1586
|
.Confusion => Effects.confusion(battle, player, move, log),
|
|
@@ -1550,7 +1596,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
|
|
|
1550
1596
|
.Poison => Effects.poison(battle, player, move, log),
|
|
1551
1597
|
.Reflect => Effects.reflect(battle, player, log),
|
|
1552
1598
|
.Splash => Effects.splash(battle, player, log),
|
|
1553
|
-
.Substitute => Effects.substitute(battle, player, log),
|
|
1599
|
+
.Substitute => Effects.substitute(battle, player, residual, log),
|
|
1554
1600
|
.SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
|
|
1555
1601
|
.Transform => Effects.transform(battle, player, log),
|
|
1556
1602
|
else => unreachable,
|
|
@@ -1617,10 +1663,7 @@ pub const Effects = struct {
|
|
|
1617
1663
|
side.active.volatiles.Bide = true;
|
|
1618
1664
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1619
1665
|
side.active.volatiles.state = 0;
|
|
1620
|
-
side.active.volatiles.attacks =
|
|
1621
|
-
battle.rng.range(u4, 2, 4)
|
|
1622
|
-
else
|
|
1623
|
-
(battle.rng.next() & 1) + 2);
|
|
1666
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1624
1667
|
|
|
1625
1668
|
try log.start(battle.active(player), .Bide);
|
|
1626
1669
|
}
|
|
@@ -1642,14 +1685,8 @@ pub const Effects = struct {
|
|
|
1642
1685
|
return;
|
|
1643
1686
|
}
|
|
1644
1687
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
else
|
|
1648
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1649
|
-
Gen12.percent(10)
|
|
1650
|
-
else
|
|
1651
|
-
Gen12.percent(30));
|
|
1652
|
-
if (!chance) return;
|
|
1688
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1689
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1653
1690
|
|
|
1654
1691
|
foe_stored.status = Status.init(.BRN);
|
|
1655
1692
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1673,11 +1710,7 @@ pub const Effects = struct {
|
|
|
1673
1710
|
const sub = foe.active.volatiles.Substitute;
|
|
1674
1711
|
|
|
1675
1712
|
if (move.effect == .ConfusionChance) {
|
|
1676
|
-
|
|
1677
|
-
battle.rng.chance(u8, 25, 256)
|
|
1678
|
-
else
|
|
1679
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1680
|
-
if (!chance) return;
|
|
1713
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1681
1714
|
} else {
|
|
1682
1715
|
if (showdown) {
|
|
1683
1716
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1696,10 +1729,7 @@ pub const Effects = struct {
|
|
|
1696
1729
|
|
|
1697
1730
|
if (foe.active.volatiles.Confusion) return;
|
|
1698
1731
|
foe.active.volatiles.Confusion = true;
|
|
1699
|
-
foe.active.volatiles.confusion =
|
|
1700
|
-
battle.rng.range(u8, 2, 6)
|
|
1701
|
-
else
|
|
1702
|
-
(battle.rng.next() & 3) + 2);
|
|
1732
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1703
1733
|
|
|
1704
1734
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1705
1735
|
}
|
|
@@ -1749,11 +1779,8 @@ pub const Effects = struct {
|
|
|
1749
1779
|
return Result.Error;
|
|
1750
1780
|
}
|
|
1751
1781
|
|
|
1752
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1753
|
-
volatiles.disabled_duration =
|
|
1754
|
-
battle.rng.range(u8, 1, 9)
|
|
1755
|
-
else
|
|
1756
|
-
(battle.rng.next() & 7) + 1);
|
|
1782
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1783
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1757
1784
|
|
|
1758
1785
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1759
1786
|
return null;
|
|
@@ -1765,7 +1792,7 @@ pub const Effects = struct {
|
|
|
1765
1792
|
|
|
1766
1793
|
if (battle.last_damage == 0) {
|
|
1767
1794
|
assert(showdown);
|
|
1768
|
-
return;
|
|
1795
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1769
1796
|
}
|
|
1770
1797
|
|
|
1771
1798
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1792,15 +1819,7 @@ pub const Effects = struct {
|
|
|
1792
1819
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1793
1820
|
|
|
1794
1821
|
if (volatiles.Substitute) return;
|
|
1795
|
-
|
|
1796
|
-
const chance = if (showdown)
|
|
1797
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1798
|
-
else
|
|
1799
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1800
|
-
Gen12.percent(10)
|
|
1801
|
-
else
|
|
1802
|
-
Gen12.percent(30));
|
|
1803
|
-
if (!chance) return;
|
|
1822
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1804
1823
|
|
|
1805
1824
|
volatiles.Flinch = true;
|
|
1806
1825
|
volatiles.Recharging = false;
|
|
@@ -1826,12 +1845,8 @@ pub const Effects = struct {
|
|
|
1826
1845
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1827
1846
|
}
|
|
1828
1847
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
else
|
|
1832
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1833
|
-
if (!chance) return;
|
|
1834
|
-
|
|
1848
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1849
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1835
1850
|
// Freeze Clause Mod
|
|
1836
1851
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1837
1852
|
|
|
@@ -1976,7 +1991,7 @@ pub const Effects = struct {
|
|
|
1976
1991
|
}
|
|
1977
1992
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1978
1993
|
|
|
1979
|
-
const rslot =
|
|
1994
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1980
1995
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1981
1996
|
|
|
1982
1997
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -1997,7 +2012,8 @@ pub const Effects = struct {
|
|
|
1997
2012
|
assert(!side.active.volatiles.MultiHit);
|
|
1998
2013
|
side.active.volatiles.MultiHit = true;
|
|
1999
2014
|
|
|
2000
|
-
side.active.volatiles.attacks =
|
|
2015
|
+
side.active.volatiles.attacks =
|
|
2016
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2001
2017
|
}
|
|
2002
2018
|
|
|
2003
2019
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2041,14 +2057,8 @@ pub const Effects = struct {
|
|
|
2041
2057
|
}
|
|
2042
2058
|
|
|
2043
2059
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
else
|
|
2047
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2048
|
-
Gen12.percent(10)
|
|
2049
|
-
else
|
|
2050
|
-
Gen12.percent(30));
|
|
2051
|
-
if (!chance) return;
|
|
2060
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2061
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2052
2062
|
|
|
2053
2063
|
foe_stored.status = Status.init(.PAR);
|
|
2054
2064
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2091,16 +2101,7 @@ pub const Effects = struct {
|
|
|
2091
2101
|
|
|
2092
2102
|
if (move.effect == .Poison) {
|
|
2093
2103
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2094
|
-
} else
|
|
2095
|
-
const chance = if (showdown)
|
|
2096
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2097
|
-
else
|
|
2098
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2099
|
-
Gen12.percent(20)
|
|
2100
|
-
else
|
|
2101
|
-
Gen12.percent(40));
|
|
2102
|
-
if (!chance) return;
|
|
2103
|
-
}
|
|
2104
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2104
2105
|
|
|
2105
2106
|
foe_stored.status = Status.init(.PSN);
|
|
2106
2107
|
if (toxic) {
|
|
@@ -2114,10 +2115,8 @@ pub const Effects = struct {
|
|
|
2114
2115
|
|
|
2115
2116
|
fn rage(battle: anytype, player: Player) !void {
|
|
2116
2117
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2117
|
-
if (volatiles.Rage) return;
|
|
2118
|
-
volatiles.Rage = true;
|
|
2119
2118
|
assert(!volatiles.Bide);
|
|
2120
|
-
volatiles.
|
|
2119
|
+
volatiles.Rage = true;
|
|
2121
2120
|
}
|
|
2122
2121
|
|
|
2123
2122
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2169,16 +2168,7 @@ pub const Effects = struct {
|
|
|
2169
2168
|
}
|
|
2170
2169
|
foe.active.volatiles.Recharging = false;
|
|
2171
2170
|
|
|
2172
|
-
|
|
2173
|
-
battle.rng.range(u8, 1, 8)
|
|
2174
|
-
else loop: {
|
|
2175
|
-
while (true) {
|
|
2176
|
-
const r = battle.rng.next() & 7;
|
|
2177
|
-
if (r != 0) break :loop r;
|
|
2178
|
-
}
|
|
2179
|
-
});
|
|
2180
|
-
|
|
2181
|
-
foe_stored.status = Status.slp(duration);
|
|
2171
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2182
2172
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2183
2173
|
}
|
|
2184
2174
|
|
|
@@ -2186,7 +2176,7 @@ pub const Effects = struct {
|
|
|
2186
2176
|
try log.activate(battle.active(player), .Splash);
|
|
2187
2177
|
}
|
|
2188
2178
|
|
|
2189
|
-
fn substitute(battle: anytype, player: Player, log: anytype) !void {
|
|
2179
|
+
fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
2190
2180
|
var side = battle.side(player);
|
|
2191
2181
|
if (side.active.volatiles.Substitute) {
|
|
2192
2182
|
try log.fail(battle.active(player), .Substitute);
|
|
@@ -2211,14 +2201,18 @@ pub const Effects = struct {
|
|
|
2211
2201
|
side.active.volatiles.substitute = hp + 1;
|
|
2212
2202
|
side.active.volatiles.Substitute = true;
|
|
2213
2203
|
try log.start(battle.active(player), .Substitute);
|
|
2214
|
-
if (hp > 0)
|
|
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
|
+
}
|
|
2215
2208
|
}
|
|
2216
2209
|
|
|
2217
2210
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2218
2211
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2219
2212
|
|
|
2220
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2213
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2221
2214
|
_ = try checkHit(battle, player, move, log);
|
|
2215
|
+
battle.last_damage = 0;
|
|
2222
2216
|
}
|
|
2223
2217
|
|
|
2224
2218
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2227,11 +2221,7 @@ pub const Effects = struct {
|
|
|
2227
2221
|
assert(!volatiles.Bide);
|
|
2228
2222
|
|
|
2229
2223
|
volatiles.Thrashing = true;
|
|
2230
|
-
volatiles.
|
|
2231
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2232
|
-
battle.rng.range(u8, 2, 4)
|
|
2233
|
-
else
|
|
2234
|
-
(battle.rng.next() & 1) + 2);
|
|
2224
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2235
2225
|
}
|
|
2236
2226
|
|
|
2237
2227
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2273,7 +2263,7 @@ pub const Effects = struct {
|
|
|
2273
2263
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2274
2264
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2275
2265
|
|
|
2276
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2266
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2277
2267
|
}
|
|
2278
2268
|
|
|
2279
2269
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2300,9 +2290,11 @@ pub const Effects = struct {
|
|
|
2300
2290
|
return try log.fail(ident, .None);
|
|
2301
2291
|
}
|
|
2302
2292
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2303
|
-
const stat = unmodifiedStats(battle,
|
|
2293
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2304
2294
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2305
2295
|
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;
|
|
2306
2298
|
},
|
|
2307
2299
|
.DefenseUp1, .DefenseUp2 => {
|
|
2308
2300
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2318,7 +2310,7 @@ pub const Effects = struct {
|
|
|
2318
2310
|
return try log.fail(ident, .None);
|
|
2319
2311
|
}
|
|
2320
2312
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2321
|
-
const stat = unmodifiedStats(battle,
|
|
2313
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2322
2314
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2323
2315
|
try log.boost(ident, .Defense, n);
|
|
2324
2316
|
},
|
|
@@ -2335,7 +2327,7 @@ pub const Effects = struct {
|
|
|
2335
2327
|
return try log.fail(ident, .None);
|
|
2336
2328
|
}
|
|
2337
2329
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2338
|
-
const stat = unmodifiedStats(battle,
|
|
2330
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2339
2331
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2340
2332
|
try log.boost(ident, .Speed, 2);
|
|
2341
2333
|
},
|
|
@@ -2355,7 +2347,7 @@ pub const Effects = struct {
|
|
|
2355
2347
|
return try log.fail(ident, .None);
|
|
2356
2348
|
}
|
|
2357
2349
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2358
|
-
const stat = unmodifiedStats(battle,
|
|
2350
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2359
2351
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2360
2352
|
try log.boost(ident, .SpecialAttack, n);
|
|
2361
2353
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2382,11 +2374,7 @@ pub const Effects = struct {
|
|
|
2382
2374
|
}
|
|
2383
2375
|
|
|
2384
2376
|
if (move.effect.isStatDownChance()) {
|
|
2385
|
-
|
|
2386
|
-
battle.rng.chance(u8, 85, 256)
|
|
2387
|
-
else
|
|
2388
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2389
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2377
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2390
2378
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2391
2379
|
return; // checkHit already checks for Invulnerable
|
|
2392
2380
|
}
|
|
@@ -2408,7 +2396,7 @@ pub const Effects = struct {
|
|
|
2408
2396
|
return try log.fail(foe_ident, .None);
|
|
2409
2397
|
}
|
|
2410
2398
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2411
|
-
const stat = unmodifiedStats(battle,
|
|
2399
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2412
2400
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2413
2401
|
try log.boost(foe_ident, .Attack, -1);
|
|
2414
2402
|
},
|
|
@@ -2426,7 +2414,7 @@ pub const Effects = struct {
|
|
|
2426
2414
|
return try log.fail(foe_ident, .None);
|
|
2427
2415
|
}
|
|
2428
2416
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2429
|
-
const stat = unmodifiedStats(battle,
|
|
2417
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2430
2418
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2431
2419
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2432
2420
|
},
|
|
@@ -2443,7 +2431,7 @@ pub const Effects = struct {
|
|
|
2443
2431
|
return try log.fail(foe_ident, .None);
|
|
2444
2432
|
}
|
|
2445
2433
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2446
|
-
const stat = unmodifiedStats(battle,
|
|
2434
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2447
2435
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2448
2436
|
try log.boost(foe_ident, .Speed, -1);
|
|
2449
2437
|
assert(boosts.spe >= -6);
|
|
@@ -2463,7 +2451,7 @@ pub const Effects = struct {
|
|
|
2463
2451
|
return try log.fail(foe_ident, .None);
|
|
2464
2452
|
}
|
|
2465
2453
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2466
|
-
const stat = unmodifiedStats(battle,
|
|
2454
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2467
2455
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2468
2456
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2469
2457
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2482,8 +2470,7 @@ pub const Effects = struct {
|
|
|
2482
2470
|
}
|
|
2483
2471
|
};
|
|
2484
2472
|
|
|
2485
|
-
fn unmodifiedStats(battle: anytype,
|
|
2486
|
-
const side = battle.side(who);
|
|
2473
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2487
2474
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2488
2475
|
const id = ID.from(side.active.volatiles.transform);
|
|
2489
2476
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2497,7 +2484,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2497
2484
|
}
|
|
2498
2485
|
}
|
|
2499
2486
|
|
|
2500
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2487
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2501
2488
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2502
2489
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2503
2490
|
}
|
|
@@ -2549,68 +2536,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2549
2536
|
}
|
|
2550
2537
|
}
|
|
2551
2538
|
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2539
|
+
pub const Rolls = struct {
|
|
2540
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2541
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2542
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2546
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
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;
|
|
2560
2555
|
}
|
|
2561
2556
|
}
|
|
2562
|
-
return false;
|
|
2563
|
-
}
|
|
2564
2557
|
|
|
2565
|
-
fn
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2558
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2559
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2560
|
+
return battle.rng.next() < accuracy;
|
|
2561
|
+
}
|
|
2569
2562
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
else
|
|
2575
|
-
(battle.rng.next() & 3) + 2);
|
|
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
|
+
}
|
|
2576
2567
|
|
|
2577
|
-
|
|
2578
|
-
|
|
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
|
+
}
|
|
2579
2572
|
|
|
2580
|
-
|
|
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
|
+
}
|
|
2581
2577
|
|
|
2582
|
-
fn
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
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
|
+
}
|
|
2587
2582
|
|
|
2588
|
-
fn
|
|
2589
|
-
|
|
2590
|
-
if (
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2583
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2584
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2585
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2589
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2590
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
inline fn sleepDuration(battle: anytype) u3 {
|
|
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
|
+
}
|
|
2600
|
+
|
|
2601
|
+
inline fn bideThrashDuration(battle: anytype) u3 {
|
|
2602
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2603
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2607
|
+
if (showdown) {
|
|
2608
|
+
if (check_pp == 0) {
|
|
2609
|
+
var i: usize = moves.len;
|
|
2610
|
+
while (i > 0) {
|
|
2611
|
+
i -= 1;
|
|
2612
|
+
if (moves[i].id != .None) {
|
|
2613
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
} else {
|
|
2617
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2618
|
+
var i: usize = 0;
|
|
2619
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2620
|
+
if (moves[i].pp > 0) {
|
|
2621
|
+
r -= 1;
|
|
2622
|
+
if (r == 0) break;
|
|
2623
|
+
}
|
|
2603
2624
|
}
|
|
2625
|
+
return @intCast(u4, i + 1);
|
|
2604
2626
|
}
|
|
2605
|
-
|
|
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;
|
|
2606
2632
|
}
|
|
2607
2633
|
}
|
|
2608
2634
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
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);
|
|
2612
2638
|
}
|
|
2613
|
-
|
|
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);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2655
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2656
|
+
while (true) {
|
|
2657
|
+
const r = battle.rng.next();
|
|
2658
|
+
if (r < max) return r;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
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
|
+
};
|
|
2614
2673
|
|
|
2615
2674
|
test "RNG agreement" {
|
|
2616
2675
|
if (!showdown) return;
|
|
@@ -2710,9 +2769,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2710
2769
|
while (slot <= 4) : (slot += 1) {
|
|
2711
2770
|
const m = active.moves[slot - 1];
|
|
2712
2771
|
if (m.id == .None) break;
|
|
2713
|
-
if (m.id == side.last_selected_move) {
|
|
2714
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2715
|
-
const
|
|
2772
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2773
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2774
|
+
const struggle =
|
|
2775
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2776
|
+
const s = if (struggle) 0 else slot;
|
|
2716
2777
|
out[n] = .{ .type = .Move, .data = s };
|
|
2717
2778
|
n += 1;
|
|
2718
2779
|
return n;
|