@pkmn/engine 0.1.0-dev.729b1e87 → 0.1.0-dev.7cd30bd9

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,
@@ -442,14 +442,22 @@ fn executeMove(
442
442
  .err => return @as(?Result, Result.Error),
443
443
  }
444
444
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
445
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
446
+ return null;
447
+ }
446
448
 
447
449
  return doMove(battle, player, mslot, auto, residual, log);
448
450
  }
449
451
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
452
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
453
 
452
- 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 {
453
461
  var side = battle.side(player);
454
462
  const foe = battle.foe(player);
455
463
  var active = &side.active;
@@ -504,9 +512,10 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
512
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
513
  if (showdown and try disabled(side, ident, log)) return .done;
506
514
 
507
- // Pokémon Showdown checks for Flinch *after* instead of before
515
+ // Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
516
+ // until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
517
+ // from the same original hit
508
518
  if (showdown and volatiles.Flinch) {
509
- volatiles.Flinch = false;
510
519
  try log.cant(ident, .Flinch);
511
520
  return .done;
512
521
  }
@@ -528,14 +537,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
537
  } else {
529
538
  try log.activate(ident, .Confusion);
530
539
 
531
- const confused = if (showdown)
532
- !battle.rng.chance(u8, 128, 256)
533
- else
534
- battle.rng.next() >= Gen12.percent(50) + 1;
535
-
536
- if (confused) {
540
+ if (Rolls.confused(battle)) {
537
541
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
542
+ if (!volatiles.Rage) volatiles.state = 0;
539
543
  volatiles.Bide = false;
540
544
  volatiles.Thrashing = false;
541
545
  volatiles.MultiHit = false;
@@ -559,8 +563,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
563
  player
560
564
  else
561
565
  player.foe();
566
+
567
+ const uncapped = battle.last_damage;
562
568
  // Skipping adjustDamage / randomizeDamage / checkHit
563
569
  _ = try applyDamage(battle, player, target, .Confusion, log);
570
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
571
+ if (showdown) battle.last_damage = uncapped;
564
572
 
565
573
  return .done;
566
574
  }
@@ -569,22 +577,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
577
 
570
578
  if (!showdown and try disabled(side, ident, log)) return .done;
571
579
 
572
- if (Status.is(stored.status, .PAR)) {
573
- const paralyzed = if (showdown)
574
- battle.rng.chance(u8, 63, 256)
575
- else
576
- battle.rng.next() < Gen12.percent(25);
577
-
578
- if (paralyzed) {
579
- volatiles.state = 0;
580
- volatiles.Bide = false;
581
- volatiles.Thrashing = false;
582
- volatiles.Charging = false;
583
- volatiles.Binding = false;
584
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
585
- try log.cant(ident, .Paralysis);
586
- return .done;
587
- }
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;
588
589
  }
589
590
 
590
591
  if (volatiles.Bide) {
@@ -611,7 +612,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
612
  return .done;
612
613
  }
613
614
 
615
+ const sub = showdown and foe.active.volatiles.Substitute;
614
616
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
617
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
618
+
619
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
620
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
621
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
622
+ if (showdown) {
623
+ residual.* = false;
624
+ try handleResidual(battle, player, log);
625
+ }
626
+
615
627
  return .done;
616
628
  }
617
629
 
@@ -654,6 +666,7 @@ fn canMove(
654
666
  auto: bool,
655
667
  skip_pp: bool,
656
668
  from: ?Move,
669
+ residual: *bool,
657
670
  log: anytype,
658
671
  ) !bool {
659
672
  var side = battle.side(player);
@@ -688,7 +701,7 @@ fn canMove(
688
701
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
689
702
 
690
703
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
704
+ try onBegin(battle, player, move, mslot, residual, log);
692
705
  return false;
693
706
  }
694
707
 
@@ -767,16 +780,21 @@ fn doMove(
767
780
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
781
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
782
  if (immune and move.effect != .Binding) break :miss true;
770
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) 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
+ }
771
787
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
788
  };
773
789
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
790
 
