@pkmn/engine 0.1.0-dev.4eeb346e → 0.1.0-dev.53335a32

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,
@@ -536,7 +537,12 @@ fn beforeMove(
536
537
  } else {
537
538
  try log.activate(ident, .Confusion);
538
539
 
539
- 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) {
540
546
  assert(!volatiles.MultiHit);
541
547
  if (!volatiles.Rage) volatiles.state = 0;
542
548
  volatiles.Bide = false;
@@ -562,12 +568,8 @@ fn beforeMove(
562
568
  player
563
569
  else
564
570
  player.foe();
565
-
566
- const uncapped = battle.last_damage;
567
571
  // Skipping adjustDamage / randomizeDamage / checkHit
568
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
569
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
570
- if (showdown) battle.last_damage = uncapped;
571
573
 
572
574
  return .done;
573
575
  }
@@ -576,15 +578,22 @@ fn beforeMove(
576
578
 
577
579
  if (!showdown and try disabled(side, ident, log)) return .done;
578
580
 
579
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
580
- if (!volatiles.Rage) volatiles.state = 0;
581
- volatiles.Bide = false;
582
- volatiles.Thrashing = false;
583
- volatiles.Charging = false;
584
- volatiles.Binding = false;
585
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
586
- try log.cant(ident, .Paralysis);
587
- return .done;
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
+ }
588
597
  }
589
598
 
590
599
  if (volatiles.Bide) {
@@ -611,9 +620,8 @@ fn beforeMove(
611
620
  return .done;
612
621
  }
613
622
 
614
- const sub = showdown and foe.active.volatiles.Substitute;
615
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
616
- 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);
617
625
 
618
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
619
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -779,15 +787,12 @@ fn doMove(
779
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
780
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
781
789
  if (immune and move.effect != .Binding) break :miss true;
782
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
783
- battle.last_damage = 0;
784
- break :miss true;
785
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
786
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
787
792
  };
788
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
789
794
 
790
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
791
796
  const skip = status or immune;
792
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
793
798
  if (showdown and move.effect.isMulti()) {
@@ -889,9 +894,6 @@ fn doMove(
889
894
  hits = side.active.volatiles.attacks;
890
895
  }
891
896
 
892
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
893
- const sub = showdown and foe.active.volatiles.Substitute;
894
-
895
897
  var nullified = false;
896
898
  var hit: u4 = 0;
897
899
  while (hit < hits) {
@@ -907,7 +909,7 @@ fn doMove(
907
909
  if (hit == 0 and ohko) try log.ohko();
908
910
  hit += 1;
909
911
  if (foe.stored().hp == 0) break;
910
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
911
913
  // If the substitute breaks during a multi-hit attack, the attack ends
912
914
  if (nullified) break;
913
915
  }
@@ -944,8 +946,8 @@ fn doMove(
944
946
 
945
947
  if (foe.stored().hp == 0) return null;
946
948
 
947
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
948
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
949
+ // 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);
949
951
 
950
952
  if (!move.effect.isSpecial()) {
951
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -978,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
978
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
979
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
980
982
 
981
- 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;
982
985
  }
983
986
 
984
987
  fn calcDamage(
@@ -1082,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1082
1085
 
1083
1086
  fn randomizeDamage(battle: anytype) void {
1084
1087
  if (battle.last_damage <= 1) return;
1085
- 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
+
1086
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1087
1099
  }
1088
1100
 
1089
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1090
1102
  const side = battle.side(player);
1091
- const foe = battle.foe(player);
1092
1103
 
1093
1104
  if (!try checkHit(battle, player, move, log)) return null;
1094
1105
 
1095
1106
  battle.last_damage = switch (side.last_selected_move) {
1096
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1097
1108
  .SeismicToss, .NightShade => side.stored().level,
1098
1109
  .SonicBoom => 20,
1099
1110
  .DragonRage => 40,
1100
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1101
1112
  .Psywave => power: {
1102
1113
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1103
- // GLITCH: Psywave infinite glitch loop
1104
- if (!showdown and max <= 1) return Result.Error;
1105
- 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
+ }
1106
1124
  },
1107
1125
  else => unreachable,
1108
1126
  };
1109
1127
 
1110
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1111
1129
 
1112
- const sub = showdown and foe.active.volatiles.Substitute;
1113
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1114
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1115
1132
 
1116
1133
  return null;
1117
1134
  }
@@ -1234,7 +1251,19 @@ fn metronome(
1234
1251
  ) !?Result {
1235
1252
  var side = battle.side(player);
1236
1253
 
1237
- 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
+
1238
1267
  incrementPP(side, mslot);
1239
1268
 
1240
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
@@ -1292,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1292
1321
  }
1293
1322
 
1294
1323
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1295
- const state = side.active.volatiles.state;
1296
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1297
- const overwritten = overwrite and state > 0;
1298
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1299
-
1300
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
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));
1301
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1302
1329
  accuracy = accuracy * boost[0] / boost[1];
