@pkmn/engine 0.1.0-dev.0eb29f53 → 0.1.0-dev.207fa86a

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;
@@ -346,7 +347,6 @@ fn doTurn(
346
347
  if (try checkFaint(battle, player, log)) |r| return r;
347
348
  } else if (foe_choice.type == .Pass) return null;
348
349
 
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, residual, log)) {
437
+ switch (try beforeMove(battle, player, from, log)) {
438
438
  .done => return null,
439
439
  .skip_can => skip_can = true,
440
440
  .skip_pp => skip_pp = true,
@@ -442,22 +442,14 @@ 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, residual, log)) {
446
- return null;
447
- }
445
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
448
446
 
449
447
  return doMove(battle, player, mslot, auto, residual, log);
450
448
  }
451
449
 
452
450
  const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
453
451
 
454
- fn beforeMove(
455
- battle: anytype,
456
- player: Player,
457
- from: ?Move,
458
- residual: *bool,
459
- log: anytype,
460
- ) !BeforeMove {
452
+ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
461
453
  var side = battle.side(player);
462
454
  const foe = battle.foe(player);
463
455
  var active = &side.active;
@@ -536,9 +528,14 @@ fn beforeMove(
536
528
  } else {
537
529
  try log.activate(ident, .Confusion);
538
530
 
539
- if (Rolls.confused(battle)) {
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
537
  assert(!volatiles.MultiHit);
541
- if (!volatiles.Rage) volatiles.state = 0;
538
+ volatiles.state = 0;
542
539
  volatiles.Bide = false;
543
540
  volatiles.Thrashing = false;
544
541
  volatiles.MultiHit = false;
@@ -562,12 +559,8 @@ fn beforeMove(
562
559
  player
563
560
  else
564
561
  player.foe();
565
-
566
- const uncapped = battle.last_damage;
567
562
  // Skipping adjustDamage / randomizeDamage / checkHit
568
563
  _ = 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
564
 
572
565
  return .done;
573
566
  }
@@ -576,15 +569,22 @@ fn beforeMove(
576
569
 
577
570
  if (!showdown and try disabled(side, ident, log)) return .done;
578
571
 
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;
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
+ }
588
588
  }
589
589
 
590
590
  if (volatiles.Bide) {
@@ -611,18 +611,7 @@ fn beforeMove(
611
611
  return .done;
612
612
  }
613
613
 
614
- const sub = showdown and foe.active.volatiles.Substitute;
615
614
  _ = 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
-
626
615
  return .done;
627
616
  }
628
617
 
@@ -665,7 +654,6 @@ fn canMove(
665
654
  auto: bool,
666
655
  skip_pp: bool,
667
656
  from: ?Move,
668
- residual: *bool,
669
657
  log: anytype,
670
658
  ) !bool {
671
659
  var side = battle.side(player);
@@ -700,7 +688,7 @@ fn canMove(
700
688
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
701
689
 
702
690
  if (move.effect.onBegin()) {
703
- try onBegin(battle, player, move, mslot, residual, log);
691
+ try onBegin(battle, player, move, mslot, log);
704
692
  return false;
705
693
  }
706
694
 
@@ -779,21 +767,16 @@ fn doMove(
779
767
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
780
768
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
781
769
  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
- }
770
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
786
771
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
787
772
  };
788
773
  assert(!immune or miss or (showdown and move.effect == .Binding));
789
774
 
790
- var late = showdown and move.effect != .Explode;
791
775
  const skip = status or immune;
792
776
  if ((!showdown or (!skip or counter)) and !miss) blk: {
793
777
  if (showdown and move.effect.isMulti()) {
794
778
  Effects.multiHit(battle, player, move);
795
779
  hits = side.active.volatiles.attacks;
796
- late = false;
797
780
  }
798
781
 
799
782
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -827,7 +810,7 @@ fn doMove(
827
810
  }
828
811
 
829
812
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
830
- if (showdown and skip) battle.last_damage = 0;
813
+ if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
831
814
 
832
815
  miss = if (showdown or skip)
833
816
  miss
@@ -874,9 +857,15 @@ fn doMove(
874
857
  if (showdown and side.stored().hp == 0) residual.* = false;
875
858
  } else if (move.effect == .Explode) {
876
859
  try Effects.explode(battle, player);
877
- try buildRage(battle, player.foe(), log);
878
- } else if (showdown and move.effect == .Disable) {
879
- try buildRage(battle, player.foe(), log);
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
+ }
880
869
  }
881
870
  return null;
882
871
  }
@@ -889,9 +878,6 @@ fn doMove(
889
878
  hits = side.active.volatiles.attacks;
890
879
  }
891
880
 
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
881
  var nullified = false;
896
882
  var hit: u4 = 0;
897
883
  while (hit < hits) {
@@ -907,7 +893,9 @@ fn doMove(
907
893
  if (hit == 0 and ohko) try log.ohko();
908
894
  hit += 1;
909
895
  if (foe.stored().hp == 0) break;
910
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
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
899
  // If the substitute breaks during a multi-hit attack, the attack ends
912
900
  if (nullified) break;
913
901
  }
@@ -944,9 +932,6 @@ fn doMove(
944
932
 
945
933
  if (foe.stored().hp == 0) return null;
946
934
 
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
-
950
935
  if (!move.effect.isSpecial()) {
951
936
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
952
937
  // second time here (after being executed in the "always happens" block above) but that
@@ -958,9 +943,7 @@ fn doMove(
958
943
  if (move.effect == .Twineedle) {
959
944
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
960
945
  } else if (move.effect == .Disable) {
961
- const result = try Effects.disable(battle, player, move, log);
962
- if (showdown) try buildRage(battle, player.foe(), log);
963
- return result;
946
+ return Effects.disable(battle, player, move, log);
964
947
  } else {
965
948
  try moveEffect(battle, player, move, log);
966
949
  }
@@ -978,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
978
961
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
979
962
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
980
963
 
981
- return Rolls.criticalHit(battle, chance);
964
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
982
966
  }
983
967
 
984
968
  fn calcDamage(
@@ -1082,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1082
1066
 
1083
1067
  fn randomizeDamage(battle: anytype) void {
1084
1068
  if (battle.last_damage <= 1) return;
1085
- const random = Rolls.damage(battle);
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
1079
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1087
1080
  }
1088
1081
 
1089
1082
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1090
1083
  const side = battle.side(player);
1091
- const foe = battle.foe(player);
1092
1084
 
1093
1085
  if (!try checkHit(battle, player, move, log)) return null;
1094
1086
 
1095
1087
  battle.last_damage = switch (side.last_selected_move) {
1096
- .SuperFang => @max(foe.stored().hp / 2, 1),
1088
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1097
1089
  .SeismicToss, .NightShade => side.stored().level,
1098
1090
  .SonicBoom => 20,
1099
1091
  .DragonRage => 40,
1100
1092
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1101
1093
  .Psywave => power: {
1102
1094
  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);
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
+ }
1106
1105
  },
1107
1106
  else => unreachable,
1108
1107
  };
1109
1108
 
1110
1109
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1111
1110
 
1112
- const sub = showdown and foe.active.volatiles.Substitute;
1113
1111
  _ = 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
-
1116
1112
  return null;
1117
1113
  }
1118
1114
 
@@ -1220,7 +1216,7 @@ fn mirrorMove(
1220
1216
 
1221
1217
  incrementPP(side, mslot);
1222
1218
 
1223
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1219
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1224
1220
  return doMove(battle, player, mslot, auto, residual, log);
1225
1221
  }
1226
1222
 
@@ -1234,10 +1230,22 @@ fn metronome(
1234
1230
  ) !?Result {
1235
1231
  var side = battle.side(player);
1236
1232
 
1237
- side.last_selected_move = Rolls.metronome(battle);
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
1246
  incrementPP(side, mslot);
1239
1247
 
1240
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1248
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1241
1249
  return doMove(battle, player, mslot, auto, residual, log);
1242
1250
  }
1243
1251
 
@@ -1270,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1270
1278
  if (move.effect == .Swift) return true;
1271
1279
  if (foe.active.volatiles.Invulnerable) break :miss true;
1272
1280
  }
1273
- if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1274
- (showdown and foe.active.volatiles.Substitute)))
1275
- {
1281
+ if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1276
1282
  immune.* = true;
1277
1283
  if (showdown) return false;
1278
1284
  break :miss true;
@@ -1292,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1292
1298
  }
