@pkmn/engine 0.1.0-dev.a5a9f4b2 → 0.1.0-dev.b9fb6eab

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,8 +56,6 @@ 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
-
61
59
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
60
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
61
  if (battle.turn == 0) return start(battle, log);
@@ -187,7 +185,6 @@ fn selectMove(
187
185
  }
188
186
 
189
187
  // move select
190
- volatiles.state = 0;
191
188
  if (choice.data == 0) {
192
189
  const struggle = ok: {
193
190
  for (side.active.moves, 0..) |move, i| {
@@ -304,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
304
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
305
302
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
306
303
 
307
- const p1 = Rolls.speedTie(battle);
304
+ const p1 = if (showdown)
305
+ battle.rng.range(u8, 0, 2) == 0
306
+ else
307
+ battle.rng.next() < Gen12.percent(50) + 1;
308
+
308
309
  if (!showdown) return if (p1) .P1 else .P2;
309
310
 
310
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -314,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
314
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
315
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
316
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
317
- // that out and enforce host ordering of events, so P1 just goes first regardless of who
318
+ // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
318
319
  // won the original coin flip
319
320
  if (t1 and t2) return .P1;
320
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -449,7 +450,7 @@ fn executeMove(
449
450
  return doMove(battle, player, mslot, auto, residual, log);
450
451
  }
451
452
 
452
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
453
454
 
454
455
  fn beforeMove(
455
456
  battle: anytype,
@@ -512,10 +513,9 @@ fn beforeMove(
512
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
513
514
  if (showdown and try disabled(side, ident, log)) return .done;
514
515
 
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
516
+ // Pokémon Showdown checks for Flinch *after* instead of before
518
517
  if (showdown and volatiles.Flinch) {
518
+ volatiles.Flinch = false;
519
519
  try log.cant(ident, .Flinch);
520
520
  return .done;
521
521
  }
@@ -537,7 +537,12 @@ fn beforeMove(
537
537
  } else {
538
538
  try log.activate(ident, .Confusion);
539
539
 
540
- if (Rolls.confused(battle)) {
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) {
541
546
  assert(!volatiles.MultiHit);
542
547
  if (!volatiles.Rage) volatiles.state = 0;
543
548
  volatiles.Bide = false;
@@ -563,12 +568,8 @@ fn beforeMove(
563
568
  player
564
569
  else
565
570
  player.foe();
566
-
567
- const uncapped = battle.last_damage;
568
571
  // Skipping adjustDamage / randomizeDamage / checkHit
569
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
570
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
571
- if (showdown) battle.last_damage = uncapped;
572
573
 
573
574
  return .done;
574
575
  }
@@ -577,15 +578,22 @@ fn beforeMove(
577
578
 
578
579
  if (!showdown and try disabled(side, ident, log)) return .done;
579
580
 
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;
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
+ }
589
597
  }
590
598
 
591
599
  if (volatiles.Bide) {
@@ -612,9 +620,8 @@ fn beforeMove(
612
620
  return .done;
613
621
  }
614
622
 
615
- const sub = showdown and foe.active.volatiles.Substitute;
616
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
617
- if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
624
+ if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
618
625
 
619
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
620
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -780,15 +787,12 @@ fn doMove(
780
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
781
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
782
789
  if (immune and move.effect != .Binding) break :miss true;
783
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
784
- battle.last_damage = 0;
785
- break :miss true;
786
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
787
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
788
792
  };
789
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
790
794
 
791
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
792
796
  const skip = status or immune;
793
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
794
798
  if (showdown and move.effect.isMulti()) {
@@ -890,9 +894,6 @@ fn doMove(
890
894
  hits = side.active.volatiles.attacks;
891
895
  }
892
896
 
893
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
894
- const sub = showdown and foe.active.volatiles.Substitute;
895
-
896
897
  var nullified = false;
897
898
  var hit: u4 = 0;
898
899
  while (hit < hits) {
@@ -908,7 +909,7 @@ fn doMove(
908
909
  if (hit == 0 and ohko) try log.ohko();
909
910
  hit += 1;
910
911
  if (foe.stored().hp == 0) break;
911
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
912
913
  // If the substitute breaks during a multi-hit attack, the attack ends
913
914
  if (nullified) break;
914
915
  }
@@ -945,8 +946,8 @@ fn doMove(
945
946
 
946
947
  if (foe.stored().hp == 0) return null;
947
948
 
948
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
949
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
949
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
950
+ if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
950
951
 
951
952
  if (!move.effect.isSpecial()) {
952
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -979,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
979
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
980
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
981
982
 
982
- return Rolls.criticalHit(battle, chance);
983
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
983
985
  }
984
986
 
985
987
  fn calcDamage(
@@ -1083,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1083
1085
 
1084
1086
  fn randomizeDamage(battle: anytype) void {
1085
1087
  if (battle.last_damage <= 1) return;
1086
- const random = Rolls.damage(battle);
1088
+
1089
+ const random = if (showdown)
1090
+ battle.rng.range(u8, 217, 256)
1091
+ else loop: {
1092
+ while (true) {
1093
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1094
+ if (r >= 217) break :loop r;
1095
+ }
1096
+ };
1097
+
1087
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1088
1099
  }
1089
1100
 
1090
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1091
1102
  const side = battle.side(player);
1092
- const foe = battle.foe(player);
1093
1103
 
1094
1104
  if (!try checkHit(battle, player, move, log)) return null;
1095
1105
 
1096
1106
  battle.last_damage = switch (side.last_selected_move) {
1097
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1098
1108
  .SeismicToss, .NightShade => side.stored().level,
1099
1109
  .SonicBoom => 20,
1100
1110
  .DragonRage => 40,
1101
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1102
1112
  .Psywave => power: {
1103
1113
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1104
- // GLITCH: Psywave infinite glitch loop
1105
- if (!showdown and max <= 1) return Result.Error;
1106
- break :power Rolls.psywave(battle, max);
1114
+ if (showdown) {
1115
+ break :power battle.rng.range(u8, 0, max);
1116
+ } else {
1117
+ // GLITCH: Psywave infinite glitch loop
1118
+ if (max <= 1) return Result.Error;
1119
+ while (true) {
1120
+ const r = battle.rng.next();
1121
+ if (r < max) break :power r;
1122
+ }
1123
+ }
1107
1124
  },
1108
1125
  else => unreachable,
1109
1126
  };
1110
1127
 
1111
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1112
1129
 
1113
- const sub = showdown and foe.active.volatiles.Substitute;
1114
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1115
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1116
1132
 
1117
1133
  return null;
1118
1134
  }
@@ -1235,7 +1251,19 @@ fn metronome(
1235
1251
  ) !?Result {
1236
1252
  var side = battle.side(player);
1237
1253
 
1238
- side.last_selected_move = Rolls.metronome(battle);
1254
+ side.last_selected_move = if (showdown) blk: {
1255
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1256
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1257
+ break :blk @intToEnum(Move, r + mod);
1258
+ } else loop: {
1259
+ while (true) {
1260
+ const r = battle.rng.next();
1261
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1262
+ if (r >= @enumToInt(Move.Struggle)) continue;
1263
+ break :loop @intToEnum(Move, r);
1264
+ }
1265
+ };
1266
+
1239
1267
  incrementPP(side, mslot);
1240
1268
 
1241
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
@@ -1293,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1293
1321
  }
1294
1322
 
1295
1323
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
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));
1324
+ var state = &side.active.volatiles.state;
1325
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1326
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1327
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1302
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1303
1329
  accuracy = accuracy * boost[0] / boost[1];
1304
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1305
1331
  accuracy = accuracy * boost[0] / boost[1];
1306
1332
  accuracy = @min(255, @max(1, accuracy));
1307
1333
 
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;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1311
1335
 
1312
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1313
- break :miss !Rolls.hit(battle, accuracy);
1337
+ break :miss if (showdown)
1338
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
+ else
1340
+ battle.rng.next() >= accuracy;
1314
1341
  };
1315
1342
 
1316
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1442,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1442
1469
  damage *= volatiles.toxic;
1443
1470
  }
1444
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1445
1475
  const amount = @min(damage, stored.hp);
1446
1476
  stored.hp -= amount;
1447
1477
 
1448
1478
  // As above, Pokémon Showdown uses damageOf but its not relevant
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
- }
1479
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1454
1480
 
1455
1481
  const before = foe_stored.hp;
1456
1482
  // Uncapped damage is added back to the foe
@@ -1537,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1537
1563
  return false;
1538
1564
  }
1539
1565
 
1540
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1541
- if (side.active.volatiles.disabled_move != 0) {
1542
- // A Pokémon that transforms after being disabled may end up with less move slots
1543
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1544
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1545
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1546
- m
1547
- else
1548
- side.last_selected_move;
1549
- if (m != .None and m == last) {
1550
- side.active.volatiles.Charging = false;
1551
- try log.disabled(ident, last);
1552
- return true;
1553
- }
1554
- }
1555
- return false;
1556
- }
1557
-
1558
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1559
- const side = battle.side(who);
1560
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1561
- try Effects.boost(battle, who, Move.get(.Rage), log);
1562
- }
1563
- }
1564
-
1565
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1566
- var volatiles = &active.volatiles;
1567
- assert(volatiles.Thrashing);
1568
- if (volatiles.attacks > 0) return false;
1569
-
1570
- volatiles.Thrashing = false;
1571
- volatiles.Confusion = true;
1572
- volatiles.confusion = Rolls.confusionDuration(battle);
1573
- return true;
1574
- }
1575
-
1576
1566
  inline fn onBegin(
1577
1567
  battle: anytype,
1578
1568
  player: Player,
@@ -1663,7 +1653,10 @@ pub const Effects = struct {
1663
1653
  side.active.volatiles.Bide = true;
1664
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1665
1655
  side.active.volatiles.state = 0;
1666
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1656
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
+ battle.rng.range(u4, 2, 4)
1658
+ else
1659
+ (battle.rng.next() & 1) + 2);
1667
1660
 
1668
1661
  try log.start(battle.active(player), .Bide);
1669
1662
  }