1303
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1304
1331
  accuracy = accuracy * boost[0] / boost[1];
1305
1332
  accuracy = @min(255, @max(1, accuracy));
1306
1333
 
1307
- // Pokémon Showdown only overwrites if the volatile is present
1308
- if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1309
- if (overwrite) side.active.volatiles.state = accuracy;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1310
1335
 
1311
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1312
- 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;
1313
1341
  };
1314
1342
 
1315
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1441,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1441
1469
  damage *= volatiles.toxic;
1442
1470
  }
1443
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1444
1475
  const amount = @min(damage, stored.hp);
1445
1476
  stored.hp -= amount;
1446
1477
 
1447
1478
  // As above, Pokémon Showdown uses damageOf but its not relevant
1448
- if (amount > 0) {
1449
- try log.damage(ident, stored, .LeechSeed);
1450
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1451
- if (showdown) battle.last_damage = damage;
1452
- }
1479
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1453
1480
 
1454
1481
  const before = foe_stored.hp;
1455
1482
  // Uncapped damage is added back to the foe
@@ -1536,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1536
1563
  return false;
1537
1564
  }
1538
1565
 
1539
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1540
- if (side.active.volatiles.disabled_move != 0) {
1541
- // A Pokémon that transforms after being disabled may end up with less move slots
1542
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1543
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1544
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1545
- m
1546
- else
1547
- side.last_selected_move;
1548
- if (m != .None and m == last) {
1549
- side.active.volatiles.Charging = false;
1550
- try log.disabled(ident, last);
1551
- return true;
1552
- }
1553
- }
1554
- return false;
1555
- }
1556
-
1557
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1558
- const side = battle.side(who);
1559
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1560
- try Effects.boost(battle, who, Move.get(.Rage), log);
1561
- }
1562
- }
1563
-
1564
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1565
- var volatiles = &active.volatiles;
1566
- assert(volatiles.Thrashing);
1567
- if (volatiles.attacks > 0) return false;
1568
-
1569
- volatiles.Thrashing = false;
1570
- volatiles.Confusion = true;
1571
- volatiles.confusion = Rolls.confusionDuration(battle);
1572
- return true;
1573
- }
1574
-
1575
1566
  inline fn onBegin(
1576
1567
  battle: anytype,
1577
1568
  player: Player,
@@ -1662,7 +1653,10 @@ pub const Effects = struct {
1662
1653
  side.active.volatiles.Bide = true;
1663
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1664
1655
  side.active.volatiles.state = 0;
1665
- 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);
1666
1660
 
1667
1661
  try log.start(battle.active(player), .Bide);
1668
1662
  }
@@ -1684,8 +1678,14 @@ pub const Effects = struct {
1684
1678
  return;
1685
1679
  }
1686
1680
 
1687
- if (foe.active.types.includes(move.type)) return;
1688
- 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;
1689
1689
 
1690
1690
  foe_stored.status = Status.init(.BRN);
