@pkmn/engine 0.1.0-dev.10889360 → 0.1.0-dev.115ecc94

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.
@@ -166,7 +166,7 @@ fn selectMove(
166
166
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
167
  // set the selected move will not actually be used, it will just be reported as having
168
168
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
169
+ // replicate the incorrect behavior with the correct mechanisms)
170
170
  saveMove(battle, player, choice);
171
171
  } else {
172
172
  assert(choice.data == 0);
@@ -347,10 +347,10 @@ fn doTurn(
347
347
  )) |r| return r;
348
348
  if (!replace) {
349
349
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
350
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
351
  }
352
352
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
353
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
354
  } else if (foe_choice.type == .Pass) return null;
355
355
 
356
356
  options.chance.clearPending();
@@ -369,10 +369,10 @@ fn doTurn(
369
369
  )) |r| return r;
370
370
  if (!replace) {
371
371
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
372
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
373
  }
374
374
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
375
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
376
  }
377
377
 
378
378
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +431,7 @@ fn executeMove(
431
431
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
432
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
433
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // index 1 and the current Pokémon has the same move in its first slot.
434
+ // index 1 and the current Pokémon has the same move in its first slot
435
435
  if (!auto) {
436
436
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
437
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -852,7 +852,7 @@ fn doMove(
852
852
  break :miss true;
853
853
  }
854
854
  break :miss (move.target != .Self and
855
- !moveHit(battle, player, move, &immune, &mist, options));
855
+ !try moveHit(battle, player, move, &immune, &mist, options));
856
856
  };
857
857
  assert(!immune or miss or (showdown and move.effect == .Binding));
858
858
 
@@ -899,7 +899,7 @@ fn doMove(
899
899
  miss = if (showdown or skip)
900
900
  miss
901
901
  else
902
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
902
+ (!try moveHit(battle, player, move, &immune, &mist, options) or zero);
903
903
 
904
904
  assert(showdown or miss or battle.last_damage > 0 or skip);
905
905
  assert((!showdown and miss) or !(ohko and immune));
@@ -930,7 +930,7 @@ fn doMove(
930
930
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
931
931
  try log.fail(.{ foe_ident, .None });
932
932
  } else {
933
- if (showdown or !zero) try options.chance.commit(player, .miss);
933
+ if (!showdown and !zero) try options.chance.commit(player, .miss);
934
934
  try log.lastmiss(.{});
935
935
  try log.miss(.{battle.active(player)});
936
936
  }
@@ -949,13 +949,13 @@ fn doMove(
949
949
  try options.chance.commit(player, .binding);
950
950
  }
951
951
  return null;
952
- } else {
952
+ } else if (!showdown) {
953
953
  try options.chance.commit(player, .hit);
954
954
  }
955
955
 