1293
1299
 
1294
1300
  // 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));
1301
+ var state = &side.active.volatiles.state;
1302
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1303
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1304
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1301
1305
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1302
1306
  accuracy = accuracy * boost[0] / boost[1];
1303
1307
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1304
1308
  accuracy = accuracy * boost[0] / boost[1];
1305
1309
  accuracy = @min(255, @max(1, accuracy));
1306
1310
 
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;
1311
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1310
1312
 
1311
1313
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1312
- break :miss !Rolls.hit(battle, accuracy);
1314
+ break :miss if (showdown)
1315
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1316
+ else
1317
+ battle.rng.next() >= accuracy;
1313
1318
  };
1314
1319
 
1315
1320
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1424,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1424
1429
  }
1425
1430
 
1426
1431
  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
-
1436
1432
  var damage = @max(stored.stats.hp / 16, 1);
1437
1433
 
1438
1434
  // GLITCH: Leech Seed + Toxic glitch
@@ -1444,12 +1440,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1444
1440
  const amount = @min(damage, stored.hp);
1445
1441
  stored.hp -= amount;
1446
1442
 
1443
+ var foe = battle.foe(player);
1444
+ var foe_stored = foe.stored();
1445
+ const foe_ident = battle.active(player.foe());
1446
+
1447
1447
  // 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
