@pkmn/engine 0.1.0-dev.9a0c07c3 → 0.1.0-dev.a5a9f4b2

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.
@@ -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 = if (showdown)
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 reqgardless of who
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;
@@ -450,7 +449,7 @@ fn executeMove(
450
449
  return doMove(battle, player, mslot, auto, residual, log);
451
450
  }
452
451
 
453
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
452
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
454
453
 
455
454
  fn beforeMove(
456
455
  battle: anytype,
@@ -513,9 +512,10 @@ fn beforeMove(
513
512
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
514
513
  if (showdown and try disabled(side, ident, log)) return .done;
515
514
 
516
- // 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
517
518
  if (showdown and volatiles.Flinch) {
518
- volatiles.Flinch = false;
519
519
  try log.cant(ident, .Flinch);
520
520
  return .done;
521
521
  }
@@ -537,12 +537,7 @@ fn beforeMove(
537
537
  } else {
538
538
  try log.activate(ident, .Confusion);
539
539
 
540
- const confused = if (showdown)
541
- !battle.rng.chance(u8, 128, 256)
542
- else
543
- battle.rng.next() >= Gen12.percent(50) + 1;
544
-
545
- if (confused) {
540
+ if (Rolls.confused(battle)) {
546
541
  assert(!volatiles.MultiHit);
547
542
  if (!volatiles.Rage) volatiles.state = 0;
548
543
  volatiles.Bide = false;
@@ -568,8 +563,12 @@ fn beforeMove(
568
563
  player
569
564
  else
570
565
  player.foe();
566
+
567
+ const uncapped = battle.last_damage;
571
568
  // Skipping adjustDamage / randomizeDamage / checkHit
572
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;
573
572
 
574
573
  return .done;
575
574
  }
@@ -578,22 +577,15 @@ fn beforeMove(
578
577
 
579
578
  if (!showdown and try disabled(side, ident, log)) return .done;
580
579
 
581
- if (Status.is(stored.status, .PAR)) {
582
- const paralyzed = if (showdown)
583
- battle.rng.chance(u8, 63, 256)
584
- else
585
- battle.rng.next() < Gen12.percent(25);
586
-
587
- if (paralyzed) {
588
- if (!volatiles.Rage) volatiles.state = 0;
589
- volatiles.Bide = false;
590
- volatiles.Thrashing = false;
591
- volatiles.Charging = false;
592
- volatiles.Binding = false;
593
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
594
- try log.cant(ident, .Paralysis);
595
- return .done;
596
- }
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;
597
589
  }
598
590
 
599
591
  if (volatiles.Bide) {
@@ -796,7 +788,7 @@ fn doMove(
796
788
  };
797
789
  assert(!immune or miss or (showdown and move.effect == .Binding));
798
790
 
799
- var late = showdown;
791
+ var late = showdown and move.effect != .Explode;
800
792
  const skip = status or immune;
801
793
  if ((!showdown or (!skip or counter)) and !miss) blk: {
802
794
  if (showdown and move.effect.isMulti()) {
@@ -916,7 +908,7 @@ fn doMove(
916
908
  if (hit == 0 and ohko) try log.ohko();
917
909
  hit += 1;
918
910
  if (foe.stored().hp == 0) break;
919
- if (!late and !sub) try buildRage(battle, player.foe(), log);
911
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
920
912
  // If the substitute breaks during a multi-hit attack, the attack ends
921
913
  if (nullified) break;
922
914
  }
@@ -987,8 +979,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
987
979
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
988
980
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
989
981
 
990
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
991
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
982
+ return Rolls.criticalHit(battle, chance);
992
983
  }
993
984
 
994
985
  fn calcDamage(
@@ -1092,16 +1083,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1092
1083
 
1093
1084
  fn randomizeDamage(battle: anytype) void {
1094
1085
  if (battle.last_damage <= 1) return;
1095
-
1096
- const random = if (showdown)
1097
- battle.rng.range(u8, 217, 256)
1098
- else loop: {
1099
- while (true) {
1100
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1101
- if (r >= 217) break :loop r;
1102
- }
1103
- };
1104
-
1086
+ const random = Rolls.damage(battle);
1105
1087
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1106
1088
  }
1107
1089
 
@@ -1119,16 +1101,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1119
1101
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1120
1102
  .Psywave => power: {
1121
1103
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1122
- if (showdown) {
1123
- break :power battle.rng.range(u8, 0, max);
1124
- } else {
1125
- // GLITCH: Psywave infinite glitch loop
1126
- if (max <= 1) return Result.Error;
1127
- while (true) {
1128
- const r = battle.rng.next();
1129
- if (r < max) break :power r;
1130
- }
1131
- }
1104
+ // GLITCH: Psywave infinite glitch loop
1105
+ if (!showdown and max <= 1) return Result.Error;
1106
+ break :power Rolls.psywave(battle, max);
1132
1107
  },
1133
1108
  else => unreachable,
1134
1109
  };
@@ -1260,19 +1235,7 @@ fn metronome(
1260
1235
  ) !?Result {
1261
1236
  var side = battle.side(player);
1262
1237
 
1263
- side.last_selected_move = if (showdown) blk: {
1264
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1265
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1266
- break :blk @intToEnum(Move, r + mod);
1267
- } else loop: {
1268
- while (true) {
1269
- const r = battle.rng.next();
1270
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1271
- if (r >= @enumToInt(Move.Struggle)) continue;
1272
- break :loop @intToEnum(Move, r);
1273
- }
1274
- };
1275
-
1238
+ side.last_selected_move = Rolls.metronome(battle);
1276
1239
  incrementPP(side, mslot);
1277
1240
 
1278
1241
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
@@ -1330,23 +1293,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1330
1293
  }
1331
1294
 
1332
1295
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1333
- var state = &side.active.volatiles.state;
1334
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1335
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1336
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
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));
1337
1302
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1338
1303
  accuracy = accuracy * boost[0] / boost[1];
1339
1304
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1340
1305
  accuracy = accuracy * boost[0] / boost[1];
1341
1306
  accuracy = @min(255, @max(1, accuracy));
1342
1307
 
1343
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
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;
1344
1311
 
1345
1312
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1346
- break :miss if (showdown)
1347
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1348
- else
1349
- battle.rng.next() >= accuracy;
1313
+ break :miss !Rolls.hit(battle, accuracy);
1350
1314
  };
1351
1315
 
1352
1316
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1478,14 +1442,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1478
1442
  damage *= volatiles.toxic;
1479
1443
  }
1480
1444
 
1481
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1482
- if (showdown) battle.last_damage = damage;
1483
-
1484
1445
  const amount = @min(damage, stored.hp);
1485
1446
  stored.hp -= amount;
1486
1447
 
1487
1448
  // As above, Pokémon Showdown uses damageOf but its not relevant
1488
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
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
+ }
1489
1454
 
1490
1455
  const before = foe_stored.hp;
1491
1456
  // Uncapped damage is added back to the foe
@@ -1572,6 +1537,42 @@ fn checkEBC(battle: anytype) bool {
1572
1537
  return false;
1573
1538
  }
1574
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
+
1575
1576
  inline fn onBegin(
1576
1577
  battle: anytype,
1577
1578
  player: Player,
@@ -1662,10 +1663,7 @@ pub const Effects = struct {
1662
1663
  side.active.volatiles.Bide = true;
1663
1664
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1664
1665
  side.active.volatiles.state = 0;
1665
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1666
- battle.rng.range(u4, 2, 4)
1667
- else
1668
- (battle.rng.next() & 1) + 2);
1666
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1669
1667
 
1670
1668
  try log.start(battle.active(player), .Bide);
1671
1669
  }
@@ -1687,14 +1685,8 @@ pub const Effects = struct {
1687
1685
  return;
1688
1686
  }
1689
1687
 
1690
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1691
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1692
- else
1693
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1694
- Gen12.percent(10)
1695
- else
1696
- Gen12.percent(30));
1697
- if (!chance) return;
1688
+ if (foe.active.types.includes(move.type)) return;
1689
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1698
1690
 
1699
1691
  foe_stored.status = Status.init(.BRN);
1700
1692
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1718,11 +1710,7 @@ pub const Effects = struct {
1718
1710
  const sub = foe.active.volatiles.Substitute;
1719
1711
 
1720
1712
  if (move.effect == .ConfusionChance) {
1721
- const chance = if (showdown)
1722
- battle.rng.chance(u8, 25, 256)
1723
- else
1724
- battle.rng.next() < Gen12.percent(10);
1725
- if (!chance) return;
1713
+ if (!Rolls.confusionChance(battle)) return;
1726
1714
  } else {
1727
1715
  if (showdown) {
1728
1716
  if (!try checkHit(battle, player, move, log)) {
@@ -1741,10 +1729,7 @@ pub const Effects = struct {
1741
1729
 
1742
1730
  if (foe.active.volatiles.Confusion) return;
1743
1731
  foe.active.volatiles.Confusion = true;
1744
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1745
- battle.rng.range(u8, 2, 6)
1746
- else
1747
- (battle.rng.next() & 3) + 2);
1732
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1748
1733
 
1749
1734
  try log.start(battle.active(player.foe()), .Confusion);
1750
1735
  }
@@ -1794,11 +1779,8 @@ pub const Effects = struct {
1794
1779
  return Result.Error;
1795
1780
  }
1796
1781
 
1797
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1798
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1799
- battle.rng.range(u8, 1, 9)
1800
- else
1801
- (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);
1802
1784
 
1803
1785
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1804
1786
  return null;
@@ -1810,7 +1792,7 @@ pub const Effects = struct {
1810
1792
 
1811
1793
  if (battle.last_damage == 0) {
1812
1794
  assert(showdown);
1813
- return;
1795
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1814
1796
  }
1815
1797
 
1816
1798
  const drain = @max(battle.last_damage / 2, 1);
@@ -1837,15 +1819,7 @@ pub const Effects = struct {
1837
1819
  var volatiles = &battle.foe(player).active.volatiles;
1838
1820
 
1839
1821
  if (volatiles.Substitute) return;
1840
-
1841
- const chance = if (showdown)
1842
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1843
- else
1844
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1845
- Gen12.percent(10)
1846
- else
1847
- Gen12.percent(30));
1848
- if (!chance) return;
1822
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1849
1823
 
1850
1824
  volatiles.Flinch = true;
1851
1825
  volatiles.Recharging = false;
@@ -1871,12 +1845,8 @@ pub const Effects = struct {
1871
1845
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1872
1846
  }
1873
1847
 
1874
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1875
- battle.rng.chance(u8, 26, 256)
1876
- else
1877
- battle.rng.next() < 1 + Gen12.percent(10);
1878
- if (!chance) return;
1879
-
1848
+ if (foe.active.types.includes(move.type)) return;
1849
+ if (!Rolls.secondaryChance(battle, true)) return;
1880
1850
  // Freeze Clause Mod
1881
1851
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1882
1852
 
@@ -2021,7 +1991,7 @@ pub const Effects = struct {
2021
1991
  }
2022
1992
  if (!try checkHit(battle, player, move, log)) return;
2023
1993
 
2024
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1994
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2025
1995
  side.active.move(oslot).id = foe.active.move(rslot).id;
2026
1996
 
2027
1997
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2042,7 +2012,8 @@ pub const Effects = struct {
2042
2012
  assert(!side.active.volatiles.MultiHit);
2043
2013
  side.active.volatiles.MultiHit = true;
2044
2014
 
2045
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2015
+ side.active.volatiles.attacks =
2016
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2046
2017
  }
2047
2018
 
2048
2019
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2086,14 +2057,8 @@ pub const Effects = struct {
2086
2057
  }
2087
2058
 
2088
2059
  // Body Slam can't paralyze a Normal type Pokémon
2089
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2090
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2091
- else
2092
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2093
- Gen12.percent(10)
2094
- else
2095
- Gen12.percent(30));
2096
- if (!chance) return;
2060
+ if (foe.active.types.includes(move.type)) return;
2061
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2097
2062
 
2098
2063
  foe_stored.status = Status.init(.PAR);
2099
2064
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2136,16 +2101,7 @@ pub const Effects = struct {
2136
2101
 
2137
2102
  if (move.effect == .Poison) {
2138
2103
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2139
- } else {
2140
- const chance = if (showdown)
2141
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2142
- else
2143
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2144
- Gen12.percent(20)
2145
- else
2146
- Gen12.percent(40));
2147
- if (!chance) return;
2148
- }
2104
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2149
2105
 
2150
2106
  foe_stored.status = Status.init(.PSN);
2151
2107
  if (toxic) {
@@ -2159,10 +2115,8 @@ pub const Effects = struct {
2159
2115
 
2160
2116
  fn rage(battle: anytype, player: Player) !void {
2161
2117
  var volatiles = &battle.side(player).active.volatiles;
2162
- if (volatiles.Rage) return;
2163
- volatiles.Rage = true;
2164
2118
  assert(!volatiles.Bide);
2165
- volatiles.state = 0;
2119
+ volatiles.Rage = true;
2166
2120
  }
2167
2121
 
2168
2122
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2214,16 +2168,7 @@ pub const Effects = struct {
2214
2168
  }
2215
2169
  foe.active.volatiles.Recharging = false;
2216
2170
 
2217
- const duration = @intCast(u3, if (showdown)
2218
- battle.rng.range(u8, 1, 8)
2219
- else loop: {
2220
- while (true) {
2221
- const r = battle.rng.next() & 7;
2222
- if (r != 0) break :loop r;
2223
- }
2224
- });
2225
-
2226
- foe_stored.status = Status.slp(duration);
2171
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2227
2172
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2228
2173
  }
2229
2174
 
@@ -2276,11 +2221,7 @@ pub const Effects = struct {
2276
2221
  assert(!volatiles.Bide);
2277
2222
 
2278
2223
  volatiles.Thrashing = true;
2279
- volatiles.state = 0;
2280
- volatiles.attacks = @intCast(u3, if (showdown)
2281
- battle.rng.range(u8, 2, 4)
2282
- else
2283
- (battle.rng.next() & 1) + 2);
2224
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2284
2225
  }
2285
2226
 
2286
2227
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2322,7 +2263,7 @@ pub const Effects = struct {
2322
2263
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2323
2264
  if (!showdown) foe.active.volatiles.Recharging = false;
2324
2265
 
2325
- side.active.volatiles.attacks = distribution(battle) - 1;
2266
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2326
2267
  }
2327
2268
 
2328
2269
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2349,7 +2290,7 @@ pub const Effects = struct {
2349
2290
  return try log.fail(ident, .None);
2350
2291
  }
2351
2292
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2352
- const stat = unmodifiedStats(battle, player).atk;
2293
+ const stat = unmodifiedStats(battle, side).atk;
2353
2294
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
2295
  try log.boost(ident, reason, n);
2355
2296
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2369,7 +2310,7 @@ pub const Effects = struct {
2369
2310
  return try log.fail(ident, .None);
2370
2311
  }
2371
2312
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2372
- const stat = unmodifiedStats(battle, player).def;
2313
+ const stat = unmodifiedStats(battle, side).def;
2373
2314
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2374
2315
  try log.boost(ident, .Defense, n);
2375
2316
  },
@@ -2386,7 +2327,7 @@ pub const Effects = struct {
2386
2327
  return try log.fail(ident, .None);
2387
2328
  }
2388
2329
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2389
- const stat = unmodifiedStats(battle, player).spe;
2330
+ const stat = unmodifiedStats(battle, side).spe;
2390
2331
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2391
2332
  try log.boost(ident, .Speed, 2);
2392
2333
  },
@@ -2406,7 +2347,7 @@ pub const Effects = struct {
2406
2347
  return try log.fail(ident, .None);
2407
2348
  }
2408
2349
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2409
- const stat = unmodifiedStats(battle, player).spc;
2350
+ const stat = unmodifiedStats(battle, side).spc;
2410
2351
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2411
2352
  try log.boost(ident, .SpecialAttack, n);
2412
2353
  try log.boost(ident, .SpecialDefense, n);
@@ -2433,11 +2374,7 @@ pub const Effects = struct {
2433
2374
  }
2434
2375
 
2435
2376
  if (move.effect.isStatDownChance()) {
2436
- const chance = if (showdown)
2437
- battle.rng.chance(u8, 85, 256)
2438
- else
2439
- battle.rng.next() < Gen12.percent(33) + 1;
2440
- if (!chance or foe.active.volatiles.Invulnerable) return;
2377
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2441
2378
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2442
2379
  return; // checkHit already checks for Invulnerable
2443
2380
  }
@@ -2459,7 +2396,7 @@ pub const Effects = struct {
2459
2396
  return try log.fail(foe_ident, .None);
2460
2397
  }
2461
2398
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2462
- const stat = unmodifiedStats(battle, player.foe()).atk;
2399
+ const stat = unmodifiedStats(battle, foe).atk;
2463
2400
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2464
2401
  try log.boost(foe_ident, .Attack, -1);
2465
2402
  },
@@ -2477,7 +2414,7 @@ pub const Effects = struct {
2477
2414
  return try log.fail(foe_ident, .None);
2478
2415
  }
2479
2416
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2480
- const stat = unmodifiedStats(battle, player.foe()).def;
2417
+ const stat = unmodifiedStats(battle, foe).def;
2481
2418
  stats.def = @max(1, stat * mod[0] / mod[1]);
2482
2419
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2483
2420
  },
@@ -2494,7 +2431,7 @@ pub const Effects = struct {
2494
2431
  return try log.fail(foe_ident, .None);
2495
2432
  }
2496
2433
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2497
- const stat = unmodifiedStats(battle, player.foe()).spe;
2434
+ const stat = unmodifiedStats(battle, foe).spe;
2498
2435
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2499
2436
  try log.boost(foe_ident, .Speed, -1);
2500
2437
  assert(boosts.spe >= -6);
@@ -2514,7 +2451,7 @@ pub const Effects = struct {
2514
2451
  return try log.fail(foe_ident, .None);
2515
2452
  }
2516
2453
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2517
- const stat = unmodifiedStats(battle, player.foe()).spc;
2454
+ const stat = unmodifiedStats(battle, foe).spc;
2518
2455
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2519
2456
  try log.boost(foe_ident, .SpecialAttack, -1);
2520
2457
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2533,8 +2470,7 @@ pub const Effects = struct {
2533
2470
  }
2534
2471
  };
2535
2472
 
2536
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2537
- const side = battle.side(who);
2473
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2538
2474
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2539
2475
  const id = ID.from(side.active.volatiles.transform);
2540
2476
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2600,80 +2536,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2600
2536
  }
