@pkmn/engine 0.1.0-dev.2ec67dba → 0.1.0-dev.3a273268
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 +32 -4
- 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 +382 -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 +9 -12
- package/package.json +35 -25
- package/src/bin/install-pkmn-engine +57 -30
- 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 +5 -5
- package/src/lib/gen1/helpers.zig +0 -4
- package/src/lib/gen1/mechanics.zig +292 -245
- 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,
|
|
@@ -451,7 +451,13 @@ fn executeMove(
|
|
|
451
451
|
|
|
452
452
|
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
453
453
|
|
|
454
|
-
fn beforeMove(
|
|
454
|
+
fn beforeMove(
|
|
455
|
+
battle: anytype,
|
|
456
|
+
player: Player,
|
|
457
|
+
from: ?Move,
|
|
458
|
+
residual: *bool,
|
|
459
|
+
log: anytype,
|
|
460
|
+
) !BeforeMove {
|
|
455
461
|
var side = battle.side(player);
|
|
456
462
|
const foe = battle.foe(player);
|
|
457
463
|
var active = &side.active;
|
|
@@ -530,14 +536,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
530
536
|
} else {
|
|
531
537
|
try log.activate(ident, .Confusion);
|
|
532
538
|
|
|
533
|
-
|
|
534
|
-
!battle.rng.chance(u8, 128, 256)
|
|
535
|
-
else
|
|
536
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
537
|
-
|
|
538
|
-
if (confused) {
|
|
539
|
+
if (Rolls.confused(battle)) {
|
|
539
540
|
assert(!volatiles.MultiHit);
|
|
540
|
-
volatiles.state = 0;
|
|
541
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
541
542
|
volatiles.Bide = false;
|
|
542
543
|
volatiles.Thrashing = false;
|
|
543
544
|
volatiles.MultiHit = false;
|
|
@@ -561,8 +562,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
561
562
|
player
|
|
562
563
|
else
|
|
563
564
|
player.foe();
|
|
565
|
+
|
|
566
|
+
const uncapped = battle.last_damage;
|
|
564
567
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
565
568
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
569
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
570
|
+
if (showdown) battle.last_damage = uncapped;
|
|
566
571
|
|
|
567
572
|
return .done;
|
|
568
573
|
}
|
|
@@ -571,22 +576,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
571
576
|
|
|
572
577
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
573
578
|
|
|
574
|
-
if (Status.is(stored.status, .PAR)) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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;
|
|
589
|
-
}
|
|
579
|
+
if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
|
|
580
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
581
|
+
volatiles.Bide = false;
|
|
582
|
+
volatiles.Thrashing = false;
|
|
583
|
+
volatiles.Charging = false;
|
|
584
|
+
volatiles.Binding = false;
|
|
585
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
586
|
+
try log.cant(ident, .Paralysis);
|
|
587
|
+
return .done;
|
|
590
588
|
}
|
|
591
589
|
|
|
592
590
|
if (volatiles.Bide) {
|
|
@@ -613,7 +611,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
|
|
|
613
611
|
return .done;
|
|
614
612
|
}
|
|
615
613
|
|
|
614
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
616
615
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
616
|
+
if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
617
|
+
|
|
618
|
+
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
619
|
+
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
620
|
+
// run residual here regardless of whether the foe fainted and opt-out of the default flow
|
|
621
|
+
if (showdown) {
|
|
622
|
+
residual.* = false;
|
|
623
|
+
try handleResidual(battle, player, log);
|
|
624
|
+
}
|
|
625
|
+
|
|
617
626
|
return .done;
|
|
618
627
|
}
|
|
619
628
|
|
|
@@ -770,16 +779,21 @@ fn doMove(
|
|
|
770
779
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
771
780
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
772
781
|
if (immune and move.effect != .Binding) break :miss true;
|
|
773
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
782
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
783
|
+
battle.last_damage = 0;
|
|
784
|
+
break :miss true;
|
|
785
|
+
}
|
|
774
786
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
775
787
|
};
|
|
776
788
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
777
789
|
|
|
790
|
+
var late = showdown and move.effect != .Explode;
|
|
778
791
|
const skip = status or immune;
|
|
779
792
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
780
793
|
if (showdown and move.effect.isMulti()) {
|
|
781
794
|
Effects.multiHit(battle, player, move);
|
|
782
795
|
hits = side.active.volatiles.attacks;
|
|
796
|
+
late = false;
|
|
783
797
|
}
|
|
784
798
|
|
|
785
799
|
// Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
|
|
@@ -813,7 +827,7 @@ fn doMove(
|
|
|
813
827
|
}
|
|
814
828
|
|
|
815
829
|
// Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
|
|
816
|
-
if (showdown and skip
|
|
830
|
+
if (showdown and skip) battle.last_damage = 0;
|
|
817
831
|
|
|
818
832
|
miss = if (showdown or skip)
|
|
819
833
|
miss
|
|
@@ -860,15 +874,9 @@ fn doMove(
|
|
|
860
874
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
861
875
|
} else if (move.effect == .Explode) {
|
|
862
876
|
try Effects.explode(battle, player);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} else if (showdown) {
|
|
867
|
-
if (move.effect == .Disable) {
|
|
868
|
-
if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
|
|
869
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
877
|
+
try buildRage(battle, player.foe(), log);
|
|
878
|
+
} else if (showdown and move.effect == .Disable) {
|
|
879
|
+
try buildRage(battle, player.foe(), log);
|
|
872
880
|
}
|
|
873
881
|
return null;
|
|
874
882
|
}
|
|
@@ -881,6 +889,9 @@ fn doMove(
|
|
|
881
889
|
hits = side.active.volatiles.attacks;
|
|
882
890
|
}
|
|
883
891
|
|
|
892
|
+
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
893
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
894
|
+
|
|
884
895
|
var nullified = false;
|
|
885
896
|
var hit: u4 = 0;
|
|
886
897
|
while (hit < hits) {
|
|
@@ -896,9 +907,7 @@ fn doMove(
|
|
|
896
907
|
if (hit == 0 and ohko) try log.ohko();
|
|
897
908
|
hit += 1;
|
|
898
909
|
if (foe.stored().hp == 0) break;
|
|
899
|
-
if (
|
|
900
|
-
try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
|
|
901
|
-
}
|
|
910
|
+
if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
|
|
902
911
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
903
912
|
if (nullified) break;
|
|
904
913
|
}
|
|
@@ -935,6 +944,9 @@ fn doMove(
|
|
|
935
944
|
|
|
936
945
|
if (foe.stored().hp == 0) return null;
|
|
937
946
|
|
|
947
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
|
|
948
|
+
if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
949
|
+
|
|
938
950
|
if (!move.effect.isSpecial()) {
|
|
939
951
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
940
952
|
// second time here (after being executed in the "always happens" block above) but that
|
|
@@ -946,7 +958,9 @@ fn doMove(
|
|
|
946
958
|
if (move.effect == .Twineedle) {
|
|
947
959
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
|
|
948
960
|
} else if (move.effect == .Disable) {
|
|
949
|
-
|
|
961
|
+
const result = try Effects.disable(battle, player, move, log);
|
|
962
|
+
if (showdown) try buildRage(battle, player.foe(), log);
|
|
963
|
+
return result;
|
|
950
964
|
} else {
|
|
951
965
|
try moveEffect(battle, player, move, log);
|
|
952
966
|
}
|
|
@@ -964,8 +978,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
964
978
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
965
979
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
966
980
|
|
|
967
|
-
|
|
968
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
981
|
+
return Rolls.criticalHit(battle, chance);
|
|
969
982
|
}
|
|
970
983
|
|
|
971
984
|
fn calcDamage(
|
|
@@ -1069,49 +1082,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1069
1082
|
|
|
1070
1083
|
fn randomizeDamage(battle: anytype) void {
|
|
1071
1084
|
if (battle.last_damage <= 1) return;
|
|
1072
|
-
|
|
1073
|
-
const random = if (showdown)
|
|
1074
|
-
battle.rng.range(u8, 217, 256)
|
|
1075
|
-
else loop: {
|
|
1076
|
-
while (true) {
|
|
1077
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1078
|
-
if (r >= 217) break :loop r;
|
|
1079
|
-
}
|
|
1080
|
-
};
|
|
1081
|
-
|
|
1085
|
+
const random = Rolls.damage(battle);
|
|
1082
1086
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1083
1087
|
}
|
|
1084
1088
|
|
|
1085
1089
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1086
1090
|
const side = battle.side(player);
|
|
1091
|
+
const foe = battle.foe(player);
|
|
1087
1092
|
|
|
1088
1093
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1089
1094
|
|
|
1090
1095
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1091
|
-
.SuperFang => @max(
|
|
1096
|
+
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1092
1097
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1093
1098
|
.SonicBoom => 20,
|
|
1094
1099
|
.DragonRage => 40,
|
|
1095
1100
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1096
1101
|
.Psywave => power: {
|
|
1097
1102
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1102
|
-
if (max <= 1) return Result.Error;
|
|
1103
|
-
while (true) {
|
|
1104
|
-
const r = battle.rng.next();
|
|
1105
|
-
if (r < max) break :power r;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1103
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1104
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1105
|
+
break :power Rolls.psywave(battle, max);
|
|
1108
1106
|
},
|
|
1109
1107
|
else => unreachable,
|
|
1110
1108
|
};
|
|
1111
1109
|
|
|
1112
1110
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1113
1111
|
|
|
1112
|
+
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1114
1113
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1114
|
+
if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
|
|
1115
|
+
|
|
1115
1116
|
return null;
|
|
1116
1117
|
}
|
|
1117
1118
|
|
|
@@ -1233,19 +1234,7 @@ fn metronome(
|
|
|
1233
1234
|
) !?Result {
|
|
1234
1235
|
var side = battle.side(player);
|
|
1235
1236
|
|
|
1236
|
-
side.last_selected_move =
|
|
1237
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1238
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1239
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1240
|
-
} else loop: {
|
|
1241
|
-
while (true) {
|
|
1242
|
-
const r = battle.rng.next();
|
|
1243
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1244
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1245
|
-
break :loop @intToEnum(Move, r);
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1237
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1249
1238
|
incrementPP(side, mslot);
|
|
1250
1239
|
|
|
1251
1240
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
@@ -1303,23 +1292,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1303
1292
|
}
|
|
1304
1293
|
|
|
1305
1294
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1295
|
+
const state = side.active.volatiles.state;
|
|
1296
|
+
var overwrite = move.effect == .Rage or move.effect == .Thrashing;
|
|
1297
|
+
const overwritten = overwrite and state > 0;
|
|
1298
|
+
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1299
|
+
|
|
1300
|
+
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1310
1301
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1311
1302
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1312
1303
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1313
1304
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1314
1305
|
accuracy = @min(255, @max(1, accuracy));
|
|
1315
1306
|
|
|
1316
|
-
|
|
1307
|
+
// Pokémon Showdown only overwrites if the volatile is present
|
|
1308
|
+
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1309
|
+
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1317
1310
|
|
|
1318
1311
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1319
|
-
break :miss
|
|
1320
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1321
|
-
else
|
|
1322
|
-
battle.rng.next() >= accuracy;
|
|
1312
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1323
1313
|
};
|
|
1324
1314
|
|
|
1325
1315
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1434,6 +1424,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1434
1424
|
}
|
|
1435
1425
|
|
|
1436
1426
|
if (volatiles.LeechSeed) {
|
|
1427
|
+
var foe = battle.foe(player);
|
|
1428
|
+
var foe_stored = foe.stored();
|
|
1429
|
+
const foe_ident = battle.active(player.foe());
|
|
1430
|
+
|
|
1431
|
+
if (foe_stored.hp == 0) {
|
|
1432
|
+
assert(showdown);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1437
1436
|
var damage = @max(stored.stats.hp / 16, 1);
|
|
1438
1437
|
|
|
1439
1438
|
// GLITCH: Leech Seed + Toxic glitch
|
|
@@ -1445,12 +1444,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1445
1444
|
const amount = @min(damage, stored.hp);
|
|
1446
1445
|
stored.hp -= amount;
|
|
1447
1446
|
|
|
1448
|
-
var foe = battle.foe(player);
|
|
1449
|
-
var foe_stored = foe.stored();
|
|
1450
|
-
const foe_ident = battle.active(player.foe());
|
|
1451
|
-
|
|
1452
1447
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1453
|
-
if (amount > 0)
|
|
1448
|
+
if (amount > 0) {
|
|
1449
|
+
try log.damage(ident, stored, .LeechSeed);
|
|
1450
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1451
|
+
if (showdown) battle.last_damage = damage;
|
|
1452
|
+
}
|
|
1454
1453
|
|
|
1455
1454
|
const before = foe_stored.hp;
|
|
1456
1455
|
// Uncapped damage is added back to the foe
|
|
@@ -1537,6 +1536,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1537
1536
|
return false;
|
|
1538
1537
|
}
|
|
1539
1538
|
|
|
1539
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1540
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
1541
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1542
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1543
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1544
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1545
|
+
m
|
|
1546
|
+
else
|
|
1547
|
+
side.last_selected_move;
|
|
1548
|
+
if (m != .None and m == last) {
|
|
1549
|
+
side.active.volatiles.Charging = false;
|
|
1550
|
+
try log.disabled(ident, last);
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1558
|
+
const side = battle.side(who);
|
|
1559
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1560
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1565
|
+
var volatiles = &active.volatiles;
|
|
1566
|
+
assert(volatiles.Thrashing);
|
|
1567
|
+
if (volatiles.attacks > 0) return false;
|
|
1568
|
+
|
|
1569
|
+
volatiles.Thrashing = false;
|
|
1570
|
+
volatiles.Confusion = true;
|
|
1571
|
+
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1540
1575
|
inline fn onBegin(
|
|
1541
1576
|
battle: anytype,
|
|
1542
1577
|
player: Player,
|
|
@@ -1627,10 +1662,7 @@ pub const Effects = struct {
|
|
|
1627
1662
|
side.active.volatiles.Bide = true;
|
|
1628
1663
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1629
1664
|
side.active.volatiles.state = 0;
|
|
1630
|
-
side.active.volatiles.attacks =
|
|
1631
|
-
battle.rng.range(u4, 2, 4)
|
|
1632
|
-
else
|
|
1633
|
-
(battle.rng.next() & 1) + 2);
|
|
1665
|
+
side.active.volatiles.attacks = Rolls.duration(battle);
|
|
1634
1666
|
|
|
1635
1667
|
try log.start(battle.active(player), .Bide);
|
|
1636
1668
|
}
|
|
@@ -1652,14 +1684,8 @@ pub const Effects = struct {
|
|
|
1652
1684
|
return;
|
|
1653
1685
|
}
|
|
1654
1686
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
else
|
|
1658
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1659
|
-
Gen12.percent(10)
|
|
1660
|
-
else
|
|
1661
|
-
Gen12.percent(30));
|
|
1662
|
-
if (!chance) return;
|
|
1687
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1688
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1663
1689
|
|
|
1664
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1665
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1683,11 +1709,7 @@ pub const Effects = struct {
|
|
|
1683
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1684
1710
|
|
|
1685
1711
|
if (move.effect == .ConfusionChance) {
|
|
1686
|
-
|
|
1687
|
-
battle.rng.chance(u8, 25, 256)
|
|
1688
|
-
else
|
|
1689
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1690
|
-
if (!chance) return;
|
|
1712
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1691
1713
|
} else {
|
|
1692
1714
|
if (showdown) {
|
|
1693
1715
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1706,10 +1728,7 @@ pub const Effects = struct {
|
|
|
1706
1728
|
|
|
1707
1729
|
if (foe.active.volatiles.Confusion) return;
|
|
1708
1730
|
foe.active.volatiles.Confusion = true;
|
|
1709
|
-
foe.active.volatiles.confusion =
|
|
1710
|
-
battle.rng.range(u8, 2, 6)
|
|
1711
|
-
else
|
|
1712
|
-
(battle.rng.next() & 3) + 2);
|
|
1731
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1713
1732
|
|
|
1714
1733
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1715
1734
|
}
|
|
@@ -1759,11 +1778,8 @@ pub const Effects = struct {
|
|
|
1759
1778
|
return Result.Error;
|
|
1760
1779
|
}
|
|
1761
1780
|
|
|
1762
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1763
|
-
volatiles.disabled_duration =
|
|
1764
|
-
battle.rng.range(u8, 1, 9)
|
|
1765
|
-
else
|
|
1766
|
-
(battle.rng.next() & 7) + 1);
|
|
1781
|
+
volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
|
|
1782
|
+
volatiles.disabled_duration = Rolls.disableDuration(battle);
|
|
1767
1783
|
|
|
1768
1784
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1769
1785
|
return null;
|
|
@@ -1775,7 +1791,7 @@ pub const Effects = struct {
|
|
|
1775
1791
|
|
|
1776
1792
|
if (battle.last_damage == 0) {
|
|
1777
1793
|
assert(showdown);
|
|
1778
|
-
return;
|
|
1794
|
+
if (!battle.foe(player).active.volatiles.Substitute) return;
|
|
1779
1795
|
}
|
|
1780
1796
|
|
|
1781
1797
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1802,15 +1818,7 @@ pub const Effects = struct {
|
|
|
1802
1818
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1803
1819
|
|
|
1804
1820
|
if (volatiles.Substitute) return;
|
|
1805
|
-
|
|
1806
|
-
const chance = if (showdown)
|
|
1807
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1808
|
-
else
|
|
1809
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1810
|
-
Gen12.percent(10)
|
|
1811
|
-
else
|
|
1812
|
-
Gen12.percent(30));
|
|
1813
|
-
if (!chance) return;
|
|
1821
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1814
1822
|
|
|
1815
1823
|
volatiles.Flinch = true;
|
|
1816
1824
|
volatiles.Recharging = false;
|
|
@@ -1836,12 +1844,8 @@ pub const Effects = struct {
|
|
|
1836
1844
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1837
1845
|
}
|
|
1838
1846
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
else
|
|
1842
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1843
|
-
if (!chance) return;
|
|
1844
|
-
|
|
1847
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1848
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1845
1849
|
// Freeze Clause Mod
|
|
1846
1850
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1847
1851
|
|
|
@@ -1986,7 +1990,7 @@ pub const Effects = struct {
|
|
|
1986
1990
|
}
|
|
1987
1991
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1988
1992
|
|
|
1989
|
-
const rslot =
|
|
1993
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
1990
1994
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1991
1995
|
|
|
1992
1996
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2007,7 +2011,8 @@ pub const Effects = struct {
|
|
|
2007
2011
|
assert(!side.active.volatiles.MultiHit);
|
|
2008
2012
|
side.active.volatiles.MultiHit = true;
|
|
2009
2013
|
|
|
2010
|
-
side.active.volatiles.attacks =
|
|
2014
|
+
side.active.volatiles.attacks =
|
|
2015
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2011
2016
|
}
|
|
2012
2017
|
|
|
2013
2018
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2051,14 +2056,8 @@ pub const Effects = struct {
|
|
|
2051
2056
|
}
|
|
2052
2057
|
|
|
2053
2058
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
else
|
|
2057
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2058
|
-
Gen12.percent(10)
|
|
2059
|
-
else
|
|
2060
|
-
Gen12.percent(30));
|
|
2061
|
-
if (!chance) return;
|
|
2059
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2060
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2062
2061
|
|
|
2063
2062
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2063
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2101,16 +2100,7 @@ pub const Effects = struct {
|
|
|
2101
2100
|
|
|
2102
2101
|
if (move.effect == .Poison) {
|
|
2103
2102
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2104
|
-
} else
|
|
2105
|
-
const chance = if (showdown)
|
|
2106
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2107
|
-
else
|
|
2108
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2109
|
-
Gen12.percent(20)
|
|
2110
|
-
else
|
|
2111
|
-
Gen12.percent(40));
|
|
2112
|
-
if (!chance) return;
|
|
2113
|
-
}
|
|
2103
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2114
2104
|
|
|
2115
2105
|
foe_stored.status = Status.init(.PSN);
|
|
2116
2106
|
if (toxic) {
|
|
@@ -2124,10 +2114,8 @@ pub const Effects = struct {
|
|
|
2124
2114
|
|
|
2125
2115
|
fn rage(battle: anytype, player: Player) !void {
|
|
2126
2116
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2127
|
-
if (volatiles.Rage) return;
|
|
2128
|
-
volatiles.Rage = true;
|
|
2129
2117
|
assert(!volatiles.Bide);
|
|
2130
|
-
volatiles.
|
|
2118
|
+
volatiles.Rage = true;
|
|
2131
2119
|
}
|
|
2132
2120
|
|
|
2133
2121
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2179,16 +2167,7 @@ pub const Effects = struct {
|
|
|
2179
2167
|
}
|
|
2180
2168
|
foe.active.volatiles.Recharging = false;
|
|
2181
2169
|
|
|
2182
|
-
|
|
2183
|
-
battle.rng.range(u8, 1, 8)
|
|
2184
|
-
else loop: {
|
|
2185
|
-
while (true) {
|
|
2186
|
-
const r = battle.rng.next() & 7;
|
|
2187
|
-
if (r != 0) break :loop r;
|
|
2188
|
-
}
|
|
2189
|
-
});
|
|
2190
|
-
|
|
2191
|
-
foe_stored.status = Status.slp(duration);
|
|
2170
|
+
foe_stored.status = Status.slp(Rolls.sleep(battle));
|
|
2192
2171
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2193
2172
|
}
|
|
2194
2173
|
|
|
@@ -2230,8 +2209,9 @@ pub const Effects = struct {
|
|
|
2230
2209
|
fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
2231
2210
|
if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
|
|
2232
2211
|
|
|
2233
|
-
// Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
|
|
2212
|
+
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2234
2213
|
_ = try checkHit(battle, player, move, log);
|
|
2214
|
+
battle.last_damage = 0;
|
|
2235
2215
|
}
|
|
2236
2216
|
|
|
2237
2217
|
fn thrashing(battle: anytype, player: Player) void {
|
|
@@ -2240,11 +2220,7 @@ pub const Effects = struct {
|
|
|
2240
2220
|
assert(!volatiles.Bide);
|
|
2241
2221
|
|
|
2242
2222
|
volatiles.Thrashing = true;
|
|
2243
|
-
volatiles.
|
|
2244
|
-
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2245
|
-
battle.rng.range(u8, 2, 4)
|
|
2246
|
-
else
|
|
2247
|
-
(battle.rng.next() & 1) + 2);
|
|
2223
|
+
volatiles.attacks = Rolls.duration(battle);
|
|
2248
2224
|
}
|
|
2249
2225
|
|
|
2250
2226
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2286,7 +2262,7 @@ pub const Effects = struct {
|
|
|
2286
2262
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2287
2263
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2288
2264
|
|
|
2289
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2265
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2290
2266
|
}
|
|
2291
2267
|
|
|
2292
2268
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2313,9 +2289,11 @@ pub const Effects = struct {
|
|
|
2313
2289
|
return try log.fail(ident, .None);
|
|
2314
2290
|
}
|
|
2315
2291
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2316
|
-
const stat = unmodifiedStats(battle,
|
|
2292
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2317
2293
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2318
2294
|
try log.boost(ident, reason, n);
|
|
2295
|
+
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2296
|
+
if (showdown and move.effect == .Rage) return;
|
|
2319
2297
|
},
|
|
2320
2298
|
.DefenseUp1, .DefenseUp2 => {
|
|
2321
2299
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2331,7 +2309,7 @@ pub const Effects = struct {
|
|
|
2331
2309
|
return try log.fail(ident, .None);
|
|
2332
2310
|
}
|
|
2333
2311
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2334
|
-
const stat = unmodifiedStats(battle,
|
|
2312
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2335
2313
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2336
2314
|
try log.boost(ident, .Defense, n);
|
|
2337
2315
|
},
|
|
@@ -2348,7 +2326,7 @@ pub const Effects = struct {
|
|
|
2348
2326
|
return try log.fail(ident, .None);
|
|
2349
2327
|
}
|
|
2350
2328
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2351
|
-
const stat = unmodifiedStats(battle,
|
|
2329
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2352
2330
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2353
2331
|
try log.boost(ident, .Speed, 2);
|
|
2354
2332
|
},
|
|
@@ -2368,7 +2346,7 @@ pub const Effects = struct {
|
|
|
2368
2346
|
return try log.fail(ident, .None);
|
|
2369
2347
|
}
|
|
2370
2348
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2371
|
-
const stat = unmodifiedStats(battle,
|
|
2349
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2372
2350
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2373
2351
|
try log.boost(ident, .SpecialAttack, n);
|
|
2374
2352
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2395,11 +2373,7 @@ pub const Effects = struct {
|
|
|
2395
2373
|
}
|
|
2396
2374
|
|
|
2397
2375
|
if (move.effect.isStatDownChance()) {
|
|
2398
|
-
|
|
2399
|
-
battle.rng.chance(u8, 85, 256)
|
|
2400
|
-
else
|
|
2401
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2402
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2376
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2403
2377
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2404
2378
|
return; // checkHit already checks for Invulnerable
|
|
2405
2379
|
}
|
|
@@ -2421,7 +2395,7 @@ pub const Effects = struct {
|
|
|
2421
2395
|
return try log.fail(foe_ident, .None);
|
|
2422
2396
|
}
|
|
2423
2397
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2424
|
-
const stat = unmodifiedStats(battle,
|
|
2398
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2425
2399
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2426
2400
|
try log.boost(foe_ident, .Attack, -1);
|
|
2427
2401
|
},
|
|
@@ -2439,7 +2413,7 @@ pub const Effects = struct {
|
|
|
2439
2413
|
return try log.fail(foe_ident, .None);
|
|
2440
2414
|
}
|
|
2441
2415
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2442
|
-
const stat = unmodifiedStats(battle,
|
|
2416
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2443
2417
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2444
2418
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2445
2419
|
},
|
|
@@ -2456,7 +2430,7 @@ pub const Effects = struct {
|
|
|
2456
2430
|
return try log.fail(foe_ident, .None);
|
|
2457
2431
|
}
|
|
2458
2432
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2459
|
-
const stat = unmodifiedStats(battle,
|
|
2433
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2460
2434
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2461
2435
|
try log.boost(foe_ident, .Speed, -1);
|
|
2462
2436
|
assert(boosts.spe >= -6);
|
|
@@ -2476,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2476
2450
|
return try log.fail(foe_ident, .None);
|
|
2477
2451
|
}
|
|
2478
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2479
|
-
const stat = unmodifiedStats(battle,
|
|
2453
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2480
2454
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2481
2455
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2482
2456
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2495,8 +2469,7 @@ pub const Effects = struct {
|
|
|
2495
2469
|
}
|
|
2496
2470
|
};
|
|
2497
2471
|
|
|
2498
|
-
fn unmodifiedStats(battle: anytype,
|
|
2499
|
-
const side = battle.side(who);
|
|
2472
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2500
2473
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2501
2474
|
const id = ID.from(side.active.volatiles.transform);
|
|
2502
2475
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2510,7 +2483,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
|
|
|
2510
2483
|
}
|
|
2511
2484
|
}
|
|
2512
2485
|
|
|
2513
|
-
fn isForced(active: ActivePokemon) bool {
|
|
2486
|
+
inline fn isForced(active: ActivePokemon) bool {
|
|
2514
2487
|
return active.volatiles.Recharging or active.volatiles.Rage or
|
|
2515
2488
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2516
2489
|
}
|
|
@@ -2562,68 +2535,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2562
2535
|
}
|
|
2563
2536
|
}
|
|
2564
2537
|
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2538
|
+
pub const Rolls = struct {
|
|
2539
|
+
inline fn speedTie(battle: anytype) bool {
|
|
2540
|
+
if (showdown) return battle.rng.range(u8, 0, 2) == 0;
|
|
2541
|
+
return battle.rng.next() < Gen12.percent(50) + 1;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
inline fn criticalHit(battle: anytype, chance: u16) bool {
|
|
2545
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
2546
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
inline fn damage(battle: anytype) u8 {
|
|
2550
|
+
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2551
|
+
while (true) {
|
|
2552
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2553
|
+
if (r >= 217) return r;
|
|
2573
2554
|
}
|
|
2574
2555
|
}
|
|
2575
|
-
return false;
|
|
2576
|
-
}
|
|
2577
2556
|
|
|
2578
|
-
fn
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2557
|
+
inline fn hit(battle: anytype, accuracy: u16) bool {
|
|
2558
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
|
|
2559
|
+
return battle.rng.next() < accuracy;
|
|
2560
|
+
}
|
|
2582
2561
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
else
|
|
2588
|
-
(battle.rng.next() & 3) + 2);
|
|
2562
|
+
inline fn confusionDuration(battle: anytype) u3 {
|
|
2563
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2564
|
+
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2565
|
+
}
|
|
2589
2566
|
|
|
2590
|
-
|
|
2591
|
-
|
|
2567
|
+
inline fn confused(battle: anytype) bool {
|
|
2568
|
+
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2569
|
+
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2570
|
+
}
|
|
2592
2571
|
|
|
2593
|
-
|
|
2572
|
+
inline fn paralyzed(battle: anytype) bool {
|
|
2573
|
+
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2574
|
+
return battle.rng.next() < Gen12.percent(25);
|
|
2575
|
+
}
|
|
2594
2576
|
|
|
2595
|
-
fn
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
}
|
|
2577
|
+
inline fn confusionChance(battle: anytype) bool {
|
|
2578
|
+
if (showdown) return battle.rng.chance(u8, 25, 256);
|
|
2579
|
+
return battle.rng.next() < Gen12.percent(10);
|
|
2580
|
+
}
|
|
2600
2581
|
|
|
2601
|
-
fn
|
|
2602
|
-
|
|
2603
|
-
if (
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2582
|
+
inline fn secondaryChance(battle: anytype, low: bool) bool {
|
|
2583
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
|
|
2584
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
inline fn poisonChance(battle: anytype, low: bool) bool {
|
|
2588
|
+
if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
|
|
2589
|
+
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
inline fn sleep(battle: anytype) u3 {
|
|
2593
|
+
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2594
|
+
while (true) {
|
|
2595
|
+
const r = battle.rng.next() & 7;
|
|
2596
|
+
if (r != 0) return @intCast(u3, r);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
inline fn duration(battle: anytype) u3 {
|
|
2601
|
+
if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
|
|
2602
|
+
return @intCast(u3, (battle.rng.next() & 1) + 2);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2606
|
+
if (showdown) {
|
|
2607
|
+
if (check_pp == 0) {
|
|
2608
|
+
var i: usize = moves.len;
|
|
2609
|
+
while (i > 0) {
|
|
2610
|
+
i -= 1;
|
|
2611
|
+
if (moves[i].id != .None) {
|
|
2612
|
+
return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
} else {
|
|
2616
|
+
var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2617
|
+
var i: usize = 0;
|
|
2618
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2619
|
+
if (moves[i].pp > 0) {
|
|
2620
|
+
r -= 1;
|
|
2621
|
+
if (r == 0) break;
|
|
2622
|
+
}
|
|
2616
2623
|
}
|
|
2624
|
+
return @intCast(u4, i + 1);
|
|
2617
2625
|
}
|
|
2618
|
-
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
while (true) {
|
|
2629
|
+
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2630
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2619
2631
|
}
|
|
2620
2632
|
}
|
|
2621
2633
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2634
|
+
inline fn disableDuration(battle: anytype) u4 {
|
|
2635
|
+
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2636
|
+
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2625
2637
|
}
|
|
2626
|
-
|
|
2638
|
+
|
|
2639
|
+
inline fn metronome(battle: anytype) Move {
|
|
2640
|
+
if (showdown) {
|
|
2641
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2642
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2643
|
+
return @intToEnum(Move, r + mod);
|
|
2644
|
+
}
|
|
2645
|
+
while (true) {
|
|
2646
|
+
const r = battle.rng.next();
|
|
2647
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2648
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2649
|
+
return @intToEnum(Move, r);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2654
|
+
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2655
|
+
while (true) {
|
|
2656
|
+
const r = battle.rng.next();
|
|
2657
|
+
if (r < max) return r;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
inline fn distribution(battle: anytype) u3 {
|
|
2662
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2663
|
+
const r = (battle.rng.next() & 3);
|
|
2664
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
inline fn unboost(battle: anytype) bool {
|
|
2668
|
+
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2669
|
+
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2627
2672
|
|
|
2628
2673
|
test "RNG agreement" {
|
|
2629
2674
|
if (!showdown) return;
|
|
@@ -2723,9 +2768,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2723
2768
|
while (slot <= 4) : (slot += 1) {
|
|
2724
2769
|
const m = active.moves[slot - 1];
|
|
2725
2770
|
if (m.id == .None) break;
|
|
2726
|
-
if (m.id == side.last_selected_move) {
|
|
2727
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2728
|
-
const
|
|
2771
|
+
if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
|
|
2772
|
+
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2773
|
+
const struggle =
|
|
2774
|
+
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2775
|
+
const s = if (struggle) 0 else slot;
|
|
2729
2776
|
out[n] = .{ .type = .Move, .data = s };
|
|
2730
2777
|
n += 1;
|
|
2731
2778
|
return n;
|