@pkmn/engine 0.1.0-dev.2ec67dba → 0.1.0-dev.3a273268

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,8 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
+
59
61
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
60
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
61
63
  if (battle.turn == 0) return start(battle, log);
@@ -185,6 +187,7 @@ fn selectMove(
185
187
  }
186
188
 
187
189
  // move select
190
+ volatiles.state = 0;
188
191
  if (choice.data == 0) {
189
192
  const struggle = ok: {
190
193
  for (side.active.moves, 0..) |move, i| {
@@ -301,11 +304,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
301
304
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
305
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
306
 
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
308
-
307
+ const p1 = Rolls.speedTie(battle);
309
308
  if (!showdown) return if (p1) .P1 else .P2;
310
309
 
311
310
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +314,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
314
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
315
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
316
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
318
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
317
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
318
  // won the original coin flip
320
319
  if (t1 and t2) return .P1;
321
320
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -347,6 +346,7 @@ fn doTurn(
347
346
  if (try checkFaint(battle, player, log)) |r| return r;
348
347
  } else if (foe_choice.type == .Pass) return null;
349
348
 
349
+ residual = true;
350
350
  replace = battle.side(foe_player).stored().hp == 0;
351
351
  if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
352
352
  if (!replace) {
@@ -434,7 +434,7 @@ fn executeMove(
434
434
 
435
435
  var skip_can = false;
436
436
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
437
+ switch (try beforeMove(battle, player, from, residual, log)) {
438
438
  .done => return null,
439
439
  .skip_can => skip_can = true,
440
440
  .skip_pp => skip_pp = true,
@@ -451,7 +451,13 @@ fn executeMove(
451
451
 
452
452
  const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
453
453
 
454
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
454
+ fn beforeMove(
455
+ battle: anytype,
456
+ player: Player,
457
+ from: ?Move,
458
+ residual: *bool,
459
+ log: anytype,
460
+ ) !BeforeMove {
455
461
  var side = battle.side(player);
456
462
  const foe = battle.foe(player);
457
463
  var active = &side.active;
@@ -530,14 +536,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
530
536
  } else {
531
537
  try log.activate(ident, .Confusion);
532
538
 
533
- const confused = if (showdown)
534
- !battle.rng.chance(u8, 128, 256)
535
- else
536
- battle.rng.next() >= Gen12.percent(50) + 1;
537
-
538
- if (confused) {
539
+ if (Rolls.confused(battle)) {
539
540
  assert(!volatiles.MultiHit);
540
- volatiles.state = 0;
541
+ if (!volatiles.Rage) volatiles.state = 0;
541
542
  volatiles.Bide = false;
542
543
  volatiles.Thrashing = false;
543
544
  volatiles.MultiHit = false;
@@ -561,8 +562,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
561
562
  player
562
563
  else
563
564
  player.foe();
565
+
566
+ const uncapped = battle.last_damage;
564
567
  // Skipping adjustDamage / randomizeDamage / checkHit
565
568
  _ = try applyDamage(battle, player, target, .Confusion, log);
569
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
570
+ if (showdown) battle.last_damage = uncapped;
566
571
 
567
572
  return .done;
568
573
  }
@@ -571,22 +576,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
571
576
 
572
577
  if (!showdown and try disabled(side, ident, log)) return .done;
573
578
 
574
- if (Status.is(stored.status, .PAR)) {
575
- const paralyzed = if (showdown)
576
- battle.rng.chance(u8, 63, 256)
577
- else
578
- battle.rng.next() < Gen12.percent(25);
579
-
580
- if (paralyzed) {
581
- 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;
589
- }
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;
590
588
  }
591
589
 
592
590
  if (volatiles.Bide) {
@@ -613,7 +611,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
613
611
  return .done;
614
612
  }
615
613
 
614
+ const sub = showdown and foe.active.volatiles.Substitute;
616
615
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
616
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
617
+
618
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
619
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
620
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
621
+ if (showdown) {
622
+ residual.* = false;
623
+ try handleResidual(battle, player, log);
624
+ }
625
+
617
626
  return .done;
618
627
  }
619
628
 
@@ -770,16 +779,21 @@ fn doMove(
770
779
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
771
780
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
772
781
  if (immune and move.effect != .Binding) break :miss true;
773
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) 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
+ }
774
786
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
775
787
  };
776
788
  assert(!immune or miss or (showdown and move.effect == .Binding));
777
789
 
790
+ var late = showdown and move.effect != .Explode;
778
791
  const skip = status or immune;
779
792
  if ((!showdown or (!skip or counter)) and !miss) blk: {
780
793
  if (showdown and move.effect.isMulti()) {
781
794
  Effects.multiHit(battle, player, move);
782
795
  hits = side.active.volatiles.attacks;
796
+ late = false;
783
797
  }
784
798
 
785
799
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -813,7 +827,7 @@ fn doMove(
813
827
  }
814
828
 
815
829
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
816
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
830
+ if (showdown and skip) battle.last_damage = 0;
817
831
 
818
832
  miss = if (showdown or skip)
819
833
  miss
@@ -860,15 +874,9 @@ fn doMove(
860
874
  if (showdown and side.stored().hp == 0) residual.* = false;
861
875
  } else if (move.effect == .Explode) {
862
876
  try Effects.explode(battle, player);
863
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
864
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
865
- }
866
- } else if (showdown) {
867
- if (move.effect == .Disable) {
868
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
869
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
870
- }
871
- }
877
+ try buildRage(battle, player.foe(), log);
878
+ } else if (showdown and move.effect == .Disable) {
879
+ try buildRage(battle, player.foe(), log);
872
880
  }
873
881
  return null;
874
882
  }
@@ -881,6 +889,9 @@ fn doMove(
881
889
  hits = side.active.volatiles.attacks;
882
890
  }
883
891
 
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
+
884
895
  var nullified = false;
885
896
  var hit: u4 = 0;
886
897
  while (hit < hits) {
@@ -896,9 +907,7 @@ fn doMove(
896
907
  if (hit == 0 and ohko) try log.ohko();
897
908
  hit += 1;
898
909
  if (foe.stored().hp == 0) break;
899
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
900
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
901
- }
910
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
902
911
  // If the substitute breaks during a multi-hit attack, the attack ends
903
912
  if (nullified) break;
904
913
  }
@@ -935,6 +944,9 @@ fn doMove(
935
944
 
936
945
  if (foe.stored().hp == 0) return null;
937
946
 
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
+
938
950
  if (!move.effect.isSpecial()) {
939
951
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
940
952
  // second time here (after being executed in the "always happens" block above) but that
@@ -946,7 +958,9 @@ fn doMove(
946
958
  if (move.effect == .Twineedle) {
947
959
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
948
960
  } else if (move.effect == .Disable) {
949
- return Effects.disable(battle, player, move, log);
961
+ const result = try Effects.disable(battle, player, move, log);
962
+ if (showdown) try buildRage(battle, player.foe(), log);
963
+ return result;
950
964
  } else {
951
965
  try moveEffect(battle, player, move, log);
952
966
  }
@@ -964,8 +978,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
964
978
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
965
979
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
966
980
 
967
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
968
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
981
+ return Rolls.criticalHit(battle, chance);
969
982
  }
970
983
 
971
984
  fn calcDamage(
@@ -1069,49 +1082,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1069
1082
 
1070
1083
  fn randomizeDamage(battle: anytype) void {
1071
1084
  if (battle.last_damage <= 1) return;
1072
-
1073
- const random = if (showdown)
1074
- battle.rng.range(u8, 217, 256)
1075
- else loop: {
1076
- while (true) {
1077
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1078
- if (r >= 217) break :loop r;
1079
- }
1080
- };
1081
-
1085
+ const random = Rolls.damage(battle);
1082
1086
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1083
1087
  }
1084
1088
 
1085
1089
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1086
1090
  const side = battle.side(player);
1091
+ const foe = battle.foe(player);
1087
1092
 
1088
1093
  if (!try checkHit(battle, player, move, log)) return null;
1089
1094
 
1090
1095
  battle.last_damage = switch (side.last_selected_move) {
1091
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1096
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1092
1097
  .SeismicToss, .NightShade => side.stored().level,
1093
1098
  .SonicBoom => 20,
1094
1099
  .DragonRage => 40,
1095
1100
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1096
1101
  .Psywave => power: {
1097
1102
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1098
- if (showdown) {
1099
- break :power battle.rng.range(u8, 0, max);
1100
- } else {
1101
- // GLITCH: Psywave infinite glitch loop
1102
- if (max <= 1) return Result.Error;
1103
- while (true) {
1104
- const r = battle.rng.next();
1105
- if (r < max) break :power r;
1106
- }
1107
- }
1103
+ // GLITCH: Psywave infinite glitch loop
1104
+ if (!showdown and max <= 1) return Result.Error;
1105
+ break :power Rolls.psywave(battle, max);
1108
1106
  },
1109
1107
  else => unreachable,
1110
1108
  };
1111
1109
 
1112
1110
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1113
1111
 
1112
+ const sub = showdown and foe.active.volatiles.Substitute;
1114
1113
  _ = 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);
1115
+
1115
1116
  return null;
1116
1117
  }
1117
1118
 
@@ -1233,19 +1234,7 @@ fn metronome(
1233
1234
  ) !?Result {
1234
1235
  var side = battle.side(player);
1235
1236
 
1236
- side.last_selected_move = if (showdown) blk: {
1237
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1238
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1239
- break :blk @intToEnum(Move, r + mod);
1240
- } else loop: {
1241
- while (true) {
1242
- const r = battle.rng.next();
1243
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1244
- if (r >= @enumToInt(Move.Struggle)) continue;
1245
- break :loop @intToEnum(Move, r);
1246
- }
1247
- };
1248
-
1237
+ side.last_selected_move = Rolls.metronome(battle);
1249
1238
  incrementPP(side, mslot);
1250
1239
 
1251
1240
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
@@ -1303,23 +1292,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1303
1292
  }
1304
1293
 
1305
1294
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1306
- var state = &side.active.volatiles.state;
1307
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1308
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1309
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
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));
1310
1301
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1311
1302
  accuracy = accuracy * boost[0] / boost[1];
1312
1303
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1313
1304
  accuracy = accuracy * boost[0] / boost[1];
1314
1305
  accuracy = @min(255, @max(1, accuracy));
1315
1306
 
1316
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
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;
1317
1310
 
1318
1311
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1319
- break :miss if (showdown)
1320
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1321
- else
1322
- battle.rng.next() >= accuracy;
1312
+ break :miss !Rolls.hit(battle, accuracy);
1323
1313
  };
1324
1314
 
1325
1315
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1434,6 +1424,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1434
1424
  }
1435
1425
 
1436
1426
  if (volatiles.LeechSeed) {
1427
+ var foe = battle.foe(player);
1428
+ var foe_stored = foe.stored();
1429
+ const foe_ident = battle.active(player.foe());
1430
+
1431
+ if (foe_stored.hp == 0) {
1432
+ assert(showdown);
1433
+ return;
1434
+ }
1435
+
1437
1436
  var damage = @max(stored.stats.hp / 16, 1);
1438
1437
 
1439
1438
  // GLITCH: Leech Seed + Toxic glitch
@@ -1445,12 +1444,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1445
1444
  const amount = @min(damage, stored.hp);
1446
1445
  stored.hp -= amount;
1447
1446
 
1448
- var foe = battle.foe(player);
1449
- var foe_stored = foe.stored();
1450
- const foe_ident = battle.active(player.foe());
1451
-
1452
1447
  // As above, Pokémon Showdown uses damageOf but its not relevant
1453
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
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
+ }
1454
1453
 
1455
1454
  const before = foe_stored.hp;
1456
1455
  // Uncapped damage is added back to the foe
@@ -1537,6 +1536,42 @@ fn checkEBC(battle: anytype) bool {
1537
1536
  return false;
1538
1537
  }
1539
1538
 
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
+
1540
1575
  inline fn onBegin(
1541
1576
  battle: anytype,
1542
1577
  player: Player,
@@ -1627,10 +1662,7 @@ pub const Effects = struct {
1627
1662
  side.active.volatiles.Bide = true;
1628
1663
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1629
1664
  side.active.volatiles.state = 0;
1630
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1631
- battle.rng.range(u4, 2, 4)
1632
- else
1633
- (battle.rng.next() & 1) + 2);
1665
+ side.active.volatiles.attacks = Rolls.duration(battle);
1634
1666
 
1635
1667
  try log.start(battle.active(player), .Bide);
1636
1668
  }
@@ -1652,14 +1684,8 @@ pub const Effects = struct {
1652
1684
  return;
1653
1685
  }
1654
1686
 
1655
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1656
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1657
- else
1658
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1659
- Gen12.percent(10)
1660
- else
1661
- Gen12.percent(30));
1662
- if (!chance) return;
1687
+ if (foe.active.types.includes(move.type)) return;
1688
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1663
1689
 