@@ -1685,8 +1678,14 @@ pub const Effects = struct {
1685
1678
  return;
1686
1679
  }
1687
1680
 
1688
- if (foe.active.types.includes(move.type)) return;
1689
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1681
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1682
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1683
+ else
1684
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1685
+ Gen12.percent(10)
1686
+ else
1687
+ Gen12.percent(30));
1688
+ if (!chance) return;
1690
1689
 
1691
1690
  foe_stored.status = Status.init(.BRN);
1692
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1710,7 +1709,11 @@ pub const Effects = struct {
1710
1709
  const sub = foe.active.volatiles.Substitute;
1711
1710
 
1712
1711
  if (move.effect == .ConfusionChance) {
1713
- if (!Rolls.confusionChance(battle)) return;
1712
+ const chance = if (showdown)
1713
+ battle.rng.chance(u8, 25, 256)
1714
+ else
1715
+ battle.rng.next() < Gen12.percent(10);
1716
+ if (!chance) return;
1714
1717
  } else {
1715
1718
  if (showdown) {
1716
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1729,7 +1732,10 @@ pub const Effects = struct {
1729
1732
 
1730
1733
  if (foe.active.volatiles.Confusion) return;
1731
1734
  foe.active.volatiles.Confusion = true;
1732
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1735
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
+ battle.rng.range(u8, 2, 6)
1737
+ else
1738
+ (battle.rng.next() & 3) + 2);
1733
1739
 
1734
1740
  try log.start(battle.active(player.foe()), .Confusion);
1735
1741
  }
@@ -1779,8 +1785,11 @@ pub const Effects = struct {
1779
1785
  return Result.Error;
1780
1786
  }
1781
1787
 
1782
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1783
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1788
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1789
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1790
+ battle.rng.range(u8, 1, 9)
1791
+ else
1792
+ (battle.rng.next() & 7) + 1);
1784
1793
 
1785
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1786
1795
  return null;
@@ -1792,7 +1801,7 @@ pub const Effects = struct {
1792
1801
 
1793
1802
  if (battle.last_damage == 0) {
1794
1803
  assert(showdown);
1795
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1796
1805
  }
1797
1806
 
1798
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1819,7 +1828,15 @@ pub const Effects = struct {
1819
1828
  var volatiles = &battle.foe(player).active.volatiles;
1820
1829
 
1821
1830
  if (volatiles.Substitute) return;
1822
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1831
+
1832
+ const chance = if (showdown)
1833
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1834
+ else
1835
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1836
+ Gen12.percent(10)
1837
+ else
1838
+ Gen12.percent(30));
1839
+ if (!chance) return;
1823
1840
 
1824
1841
  volatiles.Flinch = true;
1825
1842
  volatiles.Recharging = false;
@@ -1845,8 +1862,12 @@ pub const Effects = struct {
1845
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1846
1863
  }
1847
1864
 
1848
- if (foe.active.types.includes(move.type)) return;
1849
- if (!Rolls.secondaryChance(battle, true)) return;
1865
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1866
+ battle.rng.chance(u8, 26, 256)
1867
+ else
1868
+ battle.rng.next() < 1 + Gen12.percent(10);
1869
+ if (!chance) return;
1870
+
1850
1871
  // Freeze Clause Mod
1851
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1852
1873
 
@@ -1991,7 +2012,7 @@ pub const Effects = struct {
1991
2012
  }
1992
2013
  if (!try checkHit(battle, player, move, log)) return;
1993
2014
 
1994
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1995
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
1996
2017
 
1997
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2012,8 +2033,7 @@ pub const Effects = struct {
2012
2033
  assert(!side.active.volatiles.MultiHit);
2013
2034
  side.active.volatiles.MultiHit = true;
2014
2035
 
2015
- side.active.volatiles.attacks =
2016
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2017
2037
  }
2018
2038
 
2019
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2057,8 +2077,14 @@ pub const Effects = struct {
2057
2077
  }
2058
2078
 
2059
2079
  // Body Slam can't paralyze a Normal type Pokémon
2060
- if (foe.active.types.includes(move.type)) return;
2061
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2080
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2081
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2082
+ else
2083
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2084
+ Gen12.percent(10)
2085
+ else
2086
+ Gen12.percent(30));
2087
+ if (!chance) return;
2062
2088
 
2063
2089
  foe_stored.status = Status.init(.PAR);
2064
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2101,7 +2127,16 @@ pub const Effects = struct {
2101
2127
 
2102
2128
  if (move.effect == .Poison) {
2103
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2104
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2130
+ } else {
2131
+ const chance = if (showdown)
2132
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2133
+ else
2134
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2135
+ Gen12.percent(20)
2136
+ else
2137
+ Gen12.percent(40));
2138
+ if (!chance) return;
2139
+ }
2105
2140
 
2106
2141
  foe_stored.status = Status.init(.PSN);
2107
2142
  if (toxic) {
@@ -2115,8 +2150,10 @@ pub const Effects = struct {
2115
2150
 
2116
2151
  fn rage(battle: anytype, player: Player) !void {
2117
2152
  var volatiles = &battle.side(player).active.volatiles;
2118
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2119
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2120
2157
  }
2121
2158
 
2122
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2168,7 +2205,16 @@ pub const Effects = struct {
2168
2205
  }
2169
2206
  foe.active.volatiles.Recharging = false;
2170
2207
 
2171
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2208
+ const duration = @intCast(u3, if (showdown)
2209
+ battle.rng.range(u8, 1, 8)
2210
+ else loop: {
2211
+ while (true) {
2212
+ const r = battle.rng.next() & 7;
2213
+ if (r != 0) break :loop r;
2214
+ }
2215
+ });
2216
+
2217
+ foe_stored.status = Status.slp(duration);
2172
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2173
2219
  }
2174
2220
 
@@ -2221,7 +2267,11 @@ pub const Effects = struct {
2221
2267
  assert(!volatiles.Bide);
2222
2268
 
2223
2269
  volatiles.Thrashing = true;
2224
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2270
+ volatiles.state = 0;
2271
+ volatiles.attacks = @intCast(u3, if (showdown)
2272
+ battle.rng.range(u8, 2, 4)
2273
+ else
2274
+ (battle.rng.next() & 1) + 2);
2225
2275
  }
2226
2276
 
2227
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2263,7 +2313,7 @@ pub const Effects = struct {
2263
2313
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2264
2314
  if (!showdown) foe.active.volatiles.Recharging = false;
2265
2315
 
2266
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2267
2317
  }
2268
2318
 
2269
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2290,7 +2340,7 @@ pub const Effects = struct {
2290
2340
  return try log.fail(ident, .None);
2291
2341
  }
2292
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2293
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2294
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2295
2345
  try log.boost(ident, reason, n);
2296
2346
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2310,7 +2360,7 @@ pub const Effects = struct {
2310
2360
  return try log.fail(ident, .None);
2311
2361
  }
2312
2362
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2313
- const stat = unmodifiedStats(battle, side).def;
2363
+ const stat = unmodifiedStats(battle, player).def;
2314
2364
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2315
2365
  try log.boost(ident, .Defense, n);
2316
2366
  },
@@ -2327,7 +2377,7 @@ pub const Effects = struct {
2327
2377
  return try log.fail(ident, .None);
2328
2378
  }
2329
2379
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2330
- const stat = unmodifiedStats(battle, side).spe;
2380
+ const stat = unmodifiedStats(battle, player).spe;
2331
2381
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2332
2382
  try log.boost(ident, .Speed, 2);
2333
2383
  },
