@pkmn/engine 0.1.0-dev.7cd30bd9 → 0.1.0-dev.7f37a47c

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
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
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;
@@ -512,10 +504,9 @@ fn beforeMove(
512
504
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
513
505
  if (showdown and try disabled(side, ident, log)) return .done;
514
506
 
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
507
+ // Pokémon Showdown checks for Flinch *after* instead of before
518
508
  if (showdown and volatiles.Flinch) {
509
+ volatiles.Flinch = false;
519
510
  try log.cant(ident, .Flinch);
520
511
  return .done;
521
512
  }
@@ -537,9 +528,14 @@ fn beforeMove(
537
528
  } else {
538
529
  try log.activate(ident, .Confusion);
539
530
 
540
- 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) {
541
537
  assert(!volatiles.MultiHit);
542
- if (!volatiles.Rage) volatiles.state = 0;
538
+ volatiles.state = 0;
543
539
  volatiles.Bide = false;
544
540
  volatiles.Thrashing = false;
545
541
  volatiles.MultiHit = false;
@@ -563,12 +559,8 @@ fn beforeMove(
563
559
  player
564
560
  else
565
561
  player.foe();
566
-
567
- const uncapped = battle.last_damage;
568
562
  // Skipping adjustDamage / randomizeDamage / checkHit
569
563
  _ = try applyDamage(battle, player, target, .Confusion, log);
570
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
571
- if (showdown) battle.last_damage = uncapped;
572
564
 
573
565
  return .done;
574
566
  }
@@ -577,15 +569,22 @@ fn beforeMove(
577
569
 
578
570
  if (!showdown and try disabled(side, ident, log)) return .done;
579
571
 
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;
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
+ }
589
588
  }
590
589
 
591
590
  if (volatiles.Bide) {
@@ -612,18 +611,7 @@ fn beforeMove(
612
611
  return .done;
613
612
  }
614
613
 
615
- const sub = showdown and foe.active.volatiles.Substitute;
616
614
  _ = 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
-
627
615
  return .done;
628
616
  }
629
617
 
@@ -666,7 +654,6 @@ fn canMove(
666
654
  auto: bool,
667
655
  skip_pp: bool,
668
656
  from: ?Move,
669
- residual: *bool,
670
657
  log: anytype,
671
658
  ) !bool {
672
659
  var side = battle.side(player);
@@ -701,7 +688,7 @@ fn canMove(
701
688
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
702
689
 
703
690
  if (move.effect.onBegin()) {
704
- try onBegin(battle, player, move, mslot, residual, log);
691
+ try onBegin(battle, player, move, mslot, log);
705
692
  return false;
706
693
  }
707
694
 
@@ -780,21 +767,16 @@ fn doMove(
780
767
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
781
768
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
782
769
  if (immune and move.effect != .Binding) break :miss true;
783
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
784
- battle.last_damage = 0;
785
- break :miss true;
786
- }
770
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
787
771
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
788
772
  };
789
773
  assert(!immune or miss or (showdown and move.effect == .Binding));
790
774
 
791
- var late = showdown and move.effect != .Explode;
792
775
  const skip = status or immune;
793
776
  if ((!showdown or (!skip or counter)) and !miss) blk: {
794
777
  if (showdown and move.effect.isMulti()) {
795
778
  Effects.multiHit(battle, player, move);
796
779
  hits = side.active.volatiles.attacks;
797
- late = false;
798
780
  }
799
781
 
800
782
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -828,7 +810,7 @@ fn doMove(
828
810
  }
829
811
 
830
812
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
831
- if (showdown and skip) battle.last_damage = 0;
813
+ if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
832
814
 
833
815
  miss = if (showdown or skip)
834
816
  miss
@@ -875,9 +857,15 @@ fn doMove(
875
857
  if (showdown and side.stored().hp == 0) residual.* = false;
876
858
  } else if (move.effect == .Explode) {
877
859
  try Effects.explode(battle, player);
878
- try buildRage(battle, player.foe(), log);
879
- } else if (showdown and move.effect == .Disable) {
880
- 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
+ }
881
869
  }
882
870
  return null;
883
871
  }
@@ -890,9 +878,6 @@ fn doMove(
890
878
  hits = side.active.volatiles.attacks;
891
879
  }
892
880
 
893
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
894
- const sub = showdown and foe.active.volatiles.Substitute;
895
-
896
881
  var nullified = false;
897
882
  var hit: u4 = 0;
898
883
  while (hit < hits) {
@@ -908,7 +893,9 @@ fn doMove(
908
893
  if (hit == 0 and ohko) try log.ohko();
909
894
  hit += 1;
910
895
  if (foe.stored().hp == 0) break;
911
- 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
+ }
912
899
  // If the substitute breaks during a multi-hit attack, the attack ends
913
900
  if (nullified) break;
914
901
  }
@@ -945,9 +932,6 @@ fn doMove(
945
932
 
946
933
  if (foe.stored().hp == 0) return null;
947
934
 
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
-
951
935
  if (!move.effect.isSpecial()) {
952
936
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
953
937
  // second time here (after being executed in the "always happens" block above) but that
@@ -959,9 +943,7 @@ fn doMove(
959
943
  if (move.effect == .Twineedle) {
960
944
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
961
945
  } else if (move.effect == .Disable) {
962
- const result = try Effects.disable(battle, player, move, log);
963
- if (showdown) try buildRage(battle, player.foe(), log);
964
- return result;
946
+ return Effects.disable(battle, player, move, log);
965
947
  } else {
966
948
  try moveEffect(battle, player, move, log);
967
949
  }
@@ -979,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
979
961
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
980
962
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
981
963
 
982
- 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;
983
966
  }
984
967
 
985
968
  fn calcDamage(
@@ -1083,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1083
1066
 
1084
1067
  fn randomizeDamage(battle: anytype) void {
1085
1068
  if (battle.last_damage <= 1) return;
1086
- 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
+
1087
1079
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1088
1080
  }
1089
1081
 
1090
1082
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1091
1083
  const side = battle.side(player);
1092
- const foe = battle.foe(player);
1093
1084
 
1094
1085
  if (!try checkHit(battle, player, move, log)) return null;
1095
1086
 
1096
1087
  battle.last_damage = switch (side.last_selected_move) {
1097
- .SuperFang => @max(foe.stored().hp / 2, 1),
1088
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1098
1089
  .SeismicToss, .NightShade => side.stored().level,
1099
1090
  .SonicBoom => 20,
1100
1091
  .DragonRage => 40,
1101
1092
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1102
1093
  .Psywave => power: {
1103
1094
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1104
- // GLITCH: Psywave infinite glitch loop
1105
- if (!showdown and max <= 1) return Result.Error;
1106
- break :power Rolls.psywave(battle, max);
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
+ }
1107
1105
  },
1108
1106
  else => unreachable,
1109
1107
  };
1110
1108
 
1111
1109
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1112
1110
 
1113
- const sub = showdown and foe.active.volatiles.Substitute;
1114
1111
  _ = 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
-
1117
1112
  return null;
1118
1113
  }
1119
1114
 
@@ -1221,7 +1216,7 @@ fn mirrorMove(
1221
1216
 
1222
1217
  incrementPP(side, mslot);
1223
1218
 
1224
- 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;
1225
1220
  return doMove(battle, player, mslot, auto, residual, log);
1226
1221
  }
1227
1222
 
@@ -1235,10 +1230,22 @@ fn metronome(
1235
1230
  ) !?Result {
1236
1231
  var side = battle.side(player);
1237
1232
 
1238
- 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
+
1239
1246
  incrementPP(side, mslot);
1240
1247
 
1241
- 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;
1242
1249
  return doMove(battle, player, mslot, auto, residual, log);
1243
1250
  }
1244
1251
 
@@ -1271,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1271
1278
  if (move.effect == .Swift) return true;
1272
1279
  if (foe.active.volatiles.Invulnerable) break :miss true;
1273
1280
  }
1274
- if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1275
- (showdown and foe.active.volatiles.Substitute)))
1276
- {
1281
+ if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1277
1282
  immune.* = true;
1278
1283
  if (showdown) return false;
1279
1284
  break :miss true;
@@ -1293,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1293
1298
  }