2601
2537
  }
2602
2538
 
2603
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2604
- if (side.active.volatiles.disabled_move != 0) {
2605
- // A Pokémon that transforms after being disabled may end up with less move slots
2606
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2607
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2608
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2609
- m
2610
- else
2611
- side.last_selected_move;
2612
- if (m != .None and m == last) {
2613
- side.active.volatiles.Charging = false;
2614
- try log.disabled(ident, last);
2615
- return true;
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;
2616
2555
  }
2617
2556
  }
2618
- return false;
2619
- }
2620
2557
 
2621
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2622
- const side = battle.side(who);
2623
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2624
- 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;
2625
2561
  }
2626
- }
2627
2562
 
2628
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2629
- var volatiles = &active.volatiles;
2630
- assert(volatiles.Thrashing);
2631
- if (volatiles.attacks > 0) return false;
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
+ }
2632
2567
 
2633
- volatiles.Thrashing = false;
2634
- volatiles.Confusion = true;
2635
- volatiles.confusion = @intCast(u3, if (showdown)
2636
- battle.rng.range(u8, 2, 6)
2637
- else
2638
- (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
+ }
2639
2572
 
2640
- return true;
2641
- }
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
+ }
2642
2577
 
2643
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
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
+ }
2644
2582
 
2645
- fn distribution(battle: anytype) u3 {
2646
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2647
- const r = (battle.rng.next() & 3);
2648
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2649
- }
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
+ }
2650
2587
 
2651
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2652
- if (showdown) {
2653
- if (check_pp == 0) {
2654
- var i: usize = moves.len;
2655
- while (i > 0) {
2656
- i -= 1;
2657
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2658
- }
2659
- } else {
2660
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2661
- var i: usize = 0;
2662
- while (i < moves.len and r > 0) : (i += 1) {
2663
- if (moves[i].pp > 0) {
2664
- r -= 1;
2665
- if (r == 0) break;
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
+ }
2666
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);
2667
2626
  }
2668
- return @intCast(u4, i + 1);
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;
2669
2632
  }
2670
2633
  }
2671
2634
 
2672
- while (true) {
2673
- const r = @intCast(u4, rand.next() & 3);
2674
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
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);
2675
2638
  }
2676
- }
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
+ };
2677
2673
 
2678
2674
  test "RNG agreement" {
2679
2675
  if (!showdown) return;