1664
1690
  foe_stored.status = Status.init(.BRN);
1665
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1683,11 +1709,7 @@ pub const Effects = struct {
1683
1709
  const sub = foe.active.volatiles.Substitute;
1684
1710
 
1685
1711
  if (move.effect == .ConfusionChance) {
1686
- const chance = if (showdown)
1687
- battle.rng.chance(u8, 25, 256)
1688
- else
1689
- battle.rng.next() < Gen12.percent(10);
1690
- if (!chance) return;
1712
+ if (!Rolls.confusionChance(battle)) return;
1691
1713
  } else {
1692
1714
  if (showdown) {
1693
1715
  if (!try checkHit(battle, player, move, log)) {
@@ -1706,10 +1728,7 @@ pub const Effects = struct {
1706
1728
 
1707
1729
  if (foe.active.volatiles.Confusion) return;
1708
1730
  foe.active.volatiles.Confusion = true;
1709
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1710
- battle.rng.range(u8, 2, 6)
1711
- else
1712
- (battle.rng.next() & 3) + 2);
1731
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1713
1732
 
1714
1733
  try log.start(battle.active(player.foe()), .Confusion);
1715
1734
  }
@@ -1759,11 +1778,8 @@ pub const Effects = struct {
1759
1778
  return Result.Error;
1760
1779
  }
1761
1780
 
1762
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1763
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1764
- battle.rng.range(u8, 1, 9)
1765
- else
1766
- (battle.rng.next() & 7) + 1);
1781
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1782
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1767
1783
 
1768
1784
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1769
1785
  return null;
@@ -1775,7 +1791,7 @@ pub const Effects = struct {
1775
1791
 
1776
1792
  if (battle.last_damage == 0) {
1777
1793
  assert(showdown);
1778
- return;
1794
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1779
1795
  }
1780
1796
 
1781
1797
  const drain = @max(battle.last_damage / 2, 1);
@@ -1802,15 +1818,7 @@ pub const Effects = struct {
1802
1818
  var volatiles = &battle.foe(player).active.volatiles;
1803
1819
 
1804
1820
  if (volatiles.Substitute) return;
1805
-
1806
- const chance = if (showdown)
1807
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1808
- else
1809
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1810
- Gen12.percent(10)
1811
- else
1812
- Gen12.percent(30));
1813
- if (!chance) return;
1821
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1814
1822
 
1815
1823
  volatiles.Flinch = true;
1816
1824
  volatiles.Recharging = false;
@@ -1836,12 +1844,8 @@ pub const Effects = struct {
1836
1844
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1837
1845
  }
1838
1846
 
1839
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1840
- battle.rng.chance(u8, 26, 256)
1841
- else
1842
- battle.rng.next() < 1 + Gen12.percent(10);
1843
- if (!chance) return;
1844
-
1847
+ if (foe.active.types.includes(move.type)) return;
1848
+ if (!Rolls.secondaryChance(battle, true)) return;
1845
1849
  // Freeze Clause Mod
1846
1850
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1847
1851
 
@@ -1986,7 +1990,7 @@ pub const Effects = struct {
1986
1990
  }
1987
1991
  if (!try checkHit(battle, player, move, log)) return;
1988
1992
 
1989
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1993
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1990
1994
  side.active.move(oslot).id = foe.active.move(rslot).id;
1991
1995
 
1992
1996
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2007,7 +2011,8 @@ pub const Effects = struct {
2007
2011
  assert(!side.active.volatiles.MultiHit);
2008
2012
  side.active.volatiles.MultiHit = true;
2009
2013
 
2010
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2014
+ side.active.volatiles.attacks =
2015
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2011
2016
  }
2012
2017
 
2013
2018
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2051,14 +2056,8 @@ pub const Effects = struct {
2051
2056
  }
2052
2057
 
2053
2058
  // Body Slam can't paralyze a Normal type Pokémon
2054
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2055
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2056
- else
2057
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2058
- Gen12.percent(10)
2059
- else
2060
- Gen12.percent(30));
2061
- if (!chance) return;
2059
+ if (foe.active.types.includes(move.type)) return;
2060
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2062
2061
 
2063
2062
  foe_stored.status = Status.init(.PAR);
2064
2063
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2101,16 +2100,7 @@ pub const Effects = struct {
2101
2100
 
2102
2101
  if (move.effect == .Poison) {
2103
2102
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2104
- } else {
2105
- const chance = if (showdown)
2106
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2107
- else
2108
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2109
- Gen12.percent(20)
2110
- else
2111
- Gen12.percent(40));
2112
- if (!chance) return;
2113
- }
2103
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2114
2104
 
2115
2105
  foe_stored.status = Status.init(.PSN);
2116
2106
  if (toxic) {
@@ -2124,10 +2114,8 @@ pub const Effects = struct {
2124
2114
 
2125
2115
  fn rage(battle: anytype, player: Player) !void {
2126
2116
  var volatiles = &battle.side(player).active.volatiles;
2127
- if (volatiles.Rage) return;
2128
- volatiles.Rage = true;
2129
2117
  assert(!volatiles.Bide);
2130
- volatiles.state = 0;
2118
+ volatiles.Rage = true;
2131
2119
  }
2132
2120
 
2133
2121
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2179,16 +2167,7 @@ pub const Effects = struct {
2179
2167
  }
2180
2168
  foe.active.volatiles.Recharging = false;
2181
2169
 
2182
- const duration = @intCast(u3, if (showdown)
2183
- battle.rng.range(u8, 1, 8)
2184
- else loop: {
2185
- while (true) {
2186
- const r = battle.rng.next() & 7;
2187
- if (r != 0) break :loop r;
2188
- }
2189
- });
2190
-
2191
- foe_stored.status = Status.slp(duration);
2170
+ foe_stored.status = Status.slp(Rolls.sleep(battle));
2192
2171
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2193
2172
  }
2194
2173
 
@@ -2230,8 +2209,9 @@ pub const Effects = struct {
2230
2209
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2231
2210
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2232
2211
 
2233
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2212
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2234
2213
  _ = try checkHit(battle, player, move, log);
2214
+ battle.last_damage = 0;
2235
2215
  }
2236
2216
 
2237
2217
  fn thrashing(battle: anytype, player: Player) void {
@@ -2240,11 +2220,7 @@ pub const Effects = struct {
2240
2220
  assert(!volatiles.Bide);
2241
2221
 
2242
2222
  volatiles.Thrashing = true;
2243
- volatiles.state = 0;
2244
- volatiles.attacks = @intCast(u3, if (showdown)
2245
- battle.rng.range(u8, 2, 4)
2246
- else
2247
- (battle.rng.next() & 1) + 2);
2223
+ volatiles.attacks = Rolls.duration(battle);
2248
2224
  }
2249
2225
 
2250
2226
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2286,7 +2262,7 @@ pub const Effects = struct {
2286
2262
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2287
2263
  if (!showdown) foe.active.volatiles.Recharging = false;
2288
2264
 
2289
- side.active.volatiles.attacks = distribution(battle) - 1;
2265
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2290
2266
  }
2291
2267
 
2292
2268
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2313,9 +2289,11 @@ pub const Effects = struct {
2313
2289
  return try log.fail(ident, .None);
2314
2290
  }
2315
2291
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2316
- const stat = unmodifiedStats(battle, player).atk;
2292
+ const stat = unmodifiedStats(battle, side).atk;
2317
2293
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2318
2294
  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;
2319
2297
  },
2320
2298
  .DefenseUp1, .DefenseUp2 => {
2321
2299
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2331,7 +2309,7 @@ pub const Effects = struct {
2331
2309
  return try log.fail(ident, .None);
2332
2310
  }
2333
2311
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2334
- const stat = unmodifiedStats(battle, player).def;
2312
+ const stat = unmodifiedStats(battle, side).def;
2335
2313
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2336
2314
  try log.boost(ident, .Defense, n);
2337
2315
  },
@@ -2348,7 +2326,7 @@ pub const Effects = struct {
2348
2326
  return try log.fail(ident, .None);
2349
2327
  }
2350
2328
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2351
- const stat = unmodifiedStats(battle, player).spe;
2329
+ const stat = unmodifiedStats(battle, side).spe;
2352
2330
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2353
2331
  try log.boost(ident, .Speed, 2);
2354
2332
  },
@@ -2368,7 +2346,7 @@ pub const Effects = struct {
2368
2346
  return try log.fail(ident, .None);
2369
2347
  }
2370
2348
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2371
- const stat = unmodifiedStats(battle, player).spc;
2349
+ const stat = unmodifiedStats(battle, side).spc;
2372
2350
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2351
  try log.boost(ident, .SpecialAttack, n);
2374
2352
  try log.boost(ident, .SpecialDefense, n);
@@ -2395,11 +2373,7 @@ pub const Effects = struct {
2395
2373
  }
2396
2374
 
2397
2375
  if (move.effect.isStatDownChance()) {
2398
- const chance = if (showdown)
2399
- battle.rng.chance(u8, 85, 256)
2400
- else
2401
- battle.rng.next() < Gen12.percent(33) + 1;
2402
- if (!chance or foe.active.volatiles.Invulnerable) return;
2376
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2403
2377
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2404
2378
  return; // checkHit already checks for Invulnerable
2405
2379
  }
@@ -2421,7 +2395,7 @@ pub const Effects = struct {
2421
2395
  return try log.fail(foe_ident, .None);
2422
2396
  }
2423
2397
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2424
- const stat = unmodifiedStats(battle, player.foe()).atk;
2398
+ const stat = unmodifiedStats(battle, foe).atk;
2425
2399
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2426
2400
  try log.boost(foe_ident, .Attack, -1);
2427
2401
  },
@@ -2439,7 +2413,7 @@ pub const Effects = struct {
2439
2413
  return try log.fail(foe_ident, .None);
2440
2414
  }
2441
2415
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2442
- const stat = unmodifiedStats(battle, player.foe()).def;
2416
+ const stat = unmodifiedStats(battle, foe).def;
2443
2417
  stats.def = @max(1, stat * mod[0] / mod[1]);
2444
2418
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2445
2419
  },