791
+ var late = showdown and move.effect != .Explode;
775
792
  const skip = status or immune;
776
793
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
794
  if (showdown and move.effect.isMulti()) {
778
795
  Effects.multiHit(battle, player, move);
779
796
  hits = side.active.volatiles.attacks;
797
+ late = false;
780
798
  }
781
799
 
782
800
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +828,7 @@ fn doMove(
810
828
  }
811
829
 
812
830
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
813
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
831
+ if (showdown and skip) battle.last_damage = 0;
814
832
 
815
833
  miss = if (showdown or skip)
816
834
  miss
@@ -857,15 +875,9 @@ fn doMove(
857
875
  if (showdown and side.stored().hp == 0) residual.* = false;
858
876
  } else if (move.effect == .Explode) {
859
877
  try Effects.explode(battle, player);
860
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
861
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
862
- }
863
- } else if (showdown) {
864
- if (move.effect == .Disable) {
865
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
866
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
867
- }
868
- }
878
+ try buildRage(battle, player.foe(), log);
879
+ } else if (showdown and move.effect == .Disable) {
880
+ try buildRage(battle, player.foe(), log);
869
881
  }
870
882
  return null;
871
883
  }
@@ -878,6 +890,9 @@ fn doMove(
878
890
  hits = side.active.volatiles.attacks;
879
891
  }
880
892
 
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
+
881
896
  var nullified = false;
882
897
  var hit: u4 = 0;
883
898
  while (hit < hits) {
@@ -893,9 +908,7 @@ fn doMove(
893
908
  if (hit == 0 and ohko) try log.ohko();
894
909
  hit += 1;
895
910
  if (foe.stored().hp == 0) break;
896
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
897
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
898
- }
911
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
912
  // If the substitute breaks during a multi-hit attack, the attack ends
900
913
  if (nullified) break;
901
914
  }
@@ -932,6 +945,9 @@ fn doMove(
932
945
 
933
946
  if (foe.stored().hp == 0) return null;
934
947
 
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);
950
+
935
951
  if (!move.effect.isSpecial()) {
936
952
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
953
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +959,9 @@ fn doMove(
943
959
  if (move.effect == .Twineedle) {
944
960
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
961
  } else if (move.effect == .Disable) {
946
- return Effects.disable(battle, player, move, log);
962
+ const result = try Effects.disable(battle, player, move, log);
963
+ if (showdown) try buildRage(battle, player.foe(), log);
964
+ return result;
947
965
  } else {
948
966
  try moveEffect(battle, player, move, log);
949
967
  }
@@ -961,8 +979,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
979
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
980
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
981
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
982
+ return Rolls.criticalHit(battle, chance);
966
983
  }
967
984
 
968
985
  fn calcDamage(
@@ -1066,49 +1083,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1083
 
1067
1084
  fn randomizeDamage(battle: anytype) void {
1068
1085
  if (battle.last_damage <= 1) return;
1069
-
1070
- const random = if (showdown)
1071
- battle.rng.range(u8, 217, 256)
1072
- else loop: {
1073
- while (true) {
1074
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1075
- if (r >= 217) break :loop r;
1076
- }
1077
- };
1078
-
1086
+ const random = Rolls.damage(battle);
1079
1087
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1088
  }
1081
1089
 
1082
1090
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1091
  const side = battle.side(player);
1092
+ const foe = battle.foe(player);
1084
1093
 
1085
1094
  if (!try checkHit(battle, player, move, log)) return null;
1086
1095
 
1087
1096
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1097
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1098
  .SeismicToss, .NightShade => side.stored().level,
1090
1099
  .SonicBoom => 20,
1091
1100
  .DragonRage => 40,
1092
1101
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1102
  .Psywave => power: {
1094
1103
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1095
- if (showdown) {
1096
- break :power battle.rng.range(u8, 0, max);
1097
- } else {
1098
- // GLITCH: Psywave infinite glitch loop
1099
- if (max <= 1) return Result.Error;
1100
- while (true) {
1101
- const r = battle.rng.next();
1102
- if (r < max) break :power r;
1103
- }
1104
- }
1104
+ // GLITCH: Psywave infinite glitch loop
1105
+ if (!showdown and max <= 1) return Result.Error;
1106
+ break :power Rolls.psywave(battle, max);
1105
1107
  },
1106
1108
  else => unreachable,
1107
1109
  };
1108
1110
 
1109
1111
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1112
 
1113
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1114
  _ = 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);