@@ -2347,7 +2397,7 @@ pub const Effects = struct {
2347
2397
  return try log.fail(ident, .None);
2348
2398
  }
2349
2399
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2350
- const stat = unmodifiedStats(battle, side).spc;
2400
+ const stat = unmodifiedStats(battle, player).spc;
2351
2401
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2352
2402
  try log.boost(ident, .SpecialAttack, n);
2353
2403
  try log.boost(ident, .SpecialDefense, n);
@@ -2374,7 +2424,11 @@ pub const Effects = struct {
2374
2424
  }
2375
2425
 
2376
2426
  if (move.effect.isStatDownChance()) {
2377
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2427
+ const chance = if (showdown)
2428
+ battle.rng.chance(u8, 85, 256)
2429
+ else
2430
+ battle.rng.next() < Gen12.percent(33) + 1;
2431
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2378
2432
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2379
2433
  return; // checkHit already checks for Invulnerable
2380
2434
  }
@@ -2396,7 +2450,7 @@ pub const Effects = struct {
2396
2450
  return try log.fail(foe_ident, .None);
2397
2451
  }
2398
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2399
- const stat = unmodifiedStats(battle, foe).atk;
2453
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2400
2454
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2401
2455
  try log.boost(foe_ident, .Attack, -1);
2402
2456
  },