@@ -2456,7 +2430,7 @@ pub const Effects = struct {
2456
2430
  return try log.fail(foe_ident, .None);
2457
2431
  }
2458
2432
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2459
- const stat = unmodifiedStats(battle, player.foe()).spe;
2433
+ const stat = unmodifiedStats(battle, foe).spe;
2460
2434
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2461
2435
  try log.boost(foe_ident, .Speed, -1);
2462
2436
  assert(boosts.spe >= -6);
@@ -2476,7 +2450,7 @@ pub const Effects = struct {
2476
2450
  return try log.fail(foe_ident, .None);
2477
2451
  }
2478
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2479
- const stat = unmodifiedStats(battle, player.foe()).spc;
2453
+ const stat = unmodifiedStats(battle, foe).spc;
2480
2454
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2481
2455
  try log.boost(foe_ident, .SpecialAttack, -1);
2482
2456
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2495,8 +2469,7 @@ pub const Effects = struct {
2495
2469
  }
2496
2470
  };
2497
2471
 
2498
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2499
- const side = battle.side(who);
2472
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2500
2473
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2501
2474
  const id = ID.from(side.active.volatiles.transform);
2502
2475
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2510,7 +2483,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2510
2483
  }
2511
2484
  }
2512
2485
 
2513
- fn isForced(active: ActivePokemon) bool {
2486
+ inline fn isForced(active: ActivePokemon) bool {
2514
2487
  return active.volatiles.Recharging or active.volatiles.Rage or
2515
2488
  active.volatiles.Thrashing or active.volatiles.Charging;
2516
2489
  }
@@ -2562,68 +2535,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2562
2535
  }
