@pkmn/engine 0.1.0-dev.02b7f810 → 0.1.0-dev.1da0f17c
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 +22 -22
- package/build/pkg/addon.d.ts +2 -2
- package/build/pkg/addon.js +5 -5
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build/pkg/index.d.ts +11 -10
- package/build/pkg/index.js.map +1 -1
- package/build/test/display.js +2 -1
- package/build/test/display.js.map +1 -1
- package/build.zig +17 -22
- package/package.json +8 -8
- package/src/bin/install-pkmn-engine +3 -3
- package/src/lib/binding/node.zig +5 -5
- package/src/lib/binding/wasm.zig +3 -3
- package/src/lib/common/options.zig +3 -3
- package/src/lib/common/protocol.zig +38 -38
- package/src/lib/gen1/README.md +3 -3
- package/src/lib/gen1/data.zig +6 -6
- package/src/lib/gen1/mechanics.zig +209 -220
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +7 -7
|
@@ -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);
|
|
@@ -302,11 +304,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
302
304
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
303
305
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
304
306
|
|
|
305
|
-
const p1 =
|
|
306
|
-
battle.rng.range(u8, 0, 2) == 0
|
|
307
|
-
else
|
|
308
|
-
battle.rng.next() < Gen12.percent(50) + 1;
|
|
309
|
-
|
|
307
|
+
const p1 = Rolls.speedTie(battle);
|
|
310
308
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
311
309
|
|
|
312
310
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -316,7 +314,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
316
314
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
317
315
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
318
316
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
319
|
-
// 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
|
|
320
318
|
// won the original coin flip
|
|
321
319
|
if (t1 and t2) return .P1;
|
|
322
320
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -451,7 +449,7 @@ fn executeMove(
|
|
|
451
449
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
452
450
|
}
|
|
453
451
|
|
|
454
|
-
const BeforeMove =
|
|
452
|
+
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
455
453
|
|
|
456
454
|
fn beforeMove(
|
|
457
455
|
battle: anytype,
|
|
@@ -514,9 +512,10 @@ fn beforeMove(
|
|
|
514
512
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
515
513
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
516
514
|
|
|
517
|
-
// 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
|
|
518
518
|
if (showdown and volatiles.Flinch) {
|
|
519
|
-
volatiles.Flinch = false;
|
|
520
519
|
try log.cant(ident, .Flinch);
|
|
521
520
|
return .done;
|
|
522
521
|
}
|
|
@@ -538,12 +537,7 @@ fn beforeMove(
|
|
|
538
537
|
} else {
|
|
539
538
|
try log.activate(ident, .Confusion);
|
|
540
539
|
|
|
541
|
-
|
|
542
|
-
!battle.rng.chance(u8, 128, 256)
|
|
543
|
-
else
|
|
544
|
-
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
545
|
-
|
|
546
|
-
if (confused) {
|
|
540
|
+
if (Rolls.confused(battle)) {
|
|
547
541
|
assert(!volatiles.MultiHit);
|
|
548
542
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
549
543
|
volatiles.Bide = false;
|
|
@@ -573,7 +567,7 @@ fn beforeMove(
|
|
|
573
567
|
const uncapped = battle.last_damage;
|
|
574
568
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
575
569
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
576
|
-
// Pokémon Showdown thinks that
|
|
570
|
+
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
577
571
|
if (showdown) battle.last_damage = uncapped;
|
|
578
572
|
|
|
579
573
|
return .done;
|
|
@@ -583,22 +577,15 @@ fn beforeMove(
|
|
|
583
577
|
|
|
584
578
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
585
579
|
|
|
586
|
-
if (Status.is(stored.status, .PAR)) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
volatiles.Thrashing = false;
|
|
596
|
-
volatiles.Charging = false;
|
|
597
|
-
volatiles.Binding = false;
|
|
598
|
-
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
599
|
-
try log.cant(ident, .Paralysis);
|
|
600
|
-
return .done;
|
|
601
|
-
}
|
|
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;
|
|
602
589
|
}
|
|
603
590
|
|
|
604
591
|
if (volatiles.Bide) {
|
|
@@ -992,8 +979,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
992
979
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
993
980
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
994
981
|
|
|
995
|
-
|
|
996
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
982
|
+
return Rolls.criticalHit(battle, chance);
|
|
997
983
|
}
|
|
998
984
|
|
|
999
985
|
fn calcDamage(
|
|
@@ -1097,16 +1083,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1097
1083
|
|
|
1098
1084
|
fn randomizeDamage(battle: anytype) void {
|
|
1099
1085
|
if (battle.last_damage <= 1) return;
|
|
1100
|
-
|
|
1101
|
-
const random = if (showdown)
|
|
1102
|
-
battle.rng.range(u8, 217, 256)
|
|
1103
|
-
else loop: {
|
|
1104
|
-
while (true) {
|
|
1105
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1106
|
-
if (r >= 217) break :loop r;
|
|
1107
|
-
}
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1086
|
+
const random = Rolls.damage(battle);
|
|
1110
1087
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1111
1088
|
}
|
|
1112
1089
|
|
|
@@ -1124,16 +1101,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
|
|
|
1124
1101
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1125
1102
|
.Psywave => power: {
|
|
1126
1103
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
// GLITCH: Psywave infinite glitch loop
|
|
1131
|
-
if (max <= 1) return Result.Error;
|
|
1132
|
-
while (true) {
|
|
1133
|
-
const r = battle.rng.next();
|
|
1134
|
-
if (r < max) break :power r;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1104
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1105
|
+
if (!showdown and max <= 1) return Result.Error;
|
|
1106
|
+
break :power Rolls.psywave(battle, max);
|
|
1137
1107
|
},
|
|
1138
1108
|
else => unreachable,
|
|
1139
1109
|
};
|
|
@@ -1265,19 +1235,7 @@ fn metronome(
|
|
|
1265
1235
|
) !?Result {
|
|
1266
1236
|
var side = battle.side(player);
|
|
1267
1237
|
|
|
1268
|
-
side.last_selected_move =
|
|
1269
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1270
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1271
|
-
break :blk @intToEnum(Move, r + mod);
|
|
1272
|
-
} else loop: {
|
|
1273
|
-
while (true) {
|
|
1274
|
-
const r = battle.rng.next();
|
|
1275
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1276
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1277
|
-
break :loop @intToEnum(Move, r);
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1238
|
+
side.last_selected_move = Rolls.metronome(battle);
|
|
1281
1239
|
incrementPP(side, mslot);
|
|
1282
1240
|
|
|
1283
1241
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
@@ -1352,10 +1310,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1352
1310
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1353
1311
|
|
|
1354
1312
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1355
|
-
break :miss
|
|
1356
|
-
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1357
|
-
else
|
|
1358
|
-
battle.rng.next() >= accuracy;
|
|
1313
|
+
break :miss !Rolls.hit(battle, accuracy);
|
|
1359
1314
|
};
|
|
1360
1315
|
|
|
1361
1316
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1582,6 +1537,42 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1582
1537
|
return false;
|
|
1583
1538
|
}
|
|
1584
1539
|
|
|
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
|
+
|
|
1585
1576
|
inline fn onBegin(
|
|
1586
1577
|
battle: anytype,
|
|
1587
1578
|
player: Player,
|
|
@@ -1672,10 +1663,7 @@ pub const Effects = struct {
|
|
|
1672
1663
|
side.active.volatiles.Bide = true;
|
|
1673
1664
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1674
1665
|
side.active.volatiles.state = 0;
|
|
1675
|
-
side.active.volatiles.attacks =
|
|
1676
|
-
battle.rng.range(u4, 2, 4)
|
|
1677
|
-
else
|
|
1678
|
-
(battle.rng.next() & 1) + 2);
|
|
1666
|
+
side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
1679
1667
|
|
|
1680
1668
|
try log.start(battle.active(player), .Bide);
|
|
1681
1669
|
}
|
|
@@ -1697,14 +1685,8 @@ pub const Effects = struct {
|
|
|
1697
1685
|
return;
|
|
1698
1686
|
}
|
|
1699
1687
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
else
|
|
1703
|
-
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1704
|
-
Gen12.percent(10)
|
|
1705
|
-
else
|
|
1706
|
-
Gen12.percent(30));
|
|
1707
|
-
if (!chance) return;
|
|
1688
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1689
|
+
if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
|
|
1708
1690
|
|
|
1709
1691
|
foe_stored.status = Status.init(.BRN);
|
|
1710
1692
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1728,11 +1710,7 @@ pub const Effects = struct {
|
|
|
1728
1710
|
const sub = foe.active.volatiles.Substitute;
|
|
1729
1711
|
|
|
1730
1712
|
if (move.effect == .ConfusionChance) {
|
|
1731
|
-
|
|
1732
|
-
battle.rng.chance(u8, 25, 256)
|
|
1733
|
-
else
|
|
1734
|
-
battle.rng.next() < Gen12.percent(10);
|
|
1735
|
-
if (!chance) return;
|
|
1713
|
+
if (!Rolls.confusionChance(battle)) return;
|
|
1736
1714
|
} else {
|
|
1737
1715
|
if (showdown) {
|
|
1738
1716
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1751,10 +1729,7 @@ pub const Effects = struct {
|
|
|
1751
1729
|
|
|
1752
1730
|
if (foe.active.volatiles.Confusion) return;
|
|
1753
1731
|
foe.active.volatiles.Confusion = true;
|
|
1754
|
-
foe.active.volatiles.confusion =
|
|
1755
|
-
battle.rng.range(u8, 2, 6)
|
|
1756
|
-
else
|
|
1757
|
-
(battle.rng.next() & 3) + 2);
|
|
1732
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1758
1733
|
|
|
1759
1734
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1760
1735
|
}
|
|
@@ -1804,11 +1779,8 @@ pub const Effects = struct {
|
|
|
1804
1779
|
return Result.Error;
|
|
1805
1780
|
}
|
|
1806
1781
|
|
|
1807
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1808
|
-
volatiles.disabled_duration =
|
|
1809
|
-
battle.rng.range(u8, 1, 9)
|
|
1810
|
-
else
|
|
1811
|
-
(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);
|
|
1812
1784
|
|
|
1813
1785
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1814
1786
|
return null;
|
|
@@ -1847,15 +1819,7 @@ pub const Effects = struct {
|
|
|
1847
1819
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1848
1820
|
|
|
1849
1821
|
if (volatiles.Substitute) return;
|
|
1850
|
-
|
|
1851
|
-
const chance = if (showdown)
|
|
1852
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1853
|
-
else
|
|
1854
|
-
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1855
|
-
Gen12.percent(10)
|
|
1856
|
-
else
|
|
1857
|
-
Gen12.percent(30));
|
|
1858
|
-
if (!chance) return;
|
|
1822
|
+
if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
|
|
1859
1823
|
|
|
1860
1824
|
volatiles.Flinch = true;
|
|
1861
1825
|
volatiles.Recharging = false;
|
|
@@ -1881,12 +1845,8 @@ pub const Effects = struct {
|
|
|
1881
1845
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1882
1846
|
}
|
|
1883
1847
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
else
|
|
1887
|
-
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1888
|
-
if (!chance) return;
|
|
1889
|
-
|
|
1848
|
+
if (foe.active.types.includes(move.type)) return;
|
|
1849
|
+
if (!Rolls.secondaryChance(battle, true)) return;
|
|
1890
1850
|
// Freeze Clause Mod
|
|
1891
1851
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1892
1852
|
|
|
@@ -2031,7 +1991,7 @@ pub const Effects = struct {
|
|
|
2031
1991
|
}
|
|
2032
1992
|
if (!try checkHit(battle, player, move, log)) return;
|
|
2033
1993
|
|
|
2034
|
-
const rslot =
|
|
1994
|
+
const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
|
|
2035
1995
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2036
1996
|
|
|
2037
1997
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2052,7 +2012,8 @@ pub const Effects = struct {
|
|
|
2052
2012
|
assert(!side.active.volatiles.MultiHit);
|
|
2053
2013
|
side.active.volatiles.MultiHit = true;
|
|
2054
2014
|
|
|
2055
|
-
side.active.volatiles.attacks =
|
|
2015
|
+
side.active.volatiles.attacks =
|
|
2016
|
+
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2056
2017
|
}
|
|
2057
2018
|
|
|
2058
2019
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2096,14 +2057,8 @@ pub const Effects = struct {
|
|
|
2096
2057
|
}
|
|
2097
2058
|
|
|
2098
2059
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
else
|
|
2102
|
-
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2103
|
-
Gen12.percent(10)
|
|
2104
|
-
else
|
|
2105
|
-
Gen12.percent(30));
|
|
2106
|
-
if (!chance) return;
|
|
2060
|
+
if (foe.active.types.includes(move.type)) return;
|
|
2061
|
+
if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
|
|
2107
2062
|
|
|
2108
2063
|
foe_stored.status = Status.init(.PAR);
|
|
2109
2064
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2146,16 +2101,7 @@ pub const Effects = struct {
|
|
|
2146
2101
|
|
|
2147
2102
|
if (move.effect == .Poison) {
|
|
2148
2103
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2149
|
-
} else
|
|
2150
|
-
const chance = if (showdown)
|
|
2151
|
-
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2152
|
-
else
|
|
2153
|
-
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2154
|
-
Gen12.percent(20)
|
|
2155
|
-
else
|
|
2156
|
-
Gen12.percent(40));
|
|
2157
|
-
if (!chance) return;
|
|
2158
|
-
}
|
|
2104
|
+
} else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
|
|
2159
2105
|
|
|
2160
2106
|
foe_stored.status = Status.init(.PSN);
|
|
2161
2107
|
if (toxic) {
|
|
@@ -2222,16 +2168,7 @@ pub const Effects = struct {
|
|
|
2222
2168
|
}
|
|
2223
2169
|
foe.active.volatiles.Recharging = false;
|
|
2224
2170
|
|
|
2225
|
-
|
|
2226
|
-
battle.rng.range(u8, 1, 8)
|
|
2227
|
-
else loop: {
|
|
2228
|
-
while (true) {
|
|
2229
|
-
const r = battle.rng.next() & 7;
|
|
2230
|
-
if (r != 0) break :loop r;
|
|
2231
|
-
}
|
|
2232
|
-
});
|
|
2233
|
-
|
|
2234
|
-
foe_stored.status = Status.slp(duration);
|
|
2171
|
+
foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
|
|
2235
2172
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2236
2173
|
}
|
|
2237
2174
|
|
|
@@ -2284,10 +2221,7 @@ pub const Effects = struct {
|
|
|
2284
2221
|
assert(!volatiles.Bide);
|
|
2285
2222
|
|
|
2286
2223
|
volatiles.Thrashing = true;
|
|
2287
|
-
volatiles.attacks =
|
|
2288
|
-
battle.rng.range(u8, 2, 4)
|
|
2289
|
-
else
|
|
2290
|
-
(battle.rng.next() & 1) + 2);
|
|
2224
|
+
volatiles.attacks = Rolls.bideThrashDuration(battle);
|
|
2291
2225
|
}
|
|
2292
2226
|
|
|
2293
2227
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2329,7 +2263,7 @@ pub const Effects = struct {
|
|
|
2329
2263
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2330
2264
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2331
2265
|
|
|
2332
|
-
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2266
|
+
side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
|
|
2333
2267
|
}
|
|
2334
2268
|
|
|
2335
2269
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2356,7 +2290,7 @@ pub const Effects = struct {
|
|
|
2356
2290
|
return try log.fail(ident, .None);
|
|
2357
2291
|
}
|
|
2358
2292
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2359
|
-
const stat = unmodifiedStats(battle,
|
|
2293
|
+
const stat = unmodifiedStats(battle, side).atk;
|
|
2360
2294
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2361
2295
|
try log.boost(ident, reason, n);
|
|
2362
2296
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2376,7 +2310,7 @@ pub const Effects = struct {
|
|
|
2376
2310
|
return try log.fail(ident, .None);
|
|
2377
2311
|
}
|
|
2378
2312
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2379
|
-
const stat = unmodifiedStats(battle,
|
|
2313
|
+
const stat = unmodifiedStats(battle, side).def;
|
|
2380
2314
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2381
2315
|
try log.boost(ident, .Defense, n);
|
|
2382
2316
|
},
|
|
@@ -2393,7 +2327,7 @@ pub const Effects = struct {
|
|
|
2393
2327
|
return try log.fail(ident, .None);
|
|
2394
2328
|
}
|
|
2395
2329
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2396
|
-
const stat = unmodifiedStats(battle,
|
|
2330
|
+
const stat = unmodifiedStats(battle, side).spe;
|
|
2397
2331
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2398
2332
|
try log.boost(ident, .Speed, 2);
|
|
2399
2333
|
},
|
|
@@ -2413,7 +2347,7 @@ pub const Effects = struct {
|
|
|
2413
2347
|
return try log.fail(ident, .None);
|
|
2414
2348
|
}
|
|
2415
2349
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2416
|
-
const stat = unmodifiedStats(battle,
|
|
2350
|
+
const stat = unmodifiedStats(battle, side).spc;
|
|
2417
2351
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2418
2352
|
try log.boost(ident, .SpecialAttack, n);
|
|
2419
2353
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2440,11 +2374,7 @@ pub const Effects = struct {
|
|
|
2440
2374
|
}
|
|
2441
2375
|
|
|
2442
2376
|
if (move.effect.isStatDownChance()) {
|
|
2443
|
-
|
|
2444
|
-
battle.rng.chance(u8, 85, 256)
|
|
2445
|
-
else
|
|
2446
|
-
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2447
|
-
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2377
|
+
if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
|
|
2448
2378
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2449
2379
|
return; // checkHit already checks for Invulnerable
|
|
2450
2380
|
}
|
|
@@ -2466,7 +2396,7 @@ pub const Effects = struct {
|
|
|
2466
2396
|
return try log.fail(foe_ident, .None);
|
|
2467
2397
|
}
|
|
2468
2398
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2469
|
-
const stat = unmodifiedStats(battle,
|
|
2399
|
+
const stat = unmodifiedStats(battle, foe).atk;
|
|
2470
2400
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2471
2401
|
try log.boost(foe_ident, .Attack, -1);
|
|
2472
2402
|
},
|
|
@@ -2484,7 +2414,7 @@ pub const Effects = struct {
|
|
|
2484
2414
|
return try log.fail(foe_ident, .None);
|
|
2485
2415
|
}
|
|
2486
2416
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2487
|
-
const stat = unmodifiedStats(battle,
|
|
2417
|
+
const stat = unmodifiedStats(battle, foe).def;
|
|
2488
2418
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2489
2419
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2490
2420
|
},
|
|
@@ -2501,7 +2431,7 @@ pub const Effects = struct {
|
|
|
2501
2431
|
return try log.fail(foe_ident, .None);
|
|
2502
2432
|
}
|
|
2503
2433
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2504
|
-
const stat = unmodifiedStats(battle,
|
|
2434
|
+
const stat = unmodifiedStats(battle, foe).spe;
|
|
2505
2435
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2506
2436
|
try log.boost(foe_ident, .Speed, -1);
|
|
2507
2437
|
assert(boosts.spe >= -6);
|
|
@@ -2521,7 +2451,7 @@ pub const Effects = struct {
|
|
|
2521
2451
|
return try log.fail(foe_ident, .None);
|
|
2522
2452
|
}
|
|
2523
2453
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2524
|
-
const stat = unmodifiedStats(battle,
|
|
2454
|
+
const stat = unmodifiedStats(battle, foe).spc;
|
|
2525
2455
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2526
2456
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2527
2457
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2540,8 +2470,7 @@ pub const Effects = struct {
|
|
|
2540
2470
|
}
|
|
2541
2471
|
};
|
|
2542
2472
|
|
|
2543
|
-
fn unmodifiedStats(battle: anytype,
|
|
2544
|
-
const side = battle.side(who);
|
|
2473
|
+
fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
|
|
2545
2474
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2546
2475
|
const id = ID.from(side.active.volatiles.transform);
|
|
2547
2476
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2607,80 +2536,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2607
2536
|
}
|
|
2608
2537
|
}
|
|
2609
2538
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
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;
|
|
2623
2555
|
}
|
|
2624
2556
|
}
|
|
2625
|
-
return false;
|
|
2626
|
-
}
|
|
2627
2557
|
|
|
2628
|
-
inline fn
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
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;
|
|
2632
2561
|
}
|
|
2633
|
-
}
|
|
2634
2562
|
|
|
2635
|
-
fn
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
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
|
+
}
|
|
2639
2567
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
else
|
|
2645
|
-
(battle.rng.next() & 3) + 2);
|
|
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
|
+
}
|
|
2646
2572
|
|
|
2647
|
-
|
|
2648
|
-
|
|
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
|
+
}
|
|
2649
2577
|
|
|
2650
|
-
|
|
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
|
+
}
|
|
2651
2582
|
|
|
2652
|
-
fn
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
}
|
|
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
|
+
}
|
|
2657
2587
|
|
|
2658
|
-
fn
|
|
2659
|
-
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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
|
+
}
|
|
2673
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
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return @intCast(u4, i + 1);
|
|
2674
2626
|
}
|
|
2675
|
-
|
|
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;
|
|
2676
2632
|
}
|
|
2677
2633
|
}
|
|
2678
2634
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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);
|
|
2682
2638
|
}
|
|
2683
|
-
|
|
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
|
+
};
|
|
2684
2673
|
|
|
2685
2674
|
test "RNG agreement" {
|
|
2686
2675
|
if (!showdown) return;
|