1294
1299
 
1295
1300
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1296
- const state = side.active.volatiles.state;
1297
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1298
- const overwritten = overwrite and state > 0;
1299
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1300
-
1301
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
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));
1302
1305
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1303
1306
  accuracy = accuracy * boost[0] / boost[1];
1304
1307
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1305
1308
  accuracy = accuracy * boost[0] / boost[1];
1306
1309
  accuracy = @min(255, @max(1, accuracy));
1307
1310
 
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;
1311
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1311
1312
 
1312
1313
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1313
- 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;
1314
1318
  };
1315
1319
 
1316
1320
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1425
1429
  }
1426
1430
 
1427
1431
  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
-
1437
1432
  var damage = @max(stored.stats.hp / 16, 1);
1438
1433
 
1439
1434
  // GLITCH: Leech Seed + Toxic glitch
@@ -1445,12 +1440,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1445
1440
  const amount = @min(damage, stored.hp);
1446
1441
  stored.hp -= amount;
1447
1442
 
1443
+ var foe = battle.foe(player);
1444
+ var foe_stored = foe.stored();
1445
+ const foe_ident = battle.active(player.foe());
1446
+
1448
1447
  // As above, Pokémon Showdown uses damageOf but its not relevant
1449
- if (amount > 0) {
1450
- try log.damage(ident, stored, .LeechSeed);
1451
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1452
- if (showdown) battle.last_damage = damage;
1453
- }
1448
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1454
1449
 
