@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.
@@ -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 = if (showdown)
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 reqgardless of who
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 = union(enum) { done, skip_can, skip_pp, ok, err };
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
- const confused = if (showdown)
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 Confusion damage is uncapped ¯\_(ツ)_/¯
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
- const paralyzed = if (showdown)
588
- battle.rng.chance(u8, 63, 256)
589
- else
590
- battle.rng.next() < Gen12.percent(25);
591
-
592
- if (paralyzed) {
593
- if (!volatiles.Rage) volatiles.state = 0;
594
- volatiles.Bide = false;
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
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
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
- if (showdown) {
1128
- break :power battle.rng.range(u8, 0, max);
1129
- } else {
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 = if (showdown) blk: {
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 if (showdown)
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 = @intCast(u3, if (showdown)
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
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1701
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
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
- const chance = if (showdown)
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 = @intCast(u3, if (showdown)
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, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1808
- volatiles.disabled_duration = @intCast(u4, if (showdown)
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
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1885
- battle.rng.chance(u8, 26, 256)
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 = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
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 = if (move.effect == .MultiHit) distribution(battle) else 2;
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
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2100
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
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
- const duration = @intCast(u3, if (showdown)
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 = @intCast(u3, if (showdown)
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, player).atk;
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, player).def;
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, player).spe;
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, player).spc;
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
- const chance = if (showdown)
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, player.foe()).atk;
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, player.foe()).def;
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, player.foe()).spe;
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, player.foe()).spc;
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, who: Player) *Stats(u16) {
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
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2611
- if (side.active.volatiles.disabled_move != 0) {
2612
- // A Pokémon that transforms after being disabled may end up with less move slots
2613
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2614
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2615
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2616
- m
2617
- else
2618
- side.last_selected_move;
2619
- if (m != .None and m == last) {
2620
- side.active.volatiles.Charging = false;
2621
- try log.disabled(ident, last);
2622
- 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;
2623
2555
  }
2624
2556
  }
2625
- return false;
2626
- }
2627
2557
 
2628
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2629
- const side = battle.side(who);
2630
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
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 handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2636
- var volatiles = &active.volatiles;
2637
- assert(volatiles.Thrashing);
2638
- 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
+ }
2639
2567
 
2640
- volatiles.Thrashing = false;
2641
- volatiles.Confusion = true;
2642
- volatiles.confusion = @intCast(u3, if (showdown)
2643
- battle.rng.range(u8, 2, 6)
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
- return true;
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
- 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
+ }
2651
2582
 
2652
- fn distribution(battle: anytype) u3 {
2653
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2654
- const r = (battle.rng.next() & 3);
2655
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
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 randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2659
- if (showdown) {
2660
- if (check_pp == 0) {
2661
- var i: usize = moves.len;
2662
- while (i > 0) {
2663
- i -= 1;
2664
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2665
- }
2666
- } else {
2667
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2668
- var i: usize = 0;
2669
- while (i < moves.len and r > 0) : (i += 1) {
2670
- if (moves[i].pp > 0) {
2671
- r -= 1;
2672
- 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
+ }
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
- 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;
2676
2632
  }
2677
2633
  }
2678
2634
 
2679
- while (true) {
2680
- const r = @intCast(u4, rand.next() & 3);
2681
- 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);
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;