@pkmn/engine 0.1.0-dev.545e0ef3 → 0.1.0-dev.550c3889

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,30 +56,34 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
+
59
61
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
60
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
61
63
  if (battle.turn == 0) return start(battle, log);
62
64
 
65
+ var s1 = false;
66
+ var s2 = false;
63
67
  var f1: ?Move = null;
64
68
  var f2: ?Move = null;
65
69
 
66
- if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
- if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
70
+ if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
+ if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
+
73
+ var p1 = battle.side(.P1);
74
+ var p2 = battle.side(.P2);
75
+
76
+ var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
68
78
 
69
79
  if (turnOrder(battle, c1, c2) == .P1) {
70
- if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
80
+ if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
71
81
  } else {
72
- if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
82
+ if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
73
83
  }
74
84
 
75
- var p1 = battle.side(.P1);
76
- if (p1.active.volatiles.attacks == 0) {
77
- p1.active.volatiles.Binding = false;
78
- }
79
- var p2 = battle.side(.P2);
80
- if (p2.active.volatiles.attacks == 0) {
81
- p2.active.volatiles.Binding = false;
82
- }
85
+ if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
+ if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
83
87
 
84
88
  return endTurn(battle, log);
85
89
  }
@@ -112,6 +116,7 @@ fn selectMove(
112
116
  player: Player,
113
117
  choice: Choice,
114
118
  foe_choice: Choice,
119
+ skip_turn: *bool,
115
120
  from: *?Move,
116
121
  ) ?Result {
117
122
  if (choice.type == .Pass) return null;
@@ -127,14 +132,20 @@ fn selectMove(
127
132
  if (volatiles.Recharging) return null;
128
133
  if (volatiles.Rage) {
129
134
  from.* = side.last_used_move;
130
- if (showdown) saveMove(battle, player, null);
135
+ if (showdown) {
136
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
137
+ saveMove(battle, player, null);
138
+ }
131
139
  return null;
132
140
  }
133
141
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
134
142
  if (!showdown) volatiles.Flinch = false;
135
143
  if (volatiles.Thrashing or volatiles.Charging) {
136
144
  from.* = side.last_selected_move;
137
- if (showdown) saveMove(battle, player, null);
145
+ if (showdown) {
146
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
+ saveMove(battle, player, null);
148
+ }
138
149
  return null;
139
150
  }
140
151
 
@@ -144,7 +155,10 @@ fn selectMove(
144
155
  // pre-move select
145
156
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
146
157
  assert(showdown or choice.data == 0);
147
- if (showdown) saveMove(battle, player, choice);
158
+ if (showdown) {
159
+ if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
160
+ saveMove(battle, player, choice);
161
+ }
148
162
  return null;
149
163
  }
150
164
  if (volatiles.Binding) {
@@ -175,6 +189,7 @@ fn selectMove(
175
189
  }
176
190
 
177
191
  if (battle.foe(player).active.volatiles.Binding) {
192
+ skip_turn.* = true;
178
193
  if (showdown) {
179
194
  saveMove(battle, player, choice);
180
195
  } else {
@@ -185,6 +200,7 @@ fn selectMove(
185
200
  }
186
201
 
187
202
  // move select
203
+ volatiles.state = 0;
188
204
  if (choice.data == 0) {
189
205
  const struggle = ok: {
190
206
  for (side.active.moves, 0..) |move, i| {
@@ -301,11 +317,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
301
317
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
318
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
319
 
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
308
-
320
+ const p1 = Rolls.speedTie(battle);
309
321
  if (!showdown) return if (p1) .P1 else .P2;
310
322
 
311
323
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +327,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
327
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
328
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
329
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
318
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
330
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
331
  // won the original coin flip
320
332
  if (t1 and t2) return .P1;
321
333
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +340,13 @@ fn doTurn(
328
340
  battle: anytype,
329
341
  player: Player,
330
342
  player_choice: Choice,
343
+ player_rewrap: bool,
344
+ player_skip: bool,
331
345
  player_from: ?Move,
332
346
  foe_player: Player,
333
347
  foe_choice: Choice,
348
+ foe_rewrap: bool,
349
+ foe_skip: bool,
334
350
  foe_from: ?Move,
335
351
  log: anytype,
336
352
  ) !?Result {
@@ -338,7 +354,16 @@ fn doTurn(
338
354
 
339
355
  var residual = true;
340
356
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
357
+ if (try executeMove(
358
+ battle,
359
+ player,
360
+ player_choice,
361
+ player_rewrap,
362
+ player_skip,
363
+ player_from,
364
+ &residual,
365
+ log,
366
+ )) |r| return r;
342
367
  if (!replace) {
343
368
  if (player_choice.type != .Switch) {
344
369
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -349,7 +374,16 @@ fn doTurn(
349
374
 
350
375
  residual = true;
351
376
  replace = battle.side(foe_player).stored().hp == 0;
352
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
377
+ if (try executeMove(
378
+ battle,
379
+ foe_player,
380
+ foe_choice,
381
+ foe_rewrap,
382
+ foe_skip,
383
+ foe_from,
384
+ &residual,
385
+ log,
386
+ )) |r| return r;
353
387
  if (!replace) {
354
388
  if (foe_choice.type != .Switch) {
355
389
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -372,6 +406,8 @@ fn executeMove(
372
406
  battle: anytype,
373
407
  player: Player,
374
408
  choice: Choice,
409
+ rewrap: bool,
410
+ skip: bool,
375
411
  from: ?Move,
376
412
  residual: *bool,
377
413
  log: anytype,
@@ -383,9 +419,10 @@ fn executeMove(
383
419
  return null;
384
420
  }
385
421
 
422
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
423
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
424
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
386
425
  if (side.last_selected_move == .SKIP_TURN) {
387
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
388
- // Binding instead gets handled in beforeMove after sleep and freeze
389
426
  assert(!showdown);
390
427
  if (battle.foe(player).active.volatiles.Binding) {
391
428
  try log.cant(battle.active(player), .Bound);
@@ -435,7 +472,7 @@ fn executeMove(
435
472
 
436
473
  var skip_can = false;
437
474
  var skip_pp = false;
438
- switch (try beforeMove(battle, player, from, residual, log)) {
475
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
439
476
  .done => return null,
440
477
  .skip_can => skip_can = true,
441
478
  .skip_pp => skip_pp = true,
@@ -447,14 +484,15 @@ fn executeMove(
447
484
  return null;
448
485
  }
449
486
 
450
- return doMove(battle, player, mslot, auto, residual, log);
487
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
451
488
  }
452
489
 
453
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
490
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
454
491
 
455
492
  fn beforeMove(
456
493
  battle: anytype,
457
494
  player: Player,
495
+ skip: bool,
458
496
  from: ?Move,
459
497
  residual: *bool,
460
498
  log: anytype,
@@ -486,13 +524,15 @@ fn beforeMove(
486
524
  return .done;
487
525
  }
488
526
 
489
- if (foe.active.volatiles.Binding) {
527
+ if (skip or foe.active.volatiles.Binding) {
490
528
  try log.cant(ident, .Bound);
491
529
  return .done;
492
530
  }
493
531
 
494
- if (!showdown and volatiles.Flinch) {
495
- volatiles.Flinch = false;
532
+ if (volatiles.Flinch) {
533
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
534
+ // Pokémon can sometimes flinch multiple times from the same original hit
535
+ if (!showdown) volatiles.Flinch = false;
496
536
  try log.cant(ident, .Flinch);
497
537
  return .done;
498
538
  }
@@ -513,13 +553,6 @@ fn beforeMove(
513
553
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
514
554
  if (showdown and try disabled(side, ident, log)) return .done;
515
555
 
516
- // Pokémon Showdown checks for Flinch *after* instead of before
517
- if (showdown and volatiles.Flinch) {
518
- volatiles.Flinch = false;
519
- try log.cant(ident, .Flinch);
520
- return .done;
521
- }
522
-
523
556
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
524
557
  // before the side had a selected a move - we simply need to assume this leads to a desync
525
558
  if (side.last_selected_move == .None) {
@@ -537,12 +570,7 @@ fn beforeMove(
537
570
  } else {
538
571
  try log.activate(ident, .Confusion);
539
572
 
540
- const confused = if (showdown)
541
- !battle.rng.chance(u8, 128, 256)
542
- else
543
- battle.rng.next() >= Gen12.percent(50) + 1;
544
-
545
- if (confused) {
573
+ if (Rolls.confused(battle)) {
546
574
  assert(!volatiles.MultiHit);
547
575
  if (!volatiles.Rage) volatiles.state = 0;
548
576
  volatiles.Bide = false;
@@ -568,8 +596,12 @@ fn beforeMove(
568
596
  player
569
597
  else
570
598
  player.foe();
599
+
600
+ const uncapped = battle.last_damage;
571
601
  // Skipping adjustDamage / randomizeDamage / checkHit
572
602
  _ = try applyDamage(battle, player, target, .Confusion, log);
603
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
604
+ if (showdown) battle.last_damage = uncapped;
573
605
 
574
606
  return .done;
575
607
  }
@@ -578,22 +610,15 @@ fn beforeMove(
578
610
 
579
611
  if (!showdown and try disabled(side, ident, log)) return .done;
580
612
 
581
- if (Status.is(stored.status, .PAR)) {
582
- const paralyzed = if (showdown)
583
- battle.rng.chance(u8, 63, 256)
584
- else
585
- battle.rng.next() < Gen12.percent(25);
586
-
587
- if (paralyzed) {
588
- if (!volatiles.Rage) volatiles.state = 0;
589
- volatiles.Bide = false;
590
- volatiles.Thrashing = false;
591
- volatiles.Charging = false;
592
- volatiles.Binding = false;
593
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
594
- try log.cant(ident, .Paralysis);
595
- return .done;
596
- }
613
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
614
+ if (!volatiles.Rage) volatiles.state = 0;
615
+ volatiles.Bide = false;
616
+ volatiles.Thrashing = false;
617
+ volatiles.Charging = false;
618
+ volatiles.Binding = false;
619
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
620
+ try log.cant(ident, .Paralysis);
621
+ return .done;
597
622
  }
598
623
 
599
624
  if (volatiles.Bide) {
@@ -620,8 +645,9 @@ fn beforeMove(
620
645
  return .done;
621
646
  }
622
647
 
648
+ const sub = showdown and foe.active.volatiles.Substitute;
623
649
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
624
- if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
650
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
625
651
 
626
652
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
627
653
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -642,8 +668,13 @@ fn beforeMove(
642
668
  try log.start(battle.active(player), .ConfusionSilent);
643
669
  }
644
670
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
645
- if (showdown and handleThrashing(battle, active)) {
646
- try log.start(battle.active(player), .ConfusionSilent);
671
+ if (showdown) {
672
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
673
+ // used in useMove and doesn't have the notion of skipping canMove semantics
674
+ side.last_used_move = side.last_selected_move;
675
+ if (handleThrashing(battle, active)) {
676
+ try log.start(battle.active(player), .ConfusionSilent);
677
+ }
647
678
  }
648
679
  return .skip_can;
649
680
  }
@@ -658,7 +689,11 @@ fn beforeMove(
658
689
  side.last_selected_move,
659
690
  );
660
691
  if (showdown or battle.last_damage != 0) {
692
+ const sub = showdown and foe.active.volatiles.Substitute;
661
693
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
694
+ if (battle.foe(player).stored().hp > 0 and !sub) {
695
+ try buildRage(battle, player.foe(), log);
696
+ }
662
697
  }
663
698
  return .done;
664
699
  }
@@ -716,7 +751,7 @@ fn canMove(
716
751
  Effects.thrashing(battle, player);
717
752
  } else if (!showdown and move.effect == .Binding) {
718
753
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
719
- Effects.binding(battle, player);
754
+ Effects.binding(battle, player, false);
720
755
  }
721
756
 
722
757
  return true;
@@ -757,6 +792,7 @@ fn doMove(
757
792
  battle: anytype,
758
793
  player: Player,
759
794
  mslot: u4,
795
+ rewrap: bool,
760
796
  auto: bool,
761
797
  residual: *bool,
762
798
  log: anytype,
@@ -787,12 +823,15 @@ fn doMove(
787
823
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
788
824
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
789
825
  if (immune and move.effect != .Binding) break :miss true;
790
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
826
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
827
+ battle.last_damage = 0;
828
+ break :miss true;
829
+ }
791
830
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
792
831
  };
793
832
  assert(!immune or miss or (showdown and move.effect == .Binding));
794
833
 
795
- var late = showdown;
834
+ var late = showdown and move.effect != .Explode;
796
835
  const skip = status or immune;
797
836
  if ((!showdown or (!skip or counter)) and !miss) blk: {
798
837
  if (showdown and move.effect.isMulti()) {
@@ -845,9 +884,9 @@ fn doMove(
845
884
 
846
885
  if (!showdown or !miss) {
847
886
  if (move.effect == .MirrorMove) {
848
- return mirrorMove(battle, player, mslot, auto, residual, log);
887
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
849
888
  } else if (move.effect == .Metronome) {
850
- return metronome(battle, player, mslot, auto, residual, log);
889
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
851
890
  } else if (move.effect.onEnd()) {
852
891
  try onEnd(battle, player, move, log);
853
892
  return null;
@@ -862,7 +901,7 @@ fn doMove(
862
901
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
863
902
  if (ohko) {
864
903
  try log.immune(foe_ident, .OHKO);
865
- } else if (immune and !invulnerable) {
904
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
866
905
  try log.immune(foe_ident, .None);
867
906
  } else if (mist) {
868
907
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -894,6 +933,9 @@ fn doMove(
894
933
  hits = side.active.volatiles.attacks;
895
934
  }
896
935
 
936
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
937
+ const sub = showdown and foe.active.volatiles.Substitute;
938
+
897
939
  var nullified = false;
898
940
  var hit: u4 = 0;
899
941
  while (hit < hits) {
@@ -909,7 +951,7 @@ fn doMove(
909
951
  if (hit == 0 and ohko) try log.ohko();
910
952
  hit += 1;
911
953
  if (foe.stored().hp == 0) break;
912
- if (!late) try buildRage(battle, player.foe(), log);
954
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
913
955
  // If the substitute breaks during a multi-hit attack, the attack ends
914
956
  if (nullified) break;
915
957
  }
@@ -925,7 +967,7 @@ fn doMove(
925
967
  } else if (showdown) {
926
968
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
927
969
  if (move.effect == .Binding) {
928
- Effects.binding(battle, player);
970
+ Effects.binding(battle, player, rewrap);
929
971
  if (immune) {
930
972
  battle.last_damage = 0;
931
973
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -946,8 +988,8 @@ fn doMove(
946
988
 
947
989
  if (foe.stored().hp == 0) return null;
948
990
 
949
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
950
- if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
991
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
992
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
951
993
 
952
994
  if (!move.effect.isSpecial()) {
953
995
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -980,8 +1022,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
980
1022
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
981
1023
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
982
1024
 
983
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1025
+ return Rolls.criticalHit(battle, chance);
985
1026
  }
986
1027
 
987
1028
  fn calcDamage(
@@ -1085,50 +1126,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1085
1126
 
1086
1127
  fn randomizeDamage(battle: anytype) void {
1087
1128
  if (battle.last_damage <= 1) return;
1088
-
1089
- const random = if (showdown)
1090
- battle.rng.range(u8, 217, 256)
1091
- else loop: {
1092
- while (true) {
1093
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1094
- if (r >= 217) break :loop r;
1095
- }
1096
- };
1097
-
1129
+ const random = Rolls.damage(battle);
1098
1130
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1099
1131
  }
1100
1132
 
1101
1133
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1102
1134
  const side = battle.side(player);
1135
+ const foe = battle.foe(player);
1103
1136
 
1104
1137
  if (!try checkHit(battle, player, move, log)) return null;
1105
1138
 
1106
1139
  battle.last_damage = switch (side.last_selected_move) {
1107
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1140
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1108
1141
  .SeismicToss, .NightShade => side.stored().level,
1109
1142
  .SonicBoom => 20,
1110
1143
  .DragonRage => 40,
1111
1144
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1112
1145
  .Psywave => power: {
1113
1146
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1114
- if (showdown) {
1115
- break :power battle.rng.range(u8, 0, max);
1116
- } else {
1117
- // GLITCH: Psywave infinite glitch loop
1118
- if (max <= 1) return Result.Error;
1119
- while (true) {
1120
- const r = battle.rng.next();
1121
- if (r < max) break :power r;
1122
- }
1123
- }
1147
+ // GLITCH: Psywave infinite glitch loop
1148
+ if (!showdown and max <= 1) return Result.Error;
1149
+ break :power Rolls.psywave(battle, max);
1124
1150
  },
1125
1151
  else => unreachable,
1126
1152
  };
1127
1153
 
1128
1154
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1129
1155
 
1156
+ const sub = showdown and foe.active.volatiles.Substitute;
1130
1157
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1131
- if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1158
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1132
1159
 
1133
1160
  return null;
1134
1161
  }
@@ -1177,7 +1204,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1177
1204
  // Pokémon Showdown calls checkHit before Counter
1178
1205
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1179
1206
 
1207
+ const sub = showdown and foe.active.volatiles.Substitute;
1180
1208
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1209
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1181
1210
  return null;
1182
1211
  }
1183
1212
 
@@ -1221,6 +1250,7 @@ fn mirrorMove(
1221
1250
  battle: anytype,
1222
1251
  player: Player,
1223
1252
  mslot: u4,
1253
+ rewrap: bool,
1224
1254
  auto: bool,
1225
1255
  residual: *bool,
1226
1256
  log: anytype,
@@ -1238,36 +1268,25 @@ fn mirrorMove(
1238
1268
  incrementPP(side, mslot);
1239
1269
 
1240
1270
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1241
- return doMove(battle, player, mslot, auto, residual, log);
1271
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1242
1272
  }
1243
1273
 
1244
1274
  fn metronome(
1245
1275
  battle: anytype,
1246
1276
  player: Player,
1247
1277
  mslot: u4,
1278
+ rewrap: bool,
1248
1279
  auto: bool,
1249
1280
  residual: *bool,
1250
1281
  log: anytype,
1251
1282
  ) !?Result {
1252
1283
  var side = battle.side(player);
1253
1284
 
1254
- side.last_selected_move = if (showdown) blk: {
1255
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1256
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1257
- break :blk @intToEnum(Move, r + mod);
1258
- } else loop: {
1259
- while (true) {
1260
- const r = battle.rng.next();
1261
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1262
- if (r >= @enumToInt(Move.Struggle)) continue;
1263
- break :loop @intToEnum(Move, r);
1264
- }
1265
- };
1266
-
1285
+ side.last_selected_move = Rolls.metronome(battle);
1267
1286
  incrementPP(side, mslot);
1268
1287
 
1269
1288
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1270
- return doMove(battle, player, mslot, auto, residual, log);
1289
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1271
1290
  }
1272
1291
 
1273
1292
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1321,23 +1340,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1321
1340
  }
1322
1341
 
1323
1342
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1324
- var state = &side.active.volatiles.state;
1325
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1326
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1327
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1343
+ const state = side.active.volatiles.state;
1344
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1345
+ const overwritten = overwrite and state > 0;
1346
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1347
+
1348
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1328
1349
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1329
1350
  accuracy = accuracy * boost[0] / boost[1];
1330
1351
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1331
1352
  accuracy = accuracy * boost[0] / boost[1];
1332
1353
  accuracy = @min(255, @max(1, accuracy));
1333
1354
 
1334
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1355
+ // Pokémon Showdown only overwrites if the volatile is present
1356
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1357
+ if (overwrite) side.active.volatiles.state = accuracy;
1335
1358
 
1336
1359
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1337
- break :miss if (showdown)
1338
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
- else
1340
- battle.rng.next() >= accuracy;
1360
+ break :miss !Rolls.hit(battle, accuracy);
1341
1361
  };
1342
1362
 
1343
1363
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1469,9 +1489,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1469
1489
  damage *= volatiles.toxic;
1470
1490
  }
1471
1491
 
1472
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
- if (showdown) battle.last_damage = damage;
1474
-
1475
1492
  const amount = @min(damage, stored.hp);
1476
1493
  stored.hp -= amount;
1477
1494
 
@@ -1563,6 +1580,42 @@ fn checkEBC(battle: anytype) bool {
1563
1580
  return false;
1564
1581
  }
1565
1582
 
1583
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1584
+ if (side.active.volatiles.disabled_move != 0) {
1585
+ // A Pokémon that transforms after being disabled may end up with less move slots
1586
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1587
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1588
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1589
+ m
1590
+ else
1591
+ side.last_selected_move;
1592
+ if (m != .None and m == last) {
1593
+ side.active.volatiles.Charging = false;
1594
+ try log.disabled(ident, last);
1595
+ return true;
1596
+ }
1597
+ }
1598
+ return false;
1599
+ }
1600
+
1601
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1602
+ const side = battle.side(who);
1603
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1604
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1605
+ }
1606
+ }
1607
+
1608
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1609
+ var volatiles = &active.volatiles;
1610
+ assert(volatiles.Thrashing);
1611
+ if (volatiles.attacks > 0) return false;
1612
+
1613
+ volatiles.Thrashing = false;
1614
+ volatiles.Confusion = true;
1615
+ volatiles.confusion = Rolls.confusionDuration(battle);
1616
+ return true;
1617
+ }
1618
+
1566
1619
  inline fn onBegin(
1567
1620
  battle: anytype,
1568
1621
  player: Player,
@@ -1653,10 +1706,7 @@ pub const Effects = struct {
1653
1706
  side.active.volatiles.Bide = true;
1654
1707
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1655
1708
  side.active.volatiles.state = 0;
1656
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
- battle.rng.range(u4, 2, 4)
1658
- else
1659
- (battle.rng.next() & 1) + 2);
1709
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1660
1710
 
1661
1711
  try log.start(battle.active(player), .Bide);
1662
1712
  }
@@ -1678,14 +1728,8 @@ pub const Effects = struct {
1678
1728
  return;
1679
1729
  }
1680
1730
 
1681
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1682
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1683
- else
1684
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1685
- Gen12.percent(10)
1686
- else
1687
- Gen12.percent(30));
1688
- if (!chance) return;
1731
+ if (foe.active.types.includes(move.type)) return;
1732
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1689
1733
 
1690
1734
  foe_stored.status = Status.init(.BRN);
1691
1735
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1709,11 +1753,7 @@ pub const Effects = struct {
1709
1753
  const sub = foe.active.volatiles.Substitute;
1710
1754
 
1711
1755
  if (move.effect == .ConfusionChance) {
1712
- const chance = if (showdown)
1713
- battle.rng.chance(u8, 25, 256)
1714
- else
1715
- battle.rng.next() < Gen12.percent(10);
1716
- if (!chance) return;
1756
+ if (!Rolls.confusionChance(battle)) return;
1717
1757
  } else {
1718
1758
  if (showdown) {
1719
1759
  if (!try checkHit(battle, player, move, log)) {
@@ -1732,10 +1772,7 @@ pub const Effects = struct {
1732
1772
 
1733
1773
  if (foe.active.volatiles.Confusion) return;
1734
1774
  foe.active.volatiles.Confusion = true;
1735
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
- battle.rng.range(u8, 2, 6)
1737
- else
1738
- (battle.rng.next() & 3) + 2);
1775
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1739
1776
 
1740
1777
  try log.start(battle.active(player.foe()), .Confusion);
1741
1778
  }
@@ -1785,11 +1822,8 @@ pub const Effects = struct {
1785
1822
  return Result.Error;
1786
1823
  }
1787
1824
 
1788
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1789
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1790
- battle.rng.range(u8, 1, 9)
1791
- else
1792
- (battle.rng.next() & 7) + 1);
1825
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1826
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1793
1827
 
1794
1828
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1795
1829
  return null;
@@ -1801,7 +1835,7 @@ pub const Effects = struct {
1801
1835
 
1802
1836
  if (battle.last_damage == 0) {
1803
1837
  assert(showdown);
1804
- return;
1838
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1805
1839
  }
1806
1840
 
1807
1841
  const drain = @max(battle.last_damage / 2, 1);
@@ -1828,15 +1862,7 @@ pub const Effects = struct {
1828
1862
  var volatiles = &battle.foe(player).active.volatiles;
1829
1863
 
1830
1864
  if (volatiles.Substitute) return;
1831
-
1832
- const chance = if (showdown)
1833
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1834
- else
1835
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1836
- Gen12.percent(10)
1837
- else
1838
- Gen12.percent(30));
1839
- if (!chance) return;
1865
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1840
1866
 
1841
1867
  volatiles.Flinch = true;
1842
1868
  volatiles.Recharging = false;
@@ -1862,12 +1888,8 @@ pub const Effects = struct {
1862
1888
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1863
1889
  }
1864
1890
 
1865
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1866
- battle.rng.chance(u8, 26, 256)
1867
- else
1868
- battle.rng.next() < 1 + Gen12.percent(10);
1869
- if (!chance) return;
1870
-
1891
+ if (foe.active.types.includes(move.type)) return;
1892
+ if (!Rolls.secondaryChance(battle, true)) return;
1871
1893
  // Freeze Clause Mod
1872
1894
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1873
1895
 
@@ -2012,7 +2034,7 @@ pub const Effects = struct {
2012
2034
  }
2013
2035
  if (!try checkHit(battle, player, move, log)) return;
2014
2036
 
2015
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2037
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2016
2038
  side.active.move(oslot).id = foe.active.move(rslot).id;
2017
2039
 
2018
2040
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2033,7 +2055,8 @@ pub const Effects = struct {
2033
2055
  assert(!side.active.volatiles.MultiHit);
2034
2056
  side.active.volatiles.MultiHit = true;
2035
2057
 
2036
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2058
+ side.active.volatiles.attacks =
2059
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2037
2060
  }
2038
2061
 
2039
2062
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2077,14 +2100,8 @@ pub const Effects = struct {
2077
2100
  }
2078
2101
 
2079
2102
  // Body Slam can't paralyze a Normal type Pokémon
2080
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2081
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2082
- else
2083
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2084
- Gen12.percent(10)
2085
- else
2086
- Gen12.percent(30));
2087
- if (!chance) return;
2103
+ if (foe.active.types.includes(move.type)) return;
2104
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2088
2105
 
2089
2106
  foe_stored.status = Status.init(.PAR);
2090
2107
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2127,16 +2144,7 @@ pub const Effects = struct {
2127
2144
 
2128
2145
  if (move.effect == .Poison) {
2129
2146
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2130
- } else {
2131
- const chance = if (showdown)
2132
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2133
- else
2134
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2135
- Gen12.percent(20)
2136
- else
2137
- Gen12.percent(40));
2138
- if (!chance) return;
2139
- }
2147
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2140
2148
 
2141
2149
  foe_stored.status = Status.init(.PSN);
2142
2150
  if (toxic) {
@@ -2150,10 +2158,8 @@ pub const Effects = struct {
2150
2158
 
2151
2159
  fn rage(battle: anytype, player: Player) !void {
2152
2160
  var volatiles = &battle.side(player).active.volatiles;
2153
- if (volatiles.Rage) return;
2154
- volatiles.Rage = true;
2155
2161
  assert(!volatiles.Bide);
2156
- volatiles.state = 0;
2162
+ volatiles.Rage = true;
2157
2163
  }
2158
2164
 
2159
2165
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2205,16 +2211,7 @@ pub const Effects = struct {
2205
2211
  }
2206
2212
  foe.active.volatiles.Recharging = false;
2207
2213
 
2208
- const duration = @intCast(u3, if (showdown)
2209
- battle.rng.range(u8, 1, 8)
2210
- else loop: {
2211
- while (true) {
2212
- const r = battle.rng.next() & 7;
2213
- if (r != 0) break :loop r;
2214
- }
2215
- });
2216
-
2217
- foe_stored.status = Status.slp(duration);
2214
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2218
2215
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2219
2216
  }
2220
2217
 
@@ -2267,11 +2264,7 @@ pub const Effects = struct {
2267
2264
  assert(!volatiles.Bide);
2268
2265
 
2269
2266
  volatiles.Thrashing = true;
2270
- volatiles.state = 0;
2271
- volatiles.attacks = @intCast(u3, if (showdown)
2272
- battle.rng.range(u8, 2, 4)
2273
- else
2274
- (battle.rng.next() & 1) + 2);
2267
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2275
2268
  }
2276
2269
 
2277
2270
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2303,7 +2296,7 @@ pub const Effects = struct {
2303
2296
  try log.transform(battle.active(player), foe_ident);
2304
2297
  }
2305
2298
 
2306
- fn binding(battle: anytype, player: Player) void {
2299
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2307
2300
  var side = battle.side(player);
2308
2301
  var foe = battle.foe(player);
2309
2302
 
@@ -2311,9 +2304,11 @@ pub const Effects = struct {
2311
2304
  side.active.volatiles.Binding = true;
2312
2305
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2313
2306
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2314
- if (!showdown) foe.active.volatiles.Recharging = false;
2307
+ if (!showdown) {
2308
+ foe.active.volatiles.Recharging = false;
2309
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2315
2310
 
2316
- side.active.volatiles.attacks = distribution(battle) - 1;
2311
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2317
2312
  }
2318
2313
 
2319
2314
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2340,9 +2335,11 @@ pub const Effects = struct {
2340
2335
  return try log.fail(ident, .None);
2341
2336
  }
2342
2337
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2343
- const stat = unmodifiedStats(battle, player).atk;
2338
+ const stat = unmodifiedStats(battle, side).atk;
2344
2339
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2340
  try log.boost(ident, reason, n);
2341
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2342
+ if (showdown and move.effect == .Rage) return;
2346
2343
  },
2347
2344
  .DefenseUp1, .DefenseUp2 => {
2348
2345
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2358,7 +2355,7 @@ pub const Effects = struct {
2358
2355
  return try log.fail(ident, .None);
2359
2356
  }
2360
2357
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2361
- const stat = unmodifiedStats(battle, player).def;
2358
+ const stat = unmodifiedStats(battle, side).def;
2362
2359
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2363
2360
  try log.boost(ident, .Defense, n);
2364
2361
  },
@@ -2375,7 +2372,7 @@ pub const Effects = struct {
2375
2372
  return try log.fail(ident, .None);
2376
2373
  }
2377
2374
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2378
- const stat = unmodifiedStats(battle, player).spe;
2375
+ const stat = unmodifiedStats(battle, side).spe;
2379
2376
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
2377
  try log.boost(ident, .Speed, 2);
2381
2378
  },
@@ -2395,7 +2392,7 @@ pub const Effects = struct {
2395
2392
  return try log.fail(ident, .None);
2396
2393
  }
2397
2394
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2398
- const stat = unmodifiedStats(battle, player).spc;
2395
+ const stat = unmodifiedStats(battle, side).spc;
2399
2396
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2400
2397
  try log.boost(ident, .SpecialAttack, n);
2401
2398
  try log.boost(ident, .SpecialDefense, n);
@@ -2422,11 +2419,7 @@ pub const Effects = struct {
2422
2419
  }
2423
2420
 
2424
2421
  if (move.effect.isStatDownChance()) {
2425
- const chance = if (showdown)
2426
- battle.rng.chance(u8, 85, 256)
2427
- else
2428
- battle.rng.next() < Gen12.percent(33) + 1;
2429
- if (!chance or foe.active.volatiles.Invulnerable) return;
2422
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2430
2423
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2431
2424
  return; // checkHit already checks for Invulnerable
2432
2425
  }
@@ -2448,7 +2441,7 @@ pub const Effects = struct {
2448
2441
  return try log.fail(foe_ident, .None);
2449
2442
  }
2450
2443
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2451
- const stat = unmodifiedStats(battle, player.foe()).atk;
2444
+ const stat = unmodifiedStats(battle, foe).atk;
2452
2445
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2453
2446
  try log.boost(foe_ident, .Attack, -1);
2454
2447
  },
@@ -2466,7 +2459,7 @@ pub const Effects = struct {
2466
2459
  return try log.fail(foe_ident, .None);
2467
2460
  }
2468
2461
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2469
- const stat = unmodifiedStats(battle, player.foe()).def;
2462
+ const stat = unmodifiedStats(battle, foe).def;
2470
2463
  stats.def = @max(1, stat * mod[0] / mod[1]);
2471
2464
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2472
2465
  },
@@ -2483,7 +2476,7 @@ pub const Effects = struct {
2483
2476
  return try log.fail(foe_ident, .None);
2484
2477
  }
2485
2478
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2486
- const stat = unmodifiedStats(battle, player.foe()).spe;
2479
+ const stat = unmodifiedStats(battle, foe).spe;
2487
2480
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2488
2481
  try log.boost(foe_ident, .Speed, -1);
2489
2482
  assert(boosts.spe >= -6);
@@ -2503,7 +2496,7 @@ pub const Effects = struct {
2503
2496
  return try log.fail(foe_ident, .None);
2504
2497
  }
2505
2498
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2506
- const stat = unmodifiedStats(battle, player.foe()).spc;
2499
+ const stat = unmodifiedStats(battle, foe).spc;
2507
2500
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2508
2501
  try log.boost(foe_ident, .SpecialAttack, -1);
2509
2502
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2522,8 +2515,7 @@ pub const Effects = struct {
2522
2515
  }
2523
2516
  };
2524
2517
 
2525
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
- const side = battle.side(who);
2518
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2527
2519
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2528
2520
  const id = ID.from(side.active.volatiles.transform);
2529
2521
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2589,75 +2581,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2589
2581
  }
2590
2582
  }
2591
2583
 
2592
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2593
- if (side.active.volatiles.disabled_move != 0) {
2594
- // A Pokémon that transforms after being disabled may end up with less move slots
2595
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2596
- if (m != .None and m == side.last_selected_move) {
2597
- side.active.volatiles.Charging = false;
2598
- try log.disabled(ident, side.last_selected_move);
2599
- return true;
2584
+ pub const Rolls = struct {
2585
+ inline fn speedTie(battle: anytype) bool {
2586
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2587
+ return battle.rng.next() < Gen12.percent(50) + 1;
2588
+ }
2589
+
2590
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2591
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2592
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2593
+ }
2594
+
2595
+ inline fn damage(battle: anytype) u8 {
2596
+ if (showdown) return battle.rng.range(u8, 217, 256);
2597
+ while (true) {
2598
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2599
+ if (r >= 217) return r;
2600
2600
  }
2601
2601
  }
2602
- return false;
2603
- }
2604
2602
 
2605
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2606
- const side = battle.side(who);
2607
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2608
- try Effects.boost(battle, who, Move.get(.Rage), log);
2603
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2604
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2605
+ return battle.rng.next() < accuracy;
2609
2606
  }
2610
- }
2611
2607
 
2612
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
- var volatiles = &active.volatiles;
2614
- assert(volatiles.Thrashing);
2615
- if (volatiles.attacks > 0) return false;
2608
+ inline fn confusionDuration(battle: anytype) u3 {
2609
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2610
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2611
+ }
2616
2612
 
2617
- volatiles.Thrashing = false;
2618
- volatiles.Confusion = true;
2619
- volatiles.confusion = @intCast(u3, if (showdown)
2620
- battle.rng.range(u8, 2, 6)
2621
- else
2622
- (battle.rng.next() & 3) + 2);
2613
+ inline fn confused(battle: anytype) bool {
2614
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2615
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2616
+ }
2623
2617
 
2624
- return true;
2625
- }
2618
+ inline fn paralyzed(battle: anytype) bool {
2619
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2620
+ return battle.rng.next() < Gen12.percent(25);
2621
+ }
2626
2622
 
2627
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2623
+ inline fn confusionChance(battle: anytype) bool {
2624
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2625
+ return battle.rng.next() < Gen12.percent(10);
2626
+ }
2628
2627
 
2629
- fn distribution(battle: anytype) u3 {
2630
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2631
- const r = (battle.rng.next() & 3);
2632
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2633
- }
2628
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2629
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2630
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2631
+ }
2634
2632
 
2635
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2636
- if (showdown) {
2637
- if (check_pp == 0) {
2638
- var i: usize = moves.len;
2639
- while (i > 0) {
2640
- i -= 1;
2641
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2642
- }
2643
- } else {
2644
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2645
- var i: usize = 0;
2646
- while (i < moves.len and r > 0) : (i += 1) {
2647
- if (moves[i].pp > 0) {
2648
- r -= 1;
2649
- if (r == 0) break;
2633
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2634
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2635
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2636
+ }
2637
+
2638
+ inline fn sleepDuration(battle: anytype) u3 {
2639
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2640
+ while (true) {
2641
+ const r = battle.rng.next() & 7;
2642
+ if (r != 0) return @intCast(u3, r);
2643
+ }
2644
+ }
2645
+
2646
+ inline fn bideThrashDuration(battle: anytype) u3 {
2647
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2648
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2649
+ }
2650
+
2651
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2652
+ if (showdown) {
2653
+ if (check_pp == 0) {
2654
+ var i: usize = moves.len;
2655
+ while (i > 0) {
2656
+ i -= 1;
2657
+ if (moves[i].id != .None) {
2658
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2659
+ }
2660
+ }
2661
+ } else {
2662
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2663
+ var i: usize = 0;
2664
+ while (i < moves.len and r > 0) : (i += 1) {
2665
+ if (moves[i].pp > 0) {
2666
+ r -= 1;
2667
+ if (r == 0) break;
2668
+ }
2650
2669
  }
2670
+ return @intCast(u4, i + 1);
2651
2671
  }
2652
- return @intCast(u4, i + 1);
2672
+ }
2673
+
2674
+ while (true) {
2675
+ const r = @intCast(u4, battle.rng.next() & 3);
2676
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2653
2677
  }
2654
2678
  }
2655
2679
 
2656
- while (true) {
2657
- const r = @intCast(u4, rand.next() & 3);
2658
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2680
+ inline fn disableDuration(battle: anytype) u4 {
2681
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2682
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2659
2683
  }
2660
- }
2684
+
2685
+ inline fn metronome(battle: anytype) Move {
2686
+ if (showdown) {
2687
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2688
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2689
+ return @intToEnum(Move, r + mod);
2690
+ }
2691
+ while (true) {
2692
+ const r = battle.rng.next();
2693
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2694
+ if (r >= @enumToInt(Move.Struggle)) continue;
2695
+ return @intToEnum(Move, r);
2696
+ }
2697
+ }
2698
+
2699
+ inline fn psywave(battle: anytype, max: u8) u8 {
2700
+ if (showdown) return battle.rng.range(u8, 0, max);
2701
+ while (true) {
2702
+ const r = battle.rng.next();
2703
+ if (r < max) return r;
2704
+ }
2705
+ }
2706
+
2707
+ inline fn distribution(battle: anytype) u3 {
2708
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2709
+ const r = (battle.rng.next() & 3);
2710
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2711
+ }
2712
+
2713
+ inline fn unboost(battle: anytype) bool {
2714
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2715
+ return battle.rng.next() < Gen12.percent(33) + 1;
2716
+ }
2717
+ };
2661
2718
 
2662
2719
  test "RNG agreement" {
2663
2720
  if (!showdown) return;
@@ -2757,7 +2814,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2757
2814
  while (slot <= 4) : (slot += 1) {
2758
2815
  const m = active.moves[slot - 1];
2759
2816
  if (m.id == .None) break;
2760
- if (m.id == side.last_selected_move) {
2817
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2761
2818
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2762
2819
  const struggle =
2763
2820
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);