1455
1450
  const before = foe_stored.hp;
1456
1451
  // Uncapped damage is added back to the foe
@@ -1485,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1485
1480
 
1486
1481
  fn checkEBC(battle: anytype) bool {
1487
1482
  for (battle.sides, 0..) |side, i| {
1488
- const foe = battle.sides[~@intCast(u1, i)];
1489
-
1490
1483
  var foe_all_ghosts = true;
1491
1484
  var foe_all_transform = true;
1492
- for (foe.order, 0..) |id, j| {
1493
- if (id == 0) break;
1494
- const active = j == 0;
1495
- const pokemon = foe.pokemon[id - 1];
1496
1485
 
1497
- const ghost = pokemon.hp == 0 or
1498
- (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);
1499
1490
  foe_all_ghosts = foe_all_ghosts and ghost;
1500
1491
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1501
- for (if (active) foe.active.moves else pokemon.moves) |m| {
1492
+ for (pokemon.moves) |m| {
1502
1493
  if (m.id == .None) break :transform true;
1503
1494
  if (m.id != .Transform) break :transform false;
1504
1495
  }
@@ -1506,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
1506
1497
  };
1507
1498
  }
1508
1499
 
1509
- for (side.order, 0..) |id, j| {
1510
- if (id == 0) break;
1511
- const active = j == 0;
1512
- const pokemon = side.pokemon[id - 1];
1513
-
1500
+ for (side.pokemon) |pokemon| {
1514
1501
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1515
1502
  const transform = foe_all_transform and transform: {
1516
- for (if (active) side.active.moves else pokemon.moves) |m| {
1503
+ for (pokemon.moves) |m| {
1517
1504
  if (m.id == .None) break :transform true;
1518
1505
  if (m.id != .Transform) break :transform false;
1519
1506
  }
@@ -1521,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
1521
1508
  };
1522
1509
  if (transform) continue;
1523
1510
  const no_pp = foe_all_ghosts and no_pp: {
1524
- for (if (active) side.active.moves else pokemon.moves) |m| {
1511
+ for (pokemon.moves) |m| {
1525
1512
  if (m.pp != 0) break :no_pp false;
1526
1513
  }
1527
1514
  break :no_pp true;
@@ -1537,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
1537
1524
  return false;
1538
1525
  }
1539
1526
 
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 {
1527
+ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1584
1528
  assert(move.effect.onBegin());
1585
1529
  return switch (move.effect) {
1586
1530
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1596,7 +1540,7 @@ inline fn onBegin(
1596
1540
  .Poison => Effects.poison(battle, player, move, log),
1597
1541
  .Reflect => Effects.reflect(battle, player, log),
1598
1542
  .Splash => Effects.splash(battle, player, log),
1599
- .Substitute => Effects.substitute(battle, player, residual, log),
1543
+ .Substitute => Effects.substitute(battle, player, log),
1600
1544
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1601
1545
  .Transform => Effects.transform(battle, player, log),
1602
1546
  else => unreachable,
@@ -1663,7 +1607,10 @@ pub const Effects = struct {
1663
1607
  side.active.volatiles.Bide = true;
1664
1608
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1665
1609
  side.active.volatiles.state = 0;
1666
- side.active.volatiles.attacks = Rolls.bideThrashDuration(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);
1667
1614
 
1668
1615
  try log.start(battle.active(player), .Bide);
1669
1616
  }
@@ -1685,8 +1632,14 @@ pub const Effects = struct {
1685
1632
  return;
1686
1633
  }
1687
1634
 
1688
- if (foe.active.types.includes(move.type)) return;
1689
- 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;
1690
1643
 
1691
1644
  foe_stored.status = Status.init(.BRN);
1692
1645
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1710,7 +1663,11 @@ pub const Effects = struct {
1710
1663
  const sub = foe.active.volatiles.Substitute;
1711
1664
 
1712
1665
  if (move.effect == .ConfusionChance) {
1713
- 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;
1714
1671
  } else {
1715
1672
  if (showdown) {
1716
1673
  if (!try checkHit(battle, player, move, log)) {
@@ -1729,7 +1686,10 @@ pub const Effects = struct {
1729
1686
 
1730
1687
  if (foe.active.volatiles.Confusion) return;
1731
1688
  foe.active.volatiles.Confusion = true;
1732
- 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);
1733
1693
 
1734
1694
  try log.start(battle.active(player.foe()), .Confusion);
1735
1695
  }
@@ -1779,8 +1739,11 @@ pub const Effects = struct {
1779
1739
  return Result.Error;
1780
1740
  }
1781
1741
 
1782
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1783
- 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);
1784
1747
 
1785
1748
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1786
1749
  return null;
@@ -1790,11 +1753,6 @@ pub const Effects = struct {
1790
1753
  var side = battle.side(player);
1791
1754
  var stored = side.stored();
1792
1755
 
1793
- if (battle.last_damage == 0) {
1794
- assert(showdown);
1795
- if (!battle.foe(player).active.volatiles.Substitute) return;
1796
- }
1797
-
1798
1756
  const drain = @max(battle.last_damage / 2, 1);
1799
1757
  battle.last_damage = drain;
1800
1758
 
@@ -1819,7 +1777,15 @@ pub const Effects = struct {
1819
1777
  var volatiles = &battle.foe(player).active.volatiles;
1820
1778
 
1821
1779
  if (volatiles.Substitute) return;
1822
- 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;
1823
1789
 
1824
1790
  volatiles.Flinch = true;
1825
1791
  volatiles.Recharging = false;
@@ -1845,8 +1811,12 @@ pub const Effects = struct {
1845
1811
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1846
1812
  }
1847
1813
 
1848
- if (foe.active.types.includes(move.type)) return;
1849
- 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
+
1850
1820
  // Freeze Clause Mod
1851
1821
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1852
1822
 
@@ -1991,7 +1961,7 @@ pub const Effects = struct {
1991
1961
  }
1992
1962
  if (!try checkHit(battle, player, move, log)) return;
1993
1963
 
1994
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1964
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
1995
1965
  side.active.move(oslot).id = foe.active.move(rslot).id;
1996
1966
 
1997
1967
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2012,8 +1982,7 @@ pub const Effects = struct {
2012
1982
  assert(!side.active.volatiles.MultiHit);
2013
1983
  side.active.volatiles.MultiHit = true;
2014
1984
 
2015
- side.active.volatiles.attacks =
2016
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1985
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2017
1986
  }
2018
1987
 
2019
1988
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2057,8 +2026,14 @@ pub const Effects = struct {
2057
2026
  }
2058
2027
 
2059
2028
  // Body Slam can't paralyze a Normal type Pokémon
2060
- if (foe.active.types.includes(move.type)) return;
2061
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
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;
2062
2037
 
2063
2038
  foe_stored.status = Status.init(.PAR);
2064
2039
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2088,7 +2063,7 @@ pub const Effects = struct {
2088
2063
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2089
2064
  if (!showdown)
2090
2065
  .Poison
2091
- else if (toxic == (foe_stored.status == Status.TOX))
2066
+ else if (toxic == foe.active.volatiles.Toxic)
2092
2067
  if (toxic) .Toxic else .Poison
2093
2068
  else
2094
2069
  .None
@@ -2101,7 +2076,16 @@ pub const Effects = struct {
2101
2076
 
2102
2077
  if (move.effect == .Poison) {
2103
2078
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2104
- } 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
+ }
2105
2089
 
2106
2090
  foe_stored.status = Status.init(.PSN);
2107
2091
  if (toxic) {
@@ -2115,8 +2099,10 @@ pub const Effects = struct {
2115
2099
 
2116
2100
  fn rage(battle: anytype, player: Player) !void {
2117
2101
  var volatiles = &battle.side(player).active.volatiles;
2118
- assert(!volatiles.Bide);
2102
+ if (volatiles.Rage) return;
2119
2103
  volatiles.Rage = true;
2104
+ assert(!volatiles.Bide);
2105
+ volatiles.state = 0;
2120
2106
  }
2121
2107
 
2122
2108
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2168,7 +2154,16 @@ pub const Effects = struct {
2168
2154
  }
2169
2155
  foe.active.volatiles.Recharging = false;
2170
2156
 
2171
- foe_stored.status = Status.slp(Rolls.sleepDuration(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);
2172
2167
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2173
2168
  }
2174
2169
 
@@ -2176,7 +2171,7 @@ pub const Effects = struct {
2176
2171
  try log.activate(battle.active(player), .Splash);
2177
2172
  }
2178
2173
 
2179
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2174
+ fn substitute(battle: anytype, player: Player, log: anytype) !void {
2180
2175
  var side = battle.side(player);
2181
2176
  if (side.active.volatiles.Substitute) {
2182
2177
  try log.fail(battle.active(player), .Substitute);
@@ -2201,18 +2196,14 @@ pub const Effects = struct {
2201
2196
  side.active.volatiles.substitute = hp + 1;
2202
2197
  side.active.volatiles.Substitute = true;
2203
2198
  try log.start(battle.active(player), .Substitute);
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
- }
2199
+ if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2208
2200
  }
2209
2201
 
2210
2202
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2211
2203
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2212
2204
 
2213
- // 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
2214
2206
  _ = try checkHit(battle, player, move, log);
2215
- battle.last_damage = 0;
2216
2207
  }
2217
2208
 
2218
2209
  fn thrashing(battle: anytype, player: Player) void {
@@ -2221,7 +2212,11 @@ pub const Effects = struct {
2221
2212
  assert(!volatiles.Bide);
2222
2213
 
2223
2214
  volatiles.Thrashing = true;
2224
- volatiles.attacks = Rolls.bideThrashDuration(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);
2225
2220
  }
2226
2221
 
2227
2222
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2263,7 +2258,7 @@ pub const Effects = struct {
2263
2258
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2264
2259
  if (!showdown) foe.active.volatiles.Recharging = false;
2265
2260
 
2266
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2261
+ side.active.volatiles.attacks = distribution(battle) - 1;
2267
2262
  }
2268
2263
 
2269
2264
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2290,11 +2285,9 @@ pub const Effects = struct {
2290
2285
  return try log.fail(ident, .None);
2291
2286
  }
2292
2287
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2293
- const stat = unmodifiedStats(battle, side).atk;
2288
+ const stat = unmodifiedStats(battle, player).atk;
2294
2289
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2295
2290
  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;
2298
2291
  },
2299
2292
  .DefenseUp1, .DefenseUp2 => {
2300
2293
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2310,7 +2303,7 @@ pub const Effects = struct {
2310
2303
  return try log.fail(ident, .None);
2311
2304
  }
2312
2305
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2313
- const stat = unmodifiedStats(battle, side).def;
2306
+ const stat = unmodifiedStats(battle, player).def;
2314
2307
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2315
2308
  try log.boost(ident, .Defense, n);
2316
2309
  },
@@ -2327,7 +2320,7 @@ pub const Effects = struct {
2327
2320
  return try log.fail(ident, .None);
2328
2321
  }
2329
2322
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2330
- const stat = unmodifiedStats(battle, side).spe;
2323
+ const stat = unmodifiedStats(battle, player).spe;
2331
2324
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2332
2325
  try log.boost(ident, .Speed, 2);
2333
2326
  },
@@ -2347,7 +2340,7 @@ pub const Effects = struct {
2347
2340
  return try log.fail(ident, .None);
2348
2341
  }
2349
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2350
- const stat = unmodifiedStats(battle, side).spc;
2343
+ const stat = unmodifiedStats(battle, player).spc;
2351
2344
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2352
2345
  try log.boost(ident, .SpecialAttack, n);
2353
2346
  try log.boost(ident, .SpecialDefense, n);
@@ -2374,7 +2367,11 @@ pub const Effects = struct {
2374
2367
  }
2375
2368
 
2376
2369
  if (move.effect.isStatDownChance()) {
2377
- 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;
2378
2375
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2379
2376
  return; // checkHit already checks for Invulnerable
2380
2377
  }
@@ -2396,7 +2393,7 @@ pub const Effects = struct {
2396
2393
  return try log.fail(foe_ident, .None);
2397
2394
  }
2398
2395
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2399
- const stat = unmodifiedStats(battle, foe).atk;
2396
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2400
2397
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2401
2398
  try log.boost(foe_ident, .Attack, -1);
2402
2399
  },
@@ -2414,7 +2411,7 @@ pub const Effects = struct {
2414
2411
  return try log.fail(foe_ident, .None);
2415
2412
  }
2416
2413
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2417
- const stat = unmodifiedStats(battle, foe).def;
2414
+ const stat = unmodifiedStats(battle, player.foe()).def;
2418
2415
  stats.def = @max(1, stat * mod[0] / mod[1]);
2419
2416
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2420
2417
  },
@@ -2431,7 +2428,7 @@ pub const Effects = struct {
2431
2428
  return try log.fail(foe_ident, .None);
2432
2429
  }
2433
2430
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2434
- const stat = unmodifiedStats(battle, foe).spe;
2431
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2435
2432
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2436
2433
  try log.boost(foe_ident, .Speed, -1);
2437
2434
  assert(boosts.spe >= -6);
@@ -2451,7 +2448,7 @@ pub const Effects = struct {
2451
2448
  return try log.fail(foe_ident, .None);
2452
2449
  }
2453
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2454
- const stat = unmodifiedStats(battle, foe).spc;
2451
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2455
2452
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2456
2453
  try log.boost(foe_ident, .SpecialAttack, -1);
2457
2454
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2470,7 +2467,8 @@ pub const Effects = struct {
2470
2467
  }
2471
2468
  };
2472
2469
 
2473
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2470
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
+ const side = battle.side(who);
2474
2472
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2475
2473
  const id = ID.from(side.active.volatiles.transform);
2476
2474
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2484,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2484
2482
  }
2485
2483
  }
2486
2484
 
2487
- inline fn isForced(active: ActivePokemon) bool {
2485
+ fn isForced(active: ActivePokemon) bool {
2488
2486
  return active.volatiles.Recharging or active.volatiles.Rage or
2489
2487
  active.volatiles.Thrashing or active.volatiles.Charging;
2490
2488
  }
@@ -2516,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2516
2514
  volatiles.LeechSeed = false;
2517
2515
  try log.end(ident, .LeechSeed);
2518
2516
  }
2519
- if (!showdown and volatiles.Toxic) {
2517
+ if (volatiles.Toxic) {
2520
2518
  volatiles.Toxic = false;
2521
- // 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
+ }
2522
2524
  }
2523
2525
  if (volatiles.LightScreen) {
2524
2526
  volatiles.LightScreen = false;
@@ -2528,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2528
2530
  volatiles.Reflect = false;
2529
2531
  try log.end(ident, .Reflect);
2530
2532
  }
2531
- if (showdown and volatiles.Toxic) {
2532
- volatiles.Toxic = false;
2533
- // Pokémon Showdown erroneously clears the toxic counter
2534
- volatiles.toxic = 0;
2535
- try log.end(ident, .Toxic);
2536
- }
2537
2533
  }
2538
2534
 
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;
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;
2555
2543
  }
2556
2544
  }
2545
+ return false;
2546
+ }
2557
2547
 
2558
- inline fn hit(battle: anytype, accuracy: u16) bool {
2559
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2560
- return battle.rng.next() < accuracy;
2561
- }
2562
-
2563
- inline fn confusionDuration(battle: anytype) u3 {
2564
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2565
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2566
- }
2567
-
2568
- inline fn confused(battle: anytype) bool {
2569
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2570
- return battle.rng.next() >= Gen12.percent(50) + 1;
2571
- }
2572
-
2573
- inline fn paralyzed(battle: anytype) bool {
2574
- if (showdown) return battle.rng.chance(u8, 63, 256);
2575
- return battle.rng.next() < Gen12.percent(25);
2576
- }
2577
-
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
- }
2548
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
+ var volatiles = &active.volatiles;
2550
+ assert(volatiles.Thrashing);
2551
+ if (volatiles.attacks > 0) return false;
2582
2552
 
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
- }
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);
2587
2559
 
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
- }
2560
+ return true;
2561
+ }
2592
2562
 
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
- }
2563
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2600
2564
 
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
- }
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
+ }
2605
2570
 
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
- }
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;
2624
2586
  }