1691
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1709,7 +1709,11 @@ pub const Effects = struct {
1709
1709
  const sub = foe.active.volatiles.Substitute;
1710
1710
 
1711
1711
  if (move.effect == .ConfusionChance) {
1712
- 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;
1713
1717
  } else {
1714
1718
  if (showdown) {
1715
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1728,7 +1732,10 @@ pub const Effects = struct {
1728
1732
 
1729
1733
  if (foe.active.volatiles.Confusion) return;
1730
1734
  foe.active.volatiles.Confusion = true;
1731
- 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);
1732
1739
 
1733
1740
  try log.start(battle.active(player.foe()), .Confusion);
1734
1741
  }
@@ -1778,8 +1785,11 @@ pub const Effects = struct {
1778
1785
  return Result.Error;
1779
1786
  }
1780
1787
 
1781
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1782
- 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);
1783
1793
 
1784
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1785
1795
  return null;
@@ -1791,7 +1801,7 @@ pub const Effects = struct {
1791
1801
 
1792
1802
  if (battle.last_damage == 0) {
1793
1803
  assert(showdown);
1794
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1795
1805
  }
1796
1806
 
1797
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1818,7 +1828,15 @@ pub const Effects = struct {
1818
1828
  var volatiles = &battle.foe(player).active.volatiles;
1819
1829
 
1820
1830
  if (volatiles.Substitute) return;
1821
- 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;
1822
1840
 
1823
1841
  volatiles.Flinch = true;
1824
1842
  volatiles.Recharging = false;
@@ -1844,8 +1862,12 @@ pub const Effects = struct {
1844
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1845
1863
  }
1846
1864
 
1847
- if (foe.active.types.includes(move.type)) return;
1848
- 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
+
1849
1871
  // Freeze Clause Mod
1850
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1851
1873
 
@@ -1990,7 +2012,7 @@ pub const Effects = struct {
1990
2012
  }
1991
2013
  if (!try checkHit(battle, player, move, log)) return;
1992
2014
 
1993
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1994
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
1995
2017
 
1996
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2011,8 +2033,7 @@ pub const Effects = struct {
2011
2033
  assert(!side.active.volatiles.MultiHit);
2012
2034
  side.active.volatiles.MultiHit = true;
2013
2035
 
2014
- side.active.volatiles.attacks =
2015
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2016
2037
  }
2017
2038
 
2018
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2056,8 +2077,14 @@ pub const Effects = struct {
2056
2077
  }
2057
2078
 
2058
2079
  // Body Slam can't paralyze a Normal type Pokémon
2059
- if (foe.active.types.includes(move.type)) return;
2060
- 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;
2061
2088
 
2062
2089
  foe_stored.status = Status.init(.PAR);
2063
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2100,7 +2127,16 @@ pub const Effects = struct {
2100
2127
 
2101
2128
  if (move.effect == .Poison) {
2102
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2103
- } 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
+ }
2104
2140
 
2105
2141
  foe_stored.status = Status.init(.PSN);
2106
2142
  if (toxic) {
@@ -2114,8 +2150,10 @@ pub const Effects = struct {
2114
2150
 
2115
2151
  fn rage(battle: anytype, player: Player) !void {
2116
2152
  var volatiles = &battle.side(player).active.volatiles;
2117
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2118
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2119
2157
  }
2120
2158
 
2121
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2167,7 +2205,16 @@ pub const Effects = struct {
2167
2205
  }
2168
2206
  foe.active.volatiles.Recharging = false;
2169
2207
 
2170
- 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);
2171
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2172
2219
  }
2173
2220
 
@@ -2220,7 +2267,11 @@ pub const Effects = struct {
2220
2267
  assert(!volatiles.Bide);
2221
2268
 
2222
2269
  volatiles.Thrashing = true;
2223
- 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);
2224
2275
  }
2225
2276
 
2226
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2262,7 +2313,7 @@ pub const Effects = struct {
2262
2313
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2263
2314
  if (!showdown) foe.active.volatiles.Recharging = false;
2264
2315
 
2265
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2266
2317
  }
2267
2318
 
2268
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2289,11 +2340,9 @@ pub const Effects = struct {
2289
2340
  return try log.fail(ident, .None);
2290
2341
  }
2291
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2292
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2293
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2294
2345
  try log.boost(ident, reason, n);
2295
- // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2296
- if (showdown and move.effect == .Rage) return;
2297
2346
  },
2298
2347
  .DefenseUp1, .DefenseUp2 => {
2299
2348
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2309,7 +2358,7 @@ pub const Effects = struct {
2309
2358
  return try log.fail(ident, .None);
2310
2359
  }
2311
2360
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2312
- const stat = unmodifiedStats(battle, side).def;
2361
+ const stat = unmodifiedStats(battle, player).def;
2313
2362
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2314
2363
  try log.boost(ident, .Defense, n);
2315
2364
  },
@@ -2326,7 +2375,7 @@ pub const Effects = struct {
2326
2375
  return try log.fail(ident, .None);
2327
2376
  }
2328
2377
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2329
- const stat = unmodifiedStats(battle, side).spe;
2378
+ const stat = unmodifiedStats(battle, player).spe;
2330
2379
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2331
2380
  try log.boost(ident, .Speed, 2);
2332
2381
  },
@@ -2346,7 +2395,7 @@ pub const Effects = struct {
2346
2395
  return try log.fail(ident, .None);
2347
2396
  }
2348
2397
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2349
- const stat = unmodifiedStats(battle, side).spc;
2398
+ const stat = unmodifiedStats(battle, player).spc;
2350
2399
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2351
2400
  try log.boost(ident, .SpecialAttack, n);