1116
+
1112
1117
  return null;
1113
1118
  }
1114
1119
 
@@ -1216,7 +1221,7 @@ fn mirrorMove(
1216
1221
 
1217
1222
  incrementPP(side, mslot);
1218
1223
 
1219
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1224
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1220
1225
  return doMove(battle, player, mslot, auto, residual, log);
1221
1226
  }
1222
1227
 
@@ -1230,22 +1235,10 @@ fn metronome(
1230
1235
  ) !?Result {
1231
1236
  var side = battle.side(player);
1232
1237
 
1233
- side.last_selected_move = if (showdown) blk: {
1234
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1235
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1236
- break :blk @intToEnum(Move, r + mod);
1237
- } else loop: {
1238
- while (true) {
1239
- const r = battle.rng.next();
1240
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1241
- if (r >= @enumToInt(Move.Struggle)) continue;
1242
- break :loop @intToEnum(Move, r);
1243
- }
1244
- };
1245
-
1238
+ side.last_selected_move = Rolls.metronome(battle);
1246
1239
  incrementPP(side, mslot);
1247
1240
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1241
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1249
1242
  return doMove(battle, player, mslot, auto, residual, log);
1250
1243
  }
1251
1244
 
@@ -1300,23 +1293,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1300
1293
  }
1301
1294
 
1302
1295
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1303
- var state = &side.active.volatiles.state;
1304
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1305
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1306
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1296
+ const state = side.active.volatiles.state;
1297
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1298
+ const overwritten = overwrite and state > 0;
1299
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1300
+
1301
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1307
1302
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1308
1303
  accuracy = accuracy * boost[0] / boost[1];
1309
1304
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1310
1305
  accuracy = accuracy * boost[0] / boost[1];
1311
1306
  accuracy = @min(255, @max(1, accuracy));
1312
1307
 
1313
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1308
+ // Pokémon Showdown only overwrites if the volatile is present
1309
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1310
+ if (overwrite) side.active.volatiles.state = accuracy;
1314
1311
 
1315
1312
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1316
- break :miss if (showdown)
1317
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1318
- else
1319
- battle.rng.next() >= accuracy;
1313
+ break :miss !Rolls.hit(battle, accuracy);
1320
1314
  };
1321
1315
 
1322
1316
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1431,6 +1425,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1431
1425
  }
1432
1426
 