2625
- return @intCast(u4, i + 1);
2626
2587
  }
2627
- }
2628
-
2629
- while (true) {
2630
- const r = @intCast(u4, battle.rng.next() & 3);
2631
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2632
- }
2633
- }
2634
-
2635
- inline fn disableDuration(battle: anytype) u4 {
2636
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2637
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2638
- }
2639
-
2640
- inline fn metronome(battle: anytype) Move {
2641
- if (showdown) {
2642
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2643
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2644
- return @intToEnum(Move, r + mod);
2645
- }
2646
- while (true) {
2647
- const r = battle.rng.next();
2648
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2649
- if (r >= @enumToInt(Move.Struggle)) continue;
2650
- return @intToEnum(Move, r);
2588
+ return @intCast(u4, i + 1);
2651
2589
  }
2652
2590
  }
2653
2591
 
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
- }
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;
2660
2595
  }
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
- };
2596
+ }
2673
2597
 
2674
2598
  test "RNG agreement" {
2675
2599
  if (!showdown) return;
@@ -2769,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2769
2693
  while (slot <= 4) : (slot += 1) {
2770
2694
  const m = active.moves[slot - 1];
2771
2695
  if (m.id == .None) break;
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;
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;
2777
2699
  out[n] = .{ .type = .Move, .data = s };
2778
2700
  n += 1;
2779
2701
  return n;