2563
2536
  }
2564
2537
 
2565
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2566
- if (side.active.volatiles.disabled_move != 0) {
2567
- // A Pokémon that transforms after being disabled may end up with less move slots
2568
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2569
- if (m != .None and m == side.last_selected_move) {
2570
- side.active.volatiles.Charging = false;
2571
- try log.disabled(ident, side.last_selected_move);
2572
- return true;
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;
2573
2554
  }
2574
2555
  }
2575
- return false;
2576
- }
2577
2556
 
2578
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2579
- var volatiles = &active.volatiles;
2580
- assert(volatiles.Thrashing);
2581
- if (volatiles.attacks > 0) return false;
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
+ }
2582
2561
 
2583
- volatiles.Thrashing = false;
2584
- volatiles.Confusion = true;
2585
- volatiles.confusion = @intCast(u3, if (showdown)
2586
- battle.rng.range(u8, 2, 6)
2587
- else
2588
- (battle.rng.next() & 3) + 2);
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
+ }
2589
2566
 
2590
- return true;
2591
- }
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
+ }
2592
2571
 
2593
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
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);
2575
+ }
2594
2576
 
2595
- fn distribution(battle: anytype) u3 {
2596
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2597
- const r = (battle.rng.next() & 3);
2598
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2599
- }
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
+ }
2600
2581
 