1433
1427
  if (volatiles.LeechSeed) {
1428
+ var foe = battle.foe(player);
1429
+ var foe_stored = foe.stored();
1430
+ const foe_ident = battle.active(player.foe());
1431
+
1432
+ if (foe_stored.hp == 0) {
1433
+ assert(showdown);
1434
+ return;
1435
+ }
1436
+
1434
1437
  var damage = @max(stored.stats.hp / 16, 1);
1435
1438
 
1436
1439
  // GLITCH: Leech Seed + Toxic glitch
@@ -1442,12 +1445,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1442
1445
  const amount = @min(damage, stored.hp);
1443
1446
  stored.hp -= amount;
1444
1447
 
1445
- var foe = battle.foe(player);
1446
- var foe_stored = foe.stored();
1447
- const foe_ident = battle.active(player.foe());
1448
-
1449
1448
  // As above, Pokémon Showdown uses damageOf but its not relevant
1450
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1449
+ if (amount > 0) {
1450
+ try log.damage(ident, stored, .LeechSeed);
1451
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1452
+ if (showdown) battle.last_damage = damage;
1453
+ }
1451
1454
 
1452
1455
  const before = foe_stored.hp;
1453
1456
  // Uncapped damage is added back to the foe
@@ -1534,7 +1537,50 @@ fn checkEBC(battle: anytype) bool {
1534
1537
  return false;
1535
1538
  }
1536
1539
 
1537
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
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
+ inline fn onBegin(
1577
+ battle: anytype,
1578
+ player: Player,
1579
+ move: Move.Data,
1580
+ mslot: u8,
1581
+ residual: *bool,
1582
+ log: anytype,
1583
+ ) !void {
1538
1584
  assert(move.effect.onBegin());
1539
1585
  return switch (move.effect) {
1540
1586
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1550,7 +1596,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1550
1596
  .Poison => Effects.poison(battle, player, move, log),
1551
1597
  .Reflect => Effects.reflect(battle, player, log),
1552
1598
  .Splash => Effects.splash(battle, player, log),
1553
- .Substitute => Effects.substitute(battle, player, log),
1599
+ .Substitute => Effects.substitute(battle, player, residual, log),
1554
1600
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1555
1601
  .Transform => Effects.transform(battle, player, log),
1556
1602
  else => unreachable,
@@ -1617,10 +1663,7 @@ pub const Effects = struct {
1617
1663
  side.active.volatiles.Bide = true;
1618
1664
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1619
1665
  side.active.volatiles.state = 0;
1620
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1621
- battle.rng.range(u4, 2, 4)
1622
- else
1623
- (battle.rng.next() & 1) + 2);
1666
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1624
1667
 
1625
1668
  try log.start(battle.active(player), .Bide);
1626
1669
  }
@@ -1642,14 +1685,8 @@ pub const Effects = struct {
1642
1685
  return;
1643
1686
  }
1644
1687
 
1645
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1646
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1647
- else
1648
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1649
- Gen12.percent(10)
1650
- else
1651
- Gen12.percent(30));
1652
- if (!chance) return;
1688
+ if (foe.active.types.includes(move.type)) return;
1689
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1653
1690
 
1654
1691
  foe_stored.status = Status.init(.BRN);
1655
1692
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1673,11 +1710,7 @@ pub const Effects = struct {
1673
1710
  const sub = foe.active.volatiles.Substitute;
1674
1711
 
1675
1712
  if (move.effect == .ConfusionChance) {
1676
- const chance = if (showdown)
1677
- battle.rng.chance(u8, 25, 256)
1678
- else
1679
- battle.rng.next() < Gen12.percent(10);
1680
- if (!chance) return;
1713
+ if (!Rolls.confusionChance(battle)) return;
1681
1714
  } else {
1682
1715
  if (showdown) {
1683
1716
  if (!try checkHit(battle, player, move, log)) {
@@ -1696,10 +1729,7 @@ pub const Effects = struct {
1696
1729
 
1697
1730
  if (foe.active.volatiles.Confusion) return;
1698
1731
  foe.active.volatiles.Confusion = true;
1699
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1700
- battle.rng.range(u8, 2, 6)
1701
- else
1702
- (battle.rng.next() & 3) + 2);
1732
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1703
1733
 
1704
1734
  try log.start(battle.active(player.foe()), .Confusion);
1705
1735
  }
@@ -1749,11 +1779,8 @@ pub const Effects = struct {
1749
1779
  return Result.Error;
1750
1780
  }
1751
1781
 
1752
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1753
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1754
- battle.rng.range(u8, 1, 9)
1755
- else
1756
- (battle.rng.next() & 7) + 1);
1782
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1783
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1757
1784
 
1758
1785
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1759
1786
  return null;
@@ -1765,7 +1792,7 @@ pub const Effects = struct {
1765
1792
 
1766
1793
  if (battle.last_damage == 0) {
1767
1794
  assert(showdown);
1768
- return;
1795
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1769
1796
  }
1770
1797
 
1771
1798
  const drain = @max(battle.last_damage / 2, 1);
@@ -1792,15 +1819,7 @@ pub const Effects = struct {
1792
1819
  var volatiles = &battle.foe(player).active.volatiles;
1793
1820
 
1794
1821
  if (volatiles.Substitute) return;
1795
-
1796
- const chance = if (showdown)
1797
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1798
- else
1799
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1800
- Gen12.percent(10)
1801
- else
1802
- Gen12.percent(30));
1803
- if (!chance) return;
1822
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1804
1823
 
1805
1824
  volatiles.Flinch = true;
1806
1825
  volatiles.Recharging = false;
@@ -1826,12 +1845,8 @@ pub const Effects = struct {
1826
1845
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1827
1846
  }
1828
1847
 
1829
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1830
- battle.rng.chance(u8, 26, 256)
1831
- else
1832
- battle.rng.next() < 1 + Gen12.percent(10);
1833
- if (!chance) return;
1834
-
1848
+ if (foe.active.types.includes(move.type)) return;
1849
+ if (!Rolls.secondaryChance(battle, true)) return;
1835
1850
  // Freeze Clause Mod
1836
1851
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1837
1852
 
@@ -1976,7 +1991,7 @@ pub const Effects = struct {
1976
1991
  }
1977
1992
  if (!try checkHit(battle, player, move, log)) return;
1978
1993
 
1979
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1994
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1980
1995
  side.active.move(oslot).id = foe.active.move(rslot).id;
1981
1996
 
1982
1997
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1997,7 +2012,8 @@ pub const Effects = struct {
1997
2012
  assert(!side.active.volatiles.MultiHit);
1998
2013
  side.active.volatiles.MultiHit = true;
1999
2014
 
2000
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2015
+ side.active.volatiles.attacks =
2016
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2001
2017
  }
2002
2018
 
2003
2019
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2041,14 +2057,8 @@ pub const Effects = struct {
2041
2057
  }
2042
2058
 
2043
2059
  // Body Slam can't paralyze a Normal type Pokémon
2044
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2045
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2046
- else
2047
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2048
- Gen12.percent(10)
2049
- else
2050
- Gen12.percent(30));
2051
- if (!chance) return;
2060
+ if (foe.active.types.includes(move.type)) return;
2061
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2052
2062
 
2053
2063
  foe_stored.status = Status.init(.PAR);
2054
2064
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2091,16 +2101,7 @@ pub const Effects = struct {
2091
2101
 
2092
2102
  if (move.effect == .Poison) {
2093
2103
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2094
- } else {
2095
- const chance = if (showdown)
2096
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2097
- else
2098
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2099
- Gen12.percent(20)
2100
- else
2101
- Gen12.percent(40));
2102
- if (!chance) return;
2103
- }
2104
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2104
2105
 
2105
2106
  foe_stored.status = Status.init(.PSN);
2106
2107
  if (toxic) {
@@ -2114,10 +2115,8 @@ pub const Effects = struct {
2114
2115
 
2115
2116
  fn rage(battle: anytype, player: Player) !void {
2116
2117
  var volatiles = &battle.side(player).active.volatiles;
2117
- if (volatiles.Rage) return;
2118
- volatiles.Rage = true;
2119
2118
  assert(!volatiles.Bide);
2120
- volatiles.state = 0;
2119
+ volatiles.Rage = true;
2121
2120
  }
2122
2121
 
2123
2122
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2169,16 +2168,7 @@ pub const Effects = struct {
2169
2168
  }
2170
2169
  foe.active.volatiles.Recharging = false;
2171
2170
 
2172
- const duration = @intCast(u3, if (showdown)
2173
- battle.rng.range(u8, 1, 8)
2174
- else loop: {
2175
- while (true) {
2176
- const r = battle.rng.next() & 7;
2177
- if (r != 0) break :loop r;
2178
- }
2179
- });
2180
-
2181
- foe_stored.status = Status.slp(duration);
2171
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2182
2172
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2183
2173
  }
2184
2174
 
@@ -2186,7 +2176,7 @@ pub const Effects = struct {
2186
2176
  try log.activate(battle.active(player), .Splash);
2187
2177
  }
2188
2178
 
2189
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2179
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2190
2180
  var side = battle.side(player);
2191
2181
  if (side.active.volatiles.Substitute) {
2192
2182
  try log.fail(battle.active(player), .Substitute);
@@ -2211,14 +2201,18 @@ pub const Effects = struct {
2211
2201
  side.active.volatiles.substitute = hp + 1;
2212
2202
  side.active.volatiles.Substitute = true;
2213
2203
  try log.start(battle.active(player), .Substitute);
2214
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2204
+ if (hp > 0) {
2205
+ try log.damage(battle.active(player), side.stored(), .None);
2206
+ if (showdown and side.stored().hp == 0) residual.* = false;
2207
+ }
2215
2208
  }
2216
2209
 
2217
2210
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2218
2211
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2219
2212
 
2220
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2213
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2221
2214
  _ = try checkHit(battle, player, move, log);
2215
+ battle.last_damage = 0;
2222
2216
  }
2223
2217
 
2224
2218
  fn thrashing(battle: anytype, player: Player) void {
@@ -2227,11 +2221,7 @@ pub const Effects = struct {
2227
2221
  assert(!volatiles.Bide);
2228
2222
 
2229
2223
  volatiles.Thrashing = true;
2230
- volatiles.state = 0;
2231
- volatiles.attacks = @intCast(u3, if (showdown)
2232
- battle.rng.range(u8, 2, 4)
2233
- else
2234
- (battle.rng.next() & 1) + 2);
2224
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2235
2225
  }
2236
2226
 
2237
2227
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2273,7 +2263,7 @@ pub const Effects = struct {
2273
2263
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2274
2264
  if (!showdown) foe.active.volatiles.Recharging = false;
2275
2265
 
2276
- side.active.volatiles.attacks = distribution(battle) - 1;
2266
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2277
2267
  }
2278
2268
 
2279
2269
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2300,9 +2290,11 @@ pub const Effects = struct {
2300
2290
  return try log.fail(ident, .None);
2301
2291
  }
2302
2292
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2303
- const stat = unmodifiedStats(battle, player).atk;
2293
+ const stat = unmodifiedStats(battle, side).atk;
2304
2294
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2305
2295
  try log.boost(ident, reason, n);
2296
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2297
+ if (showdown and move.effect == .Rage) return;
2306
2298
  },
2307
2299
  .DefenseUp1, .DefenseUp2 => {
2308
2300
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2318,7 +2310,7 @@ pub const Effects = struct {
2318
2310
  return try log.fail(ident, .None);
2319
2311
  }
2320
2312
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2321
- const stat = unmodifiedStats(battle, player).def;
2313
+ const stat = unmodifiedStats(battle, side).def;
2322
2314
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2323
2315
  try log.boost(ident, .Defense, n);
2324
2316
  },
@@ -2335,7 +2327,7 @@ pub const Effects = struct {
2335
2327
  return try log.fail(ident, .None);
2336
2328
  }
2337
2329
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2338
- const stat = unmodifiedStats(battle, player).spe;
2330
+ const stat = unmodifiedStats(battle, side).spe;
2339
2331
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2340
2332
  try log.boost(ident, .Speed, 2);
2341
2333
  },
@@ -2355,7 +2347,7 @@ pub const Effects = struct {
2355
2347
  return try log.fail(ident, .None);
2356
2348
  }
2357
2349
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2358
- const stat = unmodifiedStats(battle, player).spc;
2350
+ const stat = unmodifiedStats(battle, side).spc;
2359
2351
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2352
  try log.boost(ident, .SpecialAttack, n);
2361
2353
  try log.boost(ident, .SpecialDefense, n);
@@ -2382,11 +2374,7 @@ pub const Effects = struct {
2382
2374
  }
2383
2375
 
2384
2376
  if (move.effect.isStatDownChance()) {
2385
- const chance = if (showdown)
2386
- battle.rng.chance(u8, 85, 256)
2387
- else
2388
- battle.rng.next() < Gen12.percent(33) + 1;
2389
- if (!chance or foe.active.volatiles.Invulnerable) return;
2377
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2390
2378
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2391
2379
  return; // checkHit already checks for Invulnerable
2392
2380
  }
@@ -2408,7 +2396,7 @@ pub const Effects = struct {
2408
2396
  return try log.fail(foe_ident, .None);
2409
2397
  }
2410
2398
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2411
- const stat = unmodifiedStats(battle, player.foe()).atk;
2399
+ const stat = unmodifiedStats(battle, foe).atk;
2412
2400
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2413
2401
  try log.boost(foe_ident, .Attack, -1);
2414
2402
  },
@@ -2426,7 +2414,7 @@ pub const Effects = struct {
2426
2414
  return try log.fail(foe_ident, .None);
2427
2415
  }
2428
2416
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2429
- const stat = unmodifiedStats(battle, player.foe()).def;
2417
+ const stat = unmodifiedStats(battle, foe).def;
2430
2418
  stats.def = @max(1, stat * mod[0] / mod[1]);
2431
2419
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2432
2420
  },
@@ -2443,7 +2431,7 @@ pub const Effects = struct {
2443
2431
  return try log.fail(foe_ident, .None);
2444
2432
  }
2445
2433
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2446
- const stat = unmodifiedStats(battle, player.foe()).spe;
2434
+ const stat = unmodifiedStats(battle, foe).spe;
2447
2435
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2448
2436
  try log.boost(foe_ident, .Speed, -1);
2449
2437
  assert(boosts.spe >= -6);
@@ -2463,7 +2451,7 @@ pub const Effects = struct {
2463
2451
  return try log.fail(foe_ident, .None);
2464
2452
  }
2465
2453
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2466
- const stat = unmodifiedStats(battle, player.foe()).spc;
2454
+ const stat = unmodifiedStats(battle, foe).spc;
2467
2455
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2468
2456
  try log.boost(foe_ident, .SpecialAttack, -1);
2469
2457
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2482,8 +2470,7 @@ pub const Effects = struct {
2482
2470
  }
2483
2471
  };