956
956
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
957
957
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
958
- // it up above before damage calculation).
958
+ // it up above before damage calculation)
959
959
  if (!showdown and move.effect.isMulti()) {
960
960
  try Effects.multiHit(battle, player, move, options);
961
961
  hits = side.active.volatiles.attacks;
@@ -1014,12 +1014,12 @@ fn doMove(
1014
1014
  }
1015
1015
 
1016
1016
  // Substitute being broken nullifies the move's effect completely so even
1017
- // if an effect was intended to "always happen" it will still get skipped.
1017
+ // if an effect was intended to "always happen" it will still get skipped
1018
1018
  if (nullified) return null;
1019
1019
 
1020
1020
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1021
1021
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1022
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1022
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1023
1023
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1024
1024
 
1025
1025
  if (foe.stored().hp == 0) return null;
@@ -1034,7 +1034,7 @@ fn doMove(
1034
1034
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1035
1035
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1036
1036
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1037
- // puts the |-status| message before |-hitcount| instead of after.
1037
+ // puts the |-status| message before |-hitcount| instead of after
1038
1038
  if (move.effect == .Twineedle) {
1039
1039
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1040
1040
  } else if (move.effect == .Disable) {
@@ -1058,7 +1058,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1058
1058
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1059
1059
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1060
1060
 
1061
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1061
+ const spurious =
1062
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1063
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1062
1064
  }
1063
1065
 
1064
1066
  fn calcDamage(
@@ -1173,7 +1175,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1173
1175
  const foe = battle.foe(player);
1174
1176
 
1175
1177
  if (!try checkHit(battle, player, move, options)) return null;
1176
- try options.chance.commit(player, .hit);
1177
1178
 
1178
1179
  battle.last_damage = switch (side.last_selected_move) {
1179
1180
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1234,9 +1235,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1234
1235
 
1235
1236
  battle.last_damage *|= 2;
1236
1237
 
1237
- // Pokémon Showdown calls checkHit before Counter
1238
+ // Pokémon Showdown calls moveHit before Counter
1238
1239
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1239
- try options.chance.commit(player, .hit);
1240
1240
 
1241
1241
  const sub = showdown and foe.active.volatiles.Substitute;
1242
1242
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1265,7 +1265,7 @@ fn applyDamage(
1265
1265
  subbed.active.volatiles.Substitute = false;
1266
1266
  // battle.last_damage is not updated with the amount of HP the Substitute had
1267
1267
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1268
- options.calc.capped(target_player);
1268
+ options.calc.capped(sub_player);
1269
1269
  return true;
1270
1270
  } else {
1271
1271
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1332,7 +1332,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1332
1332
  var immune = false;
1333
1333
  var mist = false;
1334
1334
 
1335
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1335
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1336
+ if (!showdown) try options.chance.commit(player, .hit);
1337
+ return true;
1338
+ }
1336
1339
 
1337
1340
  assert(!immune);
1338
1341
  if (mist) {
@@ -1341,11 +1344,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1341
1344
  try options.log.activate(.{ foe_ident, .Mist });
1342
1345
  try options.log.fail(.{ foe_ident, .None });
1343
1346
  } else {
1344
- try options.chance.commit(player, .miss);
1345
1347
  try options.log.lastmiss(.{});
1346
1348
  try options.log.miss(.{battle.active(player)});
1347
1349
  }
1348
1350
 
1351
+ if (!showdown) try options.chance.commit(player, .miss);
1349
1352
  return false;
1350
1353
  }
1351
1354
 
@@ -1356,7 +1359,7 @@ fn moveHit(
1356
1359
  immune: *bool,
1357
1360
  mist: *bool,
1358
1361
  options: anytype,
1359
- ) bool {
1362
+ ) !bool {
1360
1363
  var side = battle.side(player);
1361
1364
  const foe = battle.foe(player);
1362
1365
 
@@ -1407,7 +1410,7 @@ fn moveHit(
1407
1410
  if (overwrite) side.active.volatiles.state = accuracy;
1408
1411
 
1409
1412
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1410
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1413
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1411
1414
  };
1412
1415
 
1413
1416
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,6 +1428,7 @@ fn moveHit(
1425
1428
  fn checkFaint(
1426
1429
  battle: anytype,
1427
1430
  player: Player,
1431
+ cap: bool,
1428
1432
  options: anytype,
1429
1433
  ) @TypeOf(options.log).Error!?Result {
1430
1434
  const side = battle.side(player);
@@ -1437,8 +1441,8 @@ fn checkFaint(
1437
1441
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1438
1442
  const more = player_out or foe_out;
1439
1443
 
1440
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1441
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1444
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1445
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1442
1446
 
1443
1447
  assert(!side.active.volatiles.MultiHit);
1444
1448
  assert(!foe.active.volatiles.MultiHit);
@@ -1459,7 +1463,7 @@ fn checkFaint(
1459
1463
  return .{ .p1 = foe_choice, .p2 = .Switch };
1460
1464
  }
1461
1465
 
1462
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1466
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1463
1467
  var side = battle.side(player);
1464
1468
  var foe = battle.foe(player);
1465
1469
  assert(side.stored().hp == 0);
@@ -1493,7 +1497,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1493
1497
  }
1494
1498
 
1495
1499
  try options.log.faint(.{ battle.active(player), done });
1496
- options.calc.capped(player);
1500
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1501
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1502
+ if (cap) options.calc.capped(player);
1497
1503
  return null;
1498
1504
  }
1499
1505
 
@@ -1813,7 +1819,6 @@ pub const Effects = struct {
1813
1819
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1814
1820
  if (!try checkHit(battle, player, move, options)) return;
1815
1821
  }
1816
- try options.chance.commit(player, .hit);
1817
1822
  }
1818
1823
 
1819
1824
  if (foe.active.volatiles.Confusion) return;
@@ -1846,10 +1851,7 @@ pub const Effects = struct {
1846
1851
  const foe_ident = battle.active(player.foe());
1847
1852
 
1848
1853
  // Pokémon Showdown handles hit/miss earlier in doMove
1849
- if (!showdown) {
1850
- if (!try checkHit(battle, player, move, options)) return null;
1851
- try options.chance.commit(player, .hit);
1852
- }
1854
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1853
1855
 
1854
1856
  if (volatiles.disable_move != 0) {
1855
1857
  try options.log.fail(.{ foe_ident, .None });
@@ -2051,7 +2053,6 @@ pub const Effects = struct {
2051
2053
  }
2052
2054
  }
2053
2055
 
2054
- try options.chance.commit(player, .hit);
2055
2056
  foe.active.volatiles.LeechSeed = true;
2056
2057
 
2057
2058
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2092,12 +2093,10 @@ pub const Effects = struct {
2092
2093
  if (!has_mimic) {
2093
2094
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2094
2095
  if (!try checkHit(battle, player, move, options)) return;
2095
- try options.chance.commit(player, .hit);
2096
2096
  return options.log.fail(.{ battle.active(player.foe()), .None });
2097
2097
  }
2098
2098
  }
2099
2099
  if (!try checkHit(battle, player, move, options)) return;
2100
- try options.chance.commit(player, .hit);
2101
2100
 
2102
2101
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2103
2102
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2151,7 +2150,6 @@ pub const Effects = struct {
2151
2150
  if (immune) return log.immune(.{ foe_ident, .None });
2152
2151
  if (!try checkHit(battle, player, move, options)) return;
2153
2152
  }
2154
- try options.chance.commit(player, .hit);
2155
2153
 
2156
2154
  foe_stored.status = Status.init(.PAR);
2157
2155
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2219,7 +2217,6 @@ pub const Effects = struct {
2219
2217
 
2220
2218
  if (move.effect == .Poison) {
2221
2219
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2222
- try options.chance.commit(player, .hit);
2223
2220
  } else {
2224
2221
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2225
2222
  return;
@@ -2288,9 +2285,8 @@ pub const Effects = struct {
2288
2285
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2289
2286
  });
2290
2287
  }
2291
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2288
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2292
2289
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2293
- try options.chance.commit(player, .hit);
2294
2290
  }
2295
2291
 
2296
2292
  // Sleep Clause Mod
@@ -2343,9 +2339,7 @@ pub const Effects = struct {
2343
2339
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2344
2340
  if (showdown) {
2345
2341
  if (battle.side(player).last_selected_move == .Teleport) return;
2346
- if (try checkHit(battle, player, move, options)) {
2347
- try options.chance.commit(player, .hit);
2348
- }
2342
+ _ = try checkHit(battle, player, move, options);
2349
2343
  battle.last_damage = 0;
2350
2344
  } else {
2351
2345
  try options.log.fail(.{ battle.active(player), .None });
@@ -2521,7 +2515,6 @@ pub const Effects = struct {
2521
2515
  } else {
2522
2516
  // checkHit already checks for Invulnerable
2523
2517
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2524
- try options.chance.commit(player, .hit);
2525
2518
  }
2526
2519
 
2527
2520
  var stats = &foe.active.stats;
@@ -2689,6 +2682,8 @@ pub const Rolls = struct {
2689
2682
  fn speedTie(battle: anytype, options: anytype) !bool {
2690
2683
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2691
2684
  player == .P1
2685
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2686
+ battle.rng.speedTie()
2692
2687
  else if (showdown)
2693
2688
  battle.rng.range(u8, 0, 2) == 0
2694
2689
  else
@@ -2698,21 +2693,31 @@ pub const Rolls = struct {
2698
2693
  return p1;
2699
2694
  }
2700
2695
 
2701
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2696
+ fn criticalHit(
2697
+ battle: anytype,
2698
+ player: Player,
2699
+ rate: u8,
2700
+ spurious: bool,
2701
+ options: anytype,
2702
+ ) !bool {
2702
2703
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2703
2704
  val == .true
2705
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2706
+ battle.rng.criticalHit(player, rate)
2704
2707
  else if (showdown)
2705
2708
  battle.rng.chance(u8, rate, 256)
2706
2709
  else
2707
2710
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2708
2711
 
2709
- try options.chance.criticalHit(player, crit, rate);
2712
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2710
2713
  return crit;
2711
2714
  }
2712
2715
 
2713
2716
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2714
2717
  const roll = if (options.calc.overridden(player, .damage)) |val|
2715
2718
  val
2719
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2720
+ battle.rng.damage(player)
2716
2721
  else roll: {
2717
2722
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2718
2723
  while (true) {
@@ -2726,21 +2731,27 @@ pub const Rolls = struct {
2726
2731
  return roll;
2727
2732
  }
2728
2733
 
2729
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2734
+ const uN = if (pkmn.options.miss) u8 else u9;
2735
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2736
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2730
2737
  const ok = if (options.calc.overridden(player, .hit)) |val|
2731
2738
  val == .true
2739
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2740
+ battle.rng.hit(player, acc)
2732
2741
  else if (showdown)
2733
- battle.rng.chance(u8, accuracy, 256)
2742
+ battle.rng.chance(uN, acc, 256)
2734
2743
  else
2735
- battle.rng.next() < accuracy;
2744
+ battle.rng.next() < acc;
2736
2745
 
2737
- options.chance.hit(ok, accuracy);
2746
+ try options.chance.hit(player, ok, acc);
2738
2747
  return ok;
2739
2748
  }
2740
2749
 
2741
2750
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2742
2751
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2743
2752
  val == .true
2753
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2754
+ battle.rng.confused(player)
2744
2755
  else if (showdown)
2745
2756
  !battle.rng.chance(u8, 128, 256)
2746
2757
  else
@@ -2753,6 +2764,8 @@ pub const Rolls = struct {
2753
2764
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2754
2765
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2755
2766
  val == .true
2767
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2768
+ battle.rng.paralyzed(player)
2756
2769
  else if (showdown)
2757
2770
  battle.rng.chance(u8, 63, 256)
2758
2771
  else
@@ -2765,6 +2778,8 @@ pub const Rolls = struct {
2765
2778
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2766
2779
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2767
2780
  val == .true
2781
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2782
+ battle.rng.secondaryChance(player, 25)
2768
2783
  else if (showdown)
2769
2784
  battle.rng.chance(u8, 25, 256)
2770
2785
  else
@@ -2779,6 +2794,8 @@ pub const Rolls = struct {
2779
2794
 
2780
2795
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2781
2796
  val == .true
2797
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2798
+ battle.rng.secondaryChance(player, rate)
2782
2799
  else if (showdown)
2783
2800
  battle.rng.chance(u8, rate, 256)
2784
2801
  else
@@ -2793,6 +2810,8 @@ pub const Rolls = struct {
2793
2810
 
2794
2811
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2795
2812
  val == .true
2813
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2814
+ battle.rng.secondaryChance(player, rate)
2796
2815
  else if (showdown)
2797
2816
  battle.rng.chance(u8, rate, 256)
2798
2817
  else
@@ -2805,6 +2824,8 @@ pub const Rolls = struct {
2805
2824
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2806
2825
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2807
2826
  val == .true
2827
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2828
+ battle.rng.secondaryChance(player, 85)
2808
2829
  else if (showdown)
2809
2830
  battle.rng.chance(u8, 85, 256)
2810
2831
  else
@@ -2817,6 +2838,8 @@ pub const Rolls = struct {
2817
2838
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2818
2839
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2819
2840
  val
2841
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2842
+ battle.rng.metronome(player)
2820
2843
  else if (showdown)
2821
2844
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2822
2845
  else move: {
@@ -2835,6 +2858,8 @@ pub const Rolls = struct {
2835
2858
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2836
2859
  const power = if (options.calc.overridden(player, .psywave)) |val|
2837
2860
  val - 1
2861
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2862
+ battle.rng.psywave(player)
2838
2863
  else if (showdown)
2839
2864
  battle.rng.range(u8, 0, max)
2840
2865
  else power: {
@@ -2852,6 +2877,8 @@ pub const Rolls = struct {
2852
2877
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2853
2878
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2854
2879
  @intCast(val)
2880
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2881
+ battle.rng.sleepDuration(player)
2855
2882
  else if (showdown)
2856
2883
  battle.rng.range(u3, 1, 8)
2857
2884
  else duration: {
@@ -2869,6 +2896,8 @@ pub const Rolls = struct {
2869
2896
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2870
2897
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2871
2898
  val
2899
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2900
+ battle.rng.disableDuration(player)
2872
2901
  else if (showdown)
2873
2902
  battle.rng.range(u4, 1, 9)
2874
2903
  else
@@ -2882,6 +2911,8 @@ pub const Rolls = struct {
2882
2911
  fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2883
2912
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2884
2913
  @intCast(val)
2914
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2915
+ battle.rng.confusionDuration(player, self)
2885
2916
  else if (showdown)
2886
2917
  battle.rng.range(u3, 2, 6)
2887
2918
  else
@@ -2895,6 +2926,8 @@ pub const Rolls = struct {
2895
2926
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2896
2927
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2897
2928
  @intCast(val)
2929
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2930
+ battle.rng.attackingDuration(player)
2898
2931
  else if (showdown)
2899
2932
  battle.rng.range(u3, 2, 4)
2900
2933
  else
@@ -2916,6 +2949,8 @@ pub const Rolls = struct {
2916
2949
  const roll = if (effect == .Binding) .duration else .multi_hit;
2917
2950
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2918
2951
  @intCast(val)
2952
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
2953
+ battle.rng.distribution(player)
2919
2954
  else if (showdown)
2920
2955
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2921
2956
  else n: {
@@ -2948,7 +2983,9 @@ pub const Rolls = struct {
2948
2983
  else
2949
2984
  null;
2950
2985
 
2951
- const slot: u4 = overridden orelse slot: {
2986
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
2987
+ battle.rng.moveSlot(player, moves, check_pp)
2988
+ else slot: {
2952
2989
  if (showdown) {
2953
2990
  if (check_pp == 0) {
2954
2991
  var i: usize = moves.len;
@@ -3032,7 +3069,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3032
3069
  const stored = side.stored();
3033
3070
 
3034
3071
  // While players are not given any input options on the cartridge in these cases,
3035
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3072
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3036
3073
  //
3037
3074
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3038
3075
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3040,7 +3077,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3040
3077
  // passing due to being forced into using a move. Instead of introducing another option
3041
3078
  // we simply repurpose Move with no move slot, even though pedantically this is not
3042
3079
  // strictly correct as the player would not have been presented the option to move or
3043
- // switch at all.
3080
+ // switch at all
3044
3081
  if (isForced(active)) {
3045
3082
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3046
3083
  n += 1;
@@ -3059,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3059
3096
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3060
3097
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3061
3098
  // either disables all other moves in the case of limited or requires you to select a
3062
- // move normally if sleeping/frozen/bound.
3099
+ // move normally if sleeping/frozen/bound
3063
3100
  if (!showdown and (limited or foe.active.volatiles.Binding or
3064
3101
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3065
3102
  {
@@ -3072,7 +3109,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3072
3109
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3073
3110
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3074
3111
  // Move will not result in forcing the subsequent use unless the user also had the
3075
- // proc-ed move in their moveset) and disabling all other moves.
3112
+ // proc-ed move in their moveset) and disabling all other moves
3076
3113
  if (limited) {
3077
3114
  assert(showdown);
3078
3115
  assert(side.last_selected_move != .None);