- }
1448
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1453
1449
 
1454
1450
  const before = foe_stored.hp;
1455
1451
  // Uncapped damage is added back to the foe
@@ -1484,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1484
1480
 
1485
1481
  fn checkEBC(battle: anytype) bool {
1486
1482
  for (battle.sides, 0..) |side, i| {
1487
- const foe = battle.sides[~@intCast(u1, i)];
1488
-
1489
1483
  var foe_all_ghosts = true;
1490
1484
  var foe_all_transform = true;
1491
- for (foe.order, 0..) |id, j| {
1492
- if (id == 0) break;
1493
- const active = j == 0;
1494
- const pokemon = foe.pokemon[id - 1];
1495
1485
 
1496
- const ghost = pokemon.hp == 0 or
1497
- (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1486
+ for (battle.sides[~@intCast(u1, i)].pokemon) |pokemon| {
1487
+ if (pokemon.species == .None) continue;
1488
+
1489
+ const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
1498
1490
  foe_all_ghosts = foe_all_ghosts and ghost;
1499
1491
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1500
- for (if (active) foe.active.moves else pokemon.moves) |m| {
1492
+ for (pokemon.moves) |m| {
1501
1493
  if (m.id == .None) break :transform true;
1502
1494
  if (m.id != .Transform) break :transform false;
1503
1495
  }
@@ -1505,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
1505
1497
  };
1506
1498
  }
1507
1499
 
1508
- for (side.order, 0..) |id, j| {
1509
- if (id == 0) break;
1510
- const active = j == 0;
1511
- const pokemon = side.pokemon[id - 1];
1512
-
1500
+ for (side.pokemon) |pokemon| {
1513
1501
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1514
1502
  const transform = foe_all_transform and transform: {
1515
- for (if (active) side.active.moves else pokemon.moves) |m| {
1503
+ for (pokemon.moves) |m| {
1516
1504
  if (m.id == .None) break :transform true;
1517
1505
  if (m.id != .Transform) break :transform false;
1518
1506
  }
@@ -1520,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
1520
1508
  };
1521
1509
  if (transform) continue;
1522
1510
  const no_pp = foe_all_ghosts and no_pp: {
1523
- for (if (active) side.active.moves else pokemon.moves) |m| {
1511
+ for (pokemon.moves) |m| {
1524
1512
  if (m.pp != 0) break :no_pp false;
1525
1513
  }
1526
1514
  break :no_pp true;
@@ -1536,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
1536
1524
  return false;
1537
1525
  }
1538
1526
 
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
- inline fn onBegin(
1576
- battle: anytype,
1577
- player: Player,
1578
- move: Move.Data,
1579
- mslot: u8,
1580
- residual: *bool,
1581
- log: anytype,
1582
- ) !void {
1527
+ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1583
1528
  assert(move.effect.onBegin());
1584
1529
  return switch (move.effect) {
1585
1530
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1595,7 +1540,7 @@ inline fn onBegin(
1595
1540
  .Poison => Effects.poison(battle, player, move, log),
1596
1541
  .Reflect => Effects.reflect(battle, player, log),
1597
1542
  .Splash => Effects.splash(battle, player, log),
1598
- .Substitute => Effects.substitute(battle, player, residual, log),
1543
+ .Substitute => Effects.substitute(battle, player, log),
1599
1544
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1600
1545
  .Transform => Effects.transform(battle, player, log),
1601
1546
  else => unreachable,
@@ -1662,7 +1607,10 @@ pub const Effects = struct {
1662
1607
  side.active.volatiles.Bide = true;
1663
1608
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1664
1609
  side.active.volatiles.state = 0;
1665
- side.active.volatiles.attacks = Rolls.duration(battle);
1610
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1611
+ battle.rng.range(u4, 2, 4)
1612
+ else
1613
+ (battle.rng.next() & 1) + 2);
1666
1614
 
1667
1615
  try log.start(battle.active(player), .Bide);
1668
1616
  }
@@ -1684,8 +1632,14 @@ pub const Effects = struct {
1684
1632
  return;
1685
1633
  }
1686
1634
 
1687
- if (foe.active.types.includes(move.type)) return;
1688
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1635
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1636
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1637
+ else
1638
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1639
+ Gen12.percent(10)
1640
+ else
1641
+ Gen12.percent(30));
1642
+ if (!chance) return;
1689
1643
 
1690
1644
  foe_stored.status = Status.init(.BRN);
1691
1645
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1709,7 +1663,11 @@ pub const Effects = struct {
1709
1663
  const sub = foe.active.volatiles.Substitute;
1710
1664
 
1711
1665
  if (move.effect == .ConfusionChance) {
1712
- if (!Rolls.confusionChance(battle)) return;
1666
+ const chance = if (showdown)
1667
+ battle.rng.chance(u8, 25, 256)
1668
+ else
1669
+ battle.rng.next() < Gen12.percent(10);
1670
+ if (!chance) return;
1713
1671
  } else {
1714
1672
  if (showdown) {
1715
1673
  if (!try checkHit(battle, player, move, log)) {
@@ -1728,7 +1686,10 @@ pub const Effects = struct {
1728
1686
 
1729
1687
  if (foe.active.volatiles.Confusion) return;
1730
1688
  foe.active.volatiles.Confusion = true;
1731
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1689
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1690
+ battle.rng.range(u8, 2, 6)
1691
+ else
1692
+ (battle.rng.next() & 3) + 2);
1732
1693
 
1733
1694
  try log.start(battle.active(player.foe()), .Confusion);
1734
1695
  }
@@ -1778,8 +1739,11 @@ pub const Effects = struct {
1778
1739
  return Result.Error;
1779
1740
  }
1780
1741
 
1781
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1782
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1742
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1743
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1744
+ battle.rng.range(u8, 1, 9)
1745
+ else
1746
+ (battle.rng.next() & 7) + 1);
1783
1747
 
1784
1748
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1785
1749
  return null;
@@ -1789,11 +1753,6 @@ pub const Effects = struct {
1789
1753
  var side = battle.side(player);
1790
1754
  var stored = side.stored();
1791
1755
 
1792
- if (battle.last_damage == 0) {
1793
- assert(showdown);
1794
- if (!battle.foe(player).active.volatiles.Substitute) return;
1795
- }
1796
-
1797
1756
  const drain = @max(battle.last_damage / 2, 1);
1798
1757
  battle.last_damage = drain;
1799
1758
 
@@ -1818,7 +1777,15 @@ pub const Effects = struct {
1818
1777
  var volatiles = &battle.foe(player).active.volatiles;
1819
1778
 
1820
1779
  if (volatiles.Substitute) return;
1821
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1780
+
1781
+ const chance = if (showdown)
1782
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1783
+ else
1784
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1785
+ Gen12.percent(10)
1786
+ else
1787
+ Gen12.percent(30));
1788
+ if (!chance) return;
1822
1789
 
1823
1790
  volatiles.Flinch = true;
1824
1791
  volatiles.Recharging = false;
@@ -1844,8 +1811,12 @@ pub const Effects = struct {
1844
1811
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1845
1812
  }
1846
1813
 
1847
- if (foe.active.types.includes(move.type)) return;
1848
- if (!Rolls.secondaryChance(battle, true)) return;
1814
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1815
+ battle.rng.chance(u8, 26, 256)
1816
+ else
1817
+ battle.rng.next() < 1 + Gen12.percent(10);
1818
+ if (!chance) return;
1819
+
1849
1820
  // Freeze Clause Mod
1850
1821
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1851
1822
 
@@ -1990,7 +1961,7 @@ pub const Effects = struct {
1990
1961
  }
1991
1962
  if (!try checkHit(battle, player, move, log)) return;
1992
1963
 
1993
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1964
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1994
1965
  side.active.move(oslot).id = foe.active.move(rslot).id;
1995
1966
 
1996
1967
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2011,8 +1982,7 @@ pub const Effects = struct {
2011
1982
  assert(!side.active.volatiles.MultiHit);
2012
1983
  side.active.volatiles.MultiHit = true;
2013
1984
 
2014
- side.active.volatiles.attacks =
2015
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1985
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2016
1986
  }
2017
1987
 
2018
1988
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2056,8 +2026,14 @@ pub const Effects = struct {
2056
2026
  }
2057
2027
 
2058
2028
  // 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;
2029
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2030
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2031
+ else
2032
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2033
+ Gen12.percent(10)
2034
+ else
2035
+ Gen12.percent(30));
2036
+ if (!chance) return;
2061
2037
 
2062
2038
  foe_stored.status = Status.init(.PAR);
2063
2039
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2087,7 +2063,7 @@ pub const Effects = struct {
2087
2063
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2088
2064
  if (!showdown)
2089
2065
  .Poison
2090
- else if (toxic == (foe_stored.status == Status.TOX))
2066
+ else if (toxic == foe.active.volatiles.Toxic)
2091
2067
  if (toxic) .Toxic else .Poison
2092
2068
  else
2093
2069
  .None
@@ -2100,7 +2076,16 @@ pub const Effects = struct {
2100
2076
 
2101
2077
  if (move.effect == .Poison) {
2102
2078
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2103
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2079
+ } else {
2080
+ const chance = if (showdown)
2081
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2082
+ else
2083
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2084
+ Gen12.percent(20)
2085
+ else
2086
+ Gen12.percent(40));
2087
+ if (!chance) return;
2088
+ }
2104
2089
 
2105
2090
  foe_stored.status = Status.init(.PSN);
2106
2091
  if (toxic) {
@@ -2114,8 +2099,10 @@ pub const Effects = struct {
2114
2099
 
2115
2100
  fn rage(battle: anytype, player: Player) !void {
2116
2101
  var volatiles = &battle.side(player).active.volatiles;
2117
- assert(!volatiles.Bide);
2102
+ if (volatiles.Rage) return;
2118
2103
  volatiles.Rage = true;
2104
+ assert(!volatiles.Bide);
2105
+ volatiles.state = 0;
2119
2106
  }
2120
2107
 
2121
2108
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2167,7 +2154,16 @@ pub const Effects = struct {
2167
2154
  }
2168
2155
  foe.active.volatiles.Recharging = false;
2169
2156
 
2170
- foe_stored.status = Status.slp(Rolls.sleep(battle));
2157
+ const duration = @intCast(u3, if (showdown)
2158
+ battle.rng.range(u8, 1, 8)
2159
+ else loop: {
2160
+ while (true) {
2161
+ const r = battle.rng.next() & 7;
2162
+ if (r != 0) break :loop r;
2163
+ }
2164
+ });
2165
+
2166
+ foe_stored.status = Status.slp(duration);
2171
2167
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2172
2168
  }
2173
2169
 
@@ -2175,7 +2171,7 @@ pub const Effects = struct {
2175
2171
  try log.activate(battle.active(player), .Splash);
2176
2172
  }
2177
2173
 
2178
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2174
+ fn substitute(battle: anytype, player: Player, log: anytype) !void {
2179
2175
  var side = battle.side(player);
2180
2176
  if (side.active.volatiles.Substitute) {
2181
2177
  try log.fail(battle.active(player), .Substitute);
@@ -2200,18 +2196,14 @@ pub const Effects = struct {
2200
2196
  side.active.volatiles.substitute = hp + 1;
2201
2197
  side.active.volatiles.Substitute = true;
2202
2198
  try log.start(battle.active(player), .Substitute);
2203
- if (hp > 0) {
2204
- try log.damage(battle.active(player), side.stored(), .None);
2205
- if (showdown and side.stored().hp == 0) residual.* = false;
2206
- }
2199
+ if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2207
2200
  }
2208
2201
 
2209
2202
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2210
2203
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2211
2204
 
2212
- // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2205
+ // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2213
2206
  _ = try checkHit(battle, player, move, log);
2214
- battle.last_damage = 0;
2215
2207
  }
2216
2208
 
2217
2209
  fn thrashing(battle: anytype, player: Player) void {
@@ -2220,7 +2212,11 @@ pub const Effects = struct {
2220
2212
  assert(!volatiles.Bide);
2221
2213
 
2222
2214
  volatiles.Thrashing = true;
2223
- volatiles.attacks = Rolls.duration(battle);
2215
+ volatiles.state = 0;
2216
+ volatiles.attacks = @intCast(u3, if (showdown)
2217
+ battle.rng.range(u8, 2, 4)
2218
+ else
2219
+ (battle.rng.next() & 1) + 2);
2224
2220
  }
2225
2221
 
2226
2222
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2262,7 +2258,7 @@ pub const Effects = struct {
2262
2258
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2263
2259
  if (!showdown) foe.active.volatiles.Recharging = false;
2264
2260
 
2265
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2261
+ side.active.volatiles.attacks = distribution(battle) - 1;
2266
2262
  }
2267
2263
 
2268
2264
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2289,11 +2285,9 @@ pub const Effects = struct {
2289
2285
  return try log.fail(ident, .None);
2290
2286
  }
2291
2287
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2292
- const stat = unmodifiedStats(battle, side).atk;
2288
+ const stat = unmodifiedStats(battle, player).atk;
2293
2289
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2294
2290
  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
2291
  },
2298
2292
  .DefenseUp1, .DefenseUp2 => {
2299
2293
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2309,7 +2303,7 @@ pub const Effects = struct {
2309
2303
  return try log.fail(ident, .None);
2310
2304
  }
2311
2305
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2312
- const stat = unmodifiedStats(battle, side).def;
2306
+ const stat = unmodifiedStats(battle, player).def;
2313
2307
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2314
2308
  try log.boost(ident, .Defense, n);
2315
2309
  },
@@ -2326,7 +2320,7 @@ pub const Effects = struct {
2326
2320
  return try log.fail(ident, .None);
2327
2321
  }
2328
2322
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2329
- const stat = unmodifiedStats(battle, side).spe;
2323
+ const stat = unmodifiedStats(battle, player).spe;
2330
2324
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2331
2325
  try log.boost(ident, .Speed, 2);
2332
2326
  },
@@ -2346,7 +2340,7 @@ pub const Effects = struct {
2346
2340
  return try log.fail(ident, .None);
2347
2341
  }
2348
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2349
- const stat = unmodifiedStats(battle, side).spc;
2343
+ const stat = unmodifiedStats(battle, player).spc;
2350
2344
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2351
2345
  try log.boost(ident, .SpecialAttack, n);
2352
2346
  try log.boost(ident, .SpecialDefense, n);
@@ -2373,7 +2367,11 @@ pub const Effects = struct {
2373
2367
  }
2374
2368
 
2375
2369
  if (move.effect.isStatDownChance()) {
2376
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2370
+ const chance = if (showdown)
2371
+ battle.rng.chance(u8, 85, 256)
2372
+ else
2373
+ battle.rng.next() < Gen12.percent(33) + 1;
2374
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2377
2375
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2378
2376
  return; // checkHit already checks for Invulnerable
2379
2377
  }
@@ -2395,7 +2393,7 @@ pub const Effects = struct {
2395
2393
  return try log.fail(foe_ident, .None);
2396
2394
  }
2397
2395
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2398
- const stat = unmodifiedStats(battle, foe).atk;
2396
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2399
2397
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2400
2398
  try log.boost(foe_ident, .Attack, -1);
2401
2399
  },
@@ -2413,7 +2411,7 @@ pub const Effects = struct {
2413
2411
  return try log.fail(foe_ident, .None);
2414
2412
  }
2415
2413
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2416
- const stat = unmodifiedStats(battle, foe).def;
2414
+ const stat = unmodifiedStats(battle, player.foe()).def;
2417
2415
  stats.def = @max(1, stat * mod[0] / mod[1]);
2418
2416
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2419
2417
  },
@@ -2430,7 +2428,7 @@ pub const Effects = struct {
2430
2428
  return try log.fail(foe_ident, .None);
2431
2429
  }
2432
2430
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2433
- const stat = unmodifiedStats(battle, foe).spe;
2431
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2434
2432
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2435
2433
  try log.boost(foe_ident, .Speed, -1);
2436
2434
  assert(boosts.spe >= -6);
@@ -2450,7 +2448,7 @@ pub const Effects = struct {
2450
2448
  return try log.fail(foe_ident, .None);
2451
2449
  }
2452
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2453
- const stat = unmodifiedStats(battle, foe).spc;
2451
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2454
2452
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2455
2453
  try log.boost(foe_ident, .SpecialAttack, -1);
2456
2454
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2469,7 +2467,8 @@ pub const Effects = struct {
2469
2467
  }
2470
2468
  };
2471
2469
 
2472
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2470
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
+ const side = battle.side(who);
2473
2472
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2474
2473
  const id = ID.from(side.active.volatiles.transform);
2475
2474
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2483,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2483
2482
  }
2484
2483
  }
2485
2484
 
2486
- inline fn isForced(active: ActivePokemon) bool {
2485
+ fn isForced(active: ActivePokemon) bool {
2487
2486
  return active.volatiles.Recharging or active.volatiles.Rage or
2488
2487
  active.volatiles.Thrashing or active.volatiles.Charging;
2489
2488
  }
@@ -2515,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2515
2514
  volatiles.LeechSeed = false;
2516
2515
  try log.end(ident, .LeechSeed);
2517
2516
  }
2518
- if (!showdown and volatiles.Toxic) {
2517
+ if (volatiles.Toxic) {
2519
2518
  volatiles.Toxic = false;
2520
- // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2519
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
2520
+ if (showdown) {
2521
+ volatiles.toxic = 0;
2522
+ try log.end(ident, .Toxic);
2523
+ }
2521
2524
  }
2522
2525
  if (volatiles.LightScreen) {
2523
2526
  volatiles.LightScreen = false;
@@ -2527,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2527
2530
  volatiles.Reflect = false;
2528
2531
  try log.end(ident, .Reflect);
2529
2532
  }
2530
- if (showdown and volatiles.Toxic) {
2531
- volatiles.Toxic = false;
2532
- // Pokémon Showdown erroneously clears the toxic counter
2533
- volatiles.toxic = 0;
2534
- try log.end(ident, .Toxic);
2535
- }
2536
2533
  }
2537
2534
 
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;
2535
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2536
+ if (side.active.volatiles.disabled_move != 0) {
2537
+ // A Pokémon that transforms after being disabled may end up with less move slots
2538
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2539
+ if (m != .None and m == side.last_selected_move) {
2540
+ side.active.volatiles.Charging = false;
2541
+ try log.disabled(ident, side.last_selected_move);
2542
+ return true;
2554
2543
  }
2555
2544
  }
2545
+ return false;
2546
+ }
2556
2547
 
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);
2575
- }
2576
-
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
- }
2548
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
+ var volatiles = &active.volatiles;
2550
+ assert(volatiles.Thrashing);
2551
+ if (volatiles.attacks > 0) return false;
2581
2552
 
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
- }
2553
+ volatiles.Thrashing = false;
2554
+ volatiles.Confusion = true;
2555
+ volatiles.confusion = @intCast(u3, if (showdown)
2556
+ battle.rng.range(u8, 2, 6)
2557
+ else
2558
+ (battle.rng.next() & 3) + 2);
2586
2559
 
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
- }
2560
+ return true;
2561
+ }
2591
2562
 
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
- }
2563
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2599
2564
 
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
- }
2565
+ fn distribution(battle: anytype) u3 {
2566
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2567
+ const r = (battle.rng.next() & 3);
2568
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2569
+ }
2604
2570
 
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
- }
2571
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2572
+ if (showdown) {
2573
+ if (check_pp == 0) {
2574
+ var i: usize = moves.len;
2575
+ while (i > 0) {
2576
+ i -= 1;
2577
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2578
+ }
2579
+ } else {
2580
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2581
+ var i: usize = 0;
2582
+ while (i < moves.len and r > 0) : (i += 1) {
2583
+ if (moves[i].pp > 0) {
2584
+ r -= 1;
2585
+ if (r == 0) break;
2623
2586
  }
2624
- return @intCast(u4, i + 1);
2625
2587
  }
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;
2588
+ return @intCast(u4, i + 1);
2631
2589
  }
2632
2590
  }
2633
2591
 
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);
2592
+ while (true) {
2593
+ const r = @intCast(u4, rand.next() & 3);
2594
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2637
2595
  }
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
- };
2596
+ }
2672
2597
 
2673
2598
  test "RNG agreement" {
2674
2599
  if (!showdown) return;
@@ -2768,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2768
2693
  while (slot <= 4) : (slot += 1) {
2769
2694
  const m = active.moves[slot - 1];
2770
2695
  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;
2696
+ if (m.id == side.last_selected_move) {
2697
+ // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2698
+ const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2776
2699
  out[n] = .{ .type = .Move, .data = s };
2777
2700
  n += 1;
2778
2701
  return n;