2484
2472
 
2485
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2486
- const side = battle.side(who);
2473
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2487
2474
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2488
2475
  const id = ID.from(side.active.volatiles.transform);
2489
2476
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2497,7 +2484,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2497
2484
  }
2498
2485
  }
2499
2486
 
2500
- fn isForced(active: ActivePokemon) bool {
2487
+ inline fn isForced(active: ActivePokemon) bool {
2501
2488
  return active.volatiles.Recharging or active.volatiles.Rage or
2502
2489
  active.volatiles.Thrashing or active.volatiles.Charging;
2503
2490
  }
@@ -2549,68 +2536,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2549
2536
  }
2550
2537
  }
2551
2538
 
2552
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2553
- if (side.active.volatiles.disabled_move != 0) {
2554
- // A Pokémon that transforms after being disabled may end up with less move slots
2555
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2556
- if (m != .None and m == side.last_selected_move) {
2557
- side.active.volatiles.Charging = false;
2558
- try log.disabled(ident, side.last_selected_move);
2559
- return true;
2539
+ pub const Rolls = struct {
2540
+ inline fn speedTie(battle: anytype) bool {
2541
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2542
+ return battle.rng.next() < Gen12.percent(50) + 1;
2543
+ }
2544
+
2545
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2546
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2547
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2548
+ }
2549
+
2550
+ inline fn damage(battle: anytype) u8 {
2551
+ if (showdown) return battle.rng.range(u8, 217, 256);
2552
+ while (true) {
2553
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2554
+ if (r >= 217) return r;
2560
2555
  }
2561
2556
  }
2562
- return false;
2563
- }
2564
2557
 
2565
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2566
- var volatiles = &active.volatiles;
2567
- assert(volatiles.Thrashing);
2568
- if (volatiles.attacks > 0) return false;
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
+ }
2569
2562
 