2601
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2602
- if (showdown) {
2603
- if (check_pp == 0) {
2604
- var i: usize = moves.len;
2605
- while (i > 0) {
2606
- i -= 1;
2607
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2608
- }
2609
- } else {
2610
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2611
- var i: usize = 0;
2612
- while (i < moves.len and r > 0) : (i += 1) {
2613
- if (moves[i].pp > 0) {
2614
- r -= 1;
2615
- if (r == 0) break;
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
+ }
2586
+
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
+ }
2591
+
2592
+ inline fn sleep(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
+ }
2599
+
2600
+ inline fn duration(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
+ }
2604
+
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
+ }
2616
2623
  }
2624
+ return @intCast(u4, i + 1);
2617
2625
  }
2618
- return @intCast(u4, i + 1);
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;
2619
2631
  }
2620
2632
  }
2621
2633
 
2622
- while (true) {
2623
- const r = @intCast(u4, rand.next() & 3);
2624
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
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);
2625
2637
  }
2626
- }
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
+ };
2627
2672
 
2628
2673
  test "RNG agreement" {
2629
2674
  if (!showdown) return;
@@ -2723,9 +2768,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2723
2768
  while (slot <= 4) : (slot += 1) {
2724
2769
  const m = active.moves[slot - 1];
2725
2770
  if (m.id == .None) break;
2726
- if (m.id == side.last_selected_move) {
2727
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2728
- const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
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;
2729
2776
  out[n] = .{ .type = .Move, .data = s };
2730
2777
  n += 1;
2731
2778
  return n;