@@ -2414,7 +2468,7 @@ pub const Effects = struct {
2414
2468
  return try log.fail(foe_ident, .None);
2415
2469
  }
2416
2470
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2417
- const stat = unmodifiedStats(battle, foe).def;
2471
+ const stat = unmodifiedStats(battle, player.foe()).def;
2418
2472
  stats.def = @max(1, stat * mod[0] / mod[1]);
2419
2473
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2420
2474
  },
@@ -2431,7 +2485,7 @@ pub const Effects = struct {
2431
2485
  return try log.fail(foe_ident, .None);
2432
2486
  }
2433
2487
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2434
- const stat = unmodifiedStats(battle, foe).spe;
2488
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2435
2489
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2436
2490
  try log.boost(foe_ident, .Speed, -1);
2437
2491
  assert(boosts.spe >= -6);
@@ -2451,7 +2505,7 @@ pub const Effects = struct {
2451
2505
  return try log.fail(foe_ident, .None);
2452
2506
  }
2453
2507
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2454
- const stat = unmodifiedStats(battle, foe).spc;
2508
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2455
2509
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2456
2510
  try log.boost(foe_ident, .SpecialAttack, -1);
2457
2511
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2470,7 +2524,8 @@ pub const Effects = struct {
2470
2524
  }
2471
2525
  };
2472
2526
 
2473
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2527
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2528
+ const side = battle.side(who);
2474
2529
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2475
2530
  const id = ID.from(side.active.volatiles.transform);
2476
2531
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2536,140 +2591,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2536
2591
  }
2537
2592
  }
2538
2593
 
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;
2594
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2595
+ if (side.active.volatiles.disabled_move != 0) {
2596
+ // A Pokémon that transforms after being disabled may end up with less move slots
2597
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2598
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2599
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2600
+ m
2601
+ else
2602
+ side.last_selected_move;
2603
+ if (m != .None and m == last) {
2604
+ side.active.volatiles.Charging = false;
2605
+ try log.disabled(ident, last);
2606
+ return true;
2555
2607
  }
2556
2608
  }
2609
+ return false;
2610
+ }
2557
2611
 
2558
- inline fn hit(battle: anytype, accuracy: u16) bool {
2559
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2560
- return battle.rng.next() < accuracy;
2561
- }
2562
-
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
- }
2567
-
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
- }
2572
-
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);
2612
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2613
+ const side = battle.side(who);
2614
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2615
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2576
2616
  }
2617
+ }
2577
2618
 
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
- }
2619
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2620
+ var volatiles = &active.volatiles;
2621
+ assert(volatiles.Thrashing);
2622
+ if (volatiles.attacks > 0) return false;
2582
2623
 
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
- }
2624
+ volatiles.Thrashing = false;
2625
+ volatiles.Confusion = true;
2626
+ volatiles.confusion = @intCast(u3, if (showdown)
2627
+ battle.rng.range(u8, 2, 6)
2628
+ else
2629
+ (battle.rng.next() & 3) + 2);
2587
2630
 
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
- }
2631
+ return true;
2632
+ }
2592
2633
 
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
- }
2634
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2600
2635
 
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
- }
2636
+ fn distribution(battle: anytype) u3 {
2637
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2638
+ const r = (battle.rng.next() & 3);
2639
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2640
+ }
2605
2641
 
2606
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2607
- if (showdown) {
2608
- if (check_pp == 0) {
2609
- var i: usize = moves.len;
2610
- while (i > 0) {
2611
- i -= 1;
2612
- if (moves[i].id != .None) {
2613
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2614
- }
2615
- }
2616
- } else {
2617
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2618
- var i: usize = 0;
2619
- while (i < moves.len and r > 0) : (i += 1) {
2620
- if (moves[i].pp > 0) {
2621
- r -= 1;
2622
- if (r == 0) break;
2623
- }
2642
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2643
+ if (showdown) {
2644
+ if (check_pp == 0) {
2645
+ var i: usize = moves.len;
2646
+ while (i > 0) {
2647
+ i -= 1;
2648
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2649
+ }
2650
+ } else {
2651
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2652
+ var i: usize = 0;
2653
+ while (i < moves.len and r > 0) : (i += 1) {
2654
+ if (moves[i].pp > 0) {
2655
+ r -= 1;
2656
+ if (r == 0) break;
2624
2657
  }
2625
- return @intCast(u4, i + 1);
2626
2658
  }
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;
2632
- }
2633
- }
2634
-
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);
2638
- }
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);
2659
+ return @intCast(u4, i + 1);
2651
2660
  }
2652
2661
  }
2653
2662
 
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
- }
2663
+ while (true) {
2664
+ const r = @intCast(u4, rand.next() & 3);
2665
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2660
2666
  }
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
- };
2667
+ }
2673
2668
 
2674
2669
  test "RNG agreement" {
2675
2670
  if (!showdown) return;
@@ -2769,7 +2764,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2769
2764
  while (slot <= 4) : (slot += 1) {
2770
2765
  const m = active.moves[slot - 1];
2771
2766
  if (m.id == .None) break;
2772
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2767
+ if (m.id == side.last_selected_move) {
2773
2768
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2774
2769
  const struggle =
2775
2770
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);