2570
- volatiles.Thrashing = false;
2571
- volatiles.Confusion = true;
2572
- volatiles.confusion = @intCast(u3, if (showdown)
2573
- battle.rng.range(u8, 2, 6)
2574
- else
2575
- (battle.rng.next() & 3) + 2);
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
+ }
2576
2567
 
2577
- return true;
2578
- }
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
+ }
2579
2572
 
2580
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2573
+ inline fn paralyzed(battle: anytype) bool {
2574
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2575
+ return battle.rng.next() < Gen12.percent(25);
2576
+ }
2581
2577
 
2582
- fn distribution(battle: anytype) u3 {
2583
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2584
- const r = (battle.rng.next() & 3);
2585
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2586
- }
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
+ }
2587
2582
 
2588
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2589
- if (showdown) {
2590
- if (check_pp == 0) {
2591
- var i: usize = moves.len;
2592
- while (i > 0) {
2593
- i -= 1;
2594
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2595
- }
2596
- } else {
2597
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2598
- var i: usize = 0;
2599
- while (i < moves.len and r > 0) : (i += 1) {
2600
- if (moves[i].pp > 0) {
2601
- r -= 1;
2602
- if (r == 0) break;
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
+ }
2587
+
2588
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2589
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2590
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2591
+ }
2592
+
2593
+ inline fn sleepDuration(battle: anytype) u3 {
2594
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2595
+ while (true) {
2596
+ const r = battle.rng.next() & 7;
2597
+ if (r != 0) return @intCast(u3, r);
2598
+ }
2599
+ }
2600
+
2601
+ inline fn bideThrashDuration(battle: anytype) u3 {
2602
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2603
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2604
+ }
2605
+
2606
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2607
+ if (showdown) {
2608
+ if (check_pp == 0) {
2609
+ var i: usize = moves.len;
2610
+ while (i > 0) {
2611
+ i -= 1;
2612
+ if (moves[i].id != .None) {
2613
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2614
+ }
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
+ }
2603
2624
  }
2625
+ return @intCast(u4, i + 1);
2604
2626
  }
2605
- return @intCast(u4, i + 1);
2627
+ }
2628
+
2629
+ while (true) {
2630
+ const r = @intCast(u4, battle.rng.next() & 3);
2631
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2606
2632
  }
2607
2633
  }