2352
2401
  try log.boost(ident, .SpecialDefense, n);
@@ -2373,7 +2422,11 @@ pub const Effects = struct {
2373
2422
  }
2374
2423
 
2375
2424
  if (move.effect.isStatDownChance()) {
2376
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2425
+ const chance = if (showdown)
2426
+ battle.rng.chance(u8, 85, 256)
2427
+ else
2428
+ battle.rng.next() < Gen12.percent(33) + 1;
2429
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2377
2430
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2378
2431
  return; // checkHit already checks for Invulnerable
2379
2432
  }
@@ -2395,7 +2448,7 @@ pub const Effects = struct {
2395
2448
  return try log.fail(foe_ident, .None);
2396
2449
  }
2397
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2398
- const stat = unmodifiedStats(battle, foe).atk;
2451
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2399
2452
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2400
2453
  try log.boost(foe_ident, .Attack, -1);
2401
2454
  },
@@ -2413,7 +2466,7 @@ pub const Effects = struct {
2413
2466
  return try log.fail(foe_ident, .None);
2414
2467
  }
2415
2468
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2416
- const stat = unmodifiedStats(battle, foe).def;
2469
+ const stat = unmodifiedStats(battle, player.foe()).def;
2417
2470
  stats.def = @max(1, stat * mod[0] / mod[1]);
2418
2471
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2419
2472
  },
@@ -2430,7 +2483,7 @@ pub const Effects = struct {
2430
2483
  return try log.fail(foe_ident, .None);
2431
2484
  }
2432
2485
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2433
- const stat = unmodifiedStats(battle, foe).spe;
2486
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2434
2487
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2435
2488
  try log.boost(foe_ident, .Speed, -1);
2436
2489
  assert(boosts.spe >= -6);
@@ -2450,7 +2503,7 @@ pub const Effects = struct {
2450
2503
  return try log.fail(foe_ident, .None);
2451
2504
  }
2452
2505
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2453
- const stat = unmodifiedStats(battle, foe).spc;
2506
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2454
2507
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2455
2508
  try log.boost(foe_ident, .SpecialAttack, -1);
2456
2509
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2469,7 +2522,8 @@ pub const Effects = struct {
2469
2522
  }
2470
2523
  };
2471
2524
 
2472
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2525
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
+ const side = battle.side(who);
2473
2527
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2474
2528
  const id = ID.from(side.active.volatiles.transform);
2475
2529
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2535,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2535
2589
  }
2536
2590
  }
2537
2591
 
2538
- pub const Rolls = struct {
2539
- inline fn speedTie(battle: anytype) bool {
2540
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2541
- return battle.rng.next() < Gen12.percent(50) + 1;
2542
- }
2543
-
2544
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2545
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2546
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2547
- }
2548
-
2549
- inline fn damage(battle: anytype) u8 {
2550
- if (showdown) return battle.rng.range(u8, 217, 256);
2551
- while (true) {
2552
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2553
- if (r >= 217) return r;
2592
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2593
+ if (side.active.volatiles.disabled_move != 0) {
2594
+ // A Pokémon that transforms after being disabled may end up with less move slots
2595
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2596
+ if (m != .None and m == side.last_selected_move) {
2597
+ side.active.volatiles.Charging = false;
2598
+ try log.disabled(ident, side.last_selected_move);
2599
+ return true;
2554
2600
  }
2555
2601
  }
2602
+ return false;
2603
+ }
2556
2604
 
2557
- inline fn hit(battle: anytype, accuracy: u16) bool {
2558
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2559
- return battle.rng.next() < accuracy;
2560
- }
2561
-
2562
- inline fn confusionDuration(battle: anytype) u3 {
2563
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2564
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2565
- }
2566
-
2567
- inline fn confused(battle: anytype) bool {
2568
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2569
- return battle.rng.next() >= Gen12.percent(50) + 1;
2570
- }
2571
-
2572
- inline fn paralyzed(battle: anytype) bool {
2573
- if (showdown) return battle.rng.chance(u8, 63, 256);
2574
- return battle.rng.next() < Gen12.percent(25);
2605
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2606
+ const side = battle.side(who);
2607
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2608
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2575
2609
  }
2610
+ }
2576
2611
 
2577
- inline fn confusionChance(battle: anytype) bool {
2578
- if (showdown) return battle.rng.chance(u8, 25, 256);
2579
- return battle.rng.next() < Gen12.percent(10);
2580
- }
2612
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
+ var volatiles = &active.volatiles;
2614
+ assert(volatiles.Thrashing);
2615
+ if (volatiles.attacks > 0) return false;
2581
2616
 
2582
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2583
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2584
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2585
- }
2617
+ volatiles.Thrashing = false;
2618
+ volatiles.Confusion = true;
2619
+ volatiles.confusion = @intCast(u3, if (showdown)
2620
+ battle.rng.range(u8, 2, 6)
2621
+ else
2622
+ (battle.rng.next() & 3) + 2);
2586
2623
 
2587
- inline fn poisonChance(battle: anytype, low: bool) bool {
2588
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2589
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2590
- }
2624
+ return true;
2625
+ }
2591
2626
 
2592
- inline fn sleepDuration(battle: anytype) u3 {
2593
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2594
- while (true) {
2595
- const r = battle.rng.next() & 7;
2596
- if (r != 0) return @intCast(u3, r);
2597
- }
2598
- }
2627
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2599
2628
 
2600
- inline fn bideThrashDuration(battle: anytype) u3 {
2601
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2602
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2603
- }
2629
+ fn distribution(battle: anytype) u3 {
2630
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2631
+ const r = (battle.rng.next() & 3);
2632
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2633
+ }
2604
2634
 
2605
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2606
- if (showdown) {
2607
- if (check_pp == 0) {
2608
- var i: usize = moves.len;
2609
- while (i > 0) {
2610
- i -= 1;
2611
- if (moves[i].id != .None) {
2612
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2613
- }
2614
- }
2615
- } else {
2616
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2617
- var i: usize = 0;
2618
- while (i < moves.len and r > 0) : (i += 1) {
2619
- if (moves[i].pp > 0) {
2620
- r -= 1;
2621
- if (r == 0) break;
2622
- }
2635
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2636
+ if (showdown) {
2637
+ if (check_pp == 0) {
2638
+ var i: usize = moves.len;
2639
+ while (i > 0) {
2640
+ i -= 1;
2641
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2642
+ }
2643
+ } else {
2644
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2645
+ var i: usize = 0;
2646
+ while (i < moves.len and r > 0) : (i += 1) {
2647
+ if (moves[i].pp > 0) {
2648
+ r -= 1;
2649
+ if (r == 0) break;
2623
2650
  }
2624
- return @intCast(u4, i + 1);
2625
2651
  }
2626
- }
2627
-
2628
- while (true) {
2629
- const r = @intCast(u4, battle.rng.next() & 3);
2630
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2652
+ return @intCast(u4, i + 1);
2631
2653
  }
2632
2654
  }
2633
2655
 
2634
- inline fn disableDuration(battle: anytype) u4 {
2635
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2636
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2656
+ while (true) {
2657
+ const r = @intCast(u4, rand.next() & 3);
2658
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2637
2659
  }
2638
-
2639
- inline fn metronome(battle: anytype) Move {
2640
- if (showdown) {
2641
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2642
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2643
- return @intToEnum(Move, r + mod);
2644
- }
2645
- while (true) {
2646
- const r = battle.rng.next();
2647
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2648
- if (r >= @enumToInt(Move.Struggle)) continue;
2649
- return @intToEnum(Move, r);
2650
- }
2651
- }
2652
-
2653
- inline fn psywave(battle: anytype, max: u8) u8 {
2654
- if (showdown) return battle.rng.range(u8, 0, max);
2655
- while (true) {
2656
- const r = battle.rng.next();
2657
- if (r < max) return r;
2658
- }
2659
- }
2660
-
2661
- inline fn distribution(battle: anytype) u3 {
2662
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2663
- const r = (battle.rng.next() & 3);
2664
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2665
- }
2666
-
2667
- inline fn unboost(battle: anytype) bool {
2668
- if (showdown) return battle.rng.chance(u8, 85, 256);
2669
- return battle.rng.next() < Gen12.percent(33) + 1;
2670
- }
2671
- };
2660
+ }
2672
2661
 
2673
2662
  test "RNG agreement" {
2674
2663
  if (!showdown) return;
@@ -2768,11 +2757,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2768
2757
  while (slot <= 4) : (slot += 1) {
2769
2758
  const m = active.moves[slot - 1];
2770
2759
  if (m.id == .None) break;
2771
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2772
- // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2773
- const struggle =
2774
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2775
- const s = if (struggle) 0 else slot;
2760
+ if (m.id == side.last_selected_move) {
2761
+ // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2762
+ const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2776
2763
  out[n] = .{ .type = .Move, .data = s };
2777
2764
  n += 1;
2778
2765
  return n;