2608
2634
 
2609
- while (true) {
2610
- const r = @intCast(u4, rand.next() & 3);
2611
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2635
+ inline fn disableDuration(battle: anytype) u4 {
2636
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2637
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2612
2638
  }
2613
- }
2639
+
2640
+ inline fn metronome(battle: anytype) Move {
2641
+ if (showdown) {
2642
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2643
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2644
+ return @intToEnum(Move, r + mod);
2645
+ }
2646
+ while (true) {
2647
+ const r = battle.rng.next();
2648
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2649
+ if (r >= @enumToInt(Move.Struggle)) continue;
2650
+ return @intToEnum(Move, r);
2651
+ }
2652
+ }
2653
+
2654
+ inline fn psywave(battle: anytype, max: u8) u8 {
2655
+ if (showdown) return battle.rng.range(u8, 0, max);
2656
+ while (true) {
2657
+ const r = battle.rng.next();
2658
+ if (r < max) return r;
2659
+ }
2660
+ }
2661
+
2662
+ inline fn distribution(battle: anytype) u3 {
2663
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2664
+ const r = (battle.rng.next() & 3);
2665
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2666
+ }
2667
+
2668
+ inline fn unboost(battle: anytype) bool {
2669
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2670
+ return battle.rng.next() < Gen12.percent(33) + 1;
2671
+ }
2672
+ };
2614
2673
 
2615
2674
  test "RNG agreement" {
2616
2675
  if (!showdown) return;
@@ -2710,9 +2769,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2710
2769
  while (slot <= 4) : (slot += 1) {
2711
2770
  const m = active.moves[slot - 1];
2712
2771
  if (m.id == .None) break;
2713
- if (m.id == side.last_selected_move) {
2714
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2715
- const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2772
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2773
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2774
+ const struggle =
2775
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2776
+ const s = if (struggle) 0 else slot;
2716
2777
  out[n] = .{ .type = .Move, .data = s };
2717
2778
  n += 1;
2718
2779
  return n;