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

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;
@@ -347,8 +372,18 @@ fn doTurn(
347
372
  if (try checkFaint(battle, player, log)) |r| return r;
348
373
  } else if (foe_choice.type == .Pass) return null;
349
374
 
375
+ residual = true;
350
376
  replace = battle.side(foe_player).stored().hp == 0;
351
- 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;
352
387
  if (!replace) {
353
388
  if (foe_choice.type != .Switch) {
354
389
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +406,8 @@ fn executeMove(
371
406
  battle: anytype,
372
407
  player: Player,
373
408
  choice: Choice,
409
+ rewrap: bool,
410
+ skip: bool,
374
411
  from: ?Move,
375
412
  residual: *bool,
376
413
  log: anytype,
@@ -382,9 +419,10 @@ fn executeMove(
382
419
  return null;
383
420
  }
384
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
385
425
  if (side.last_selected_move == .SKIP_TURN) {
386
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
- // Binding instead gets handled in beforeMove after sleep and freeze
388
426
  assert(!showdown);
389
427
  if (battle.foe(player).active.volatiles.Binding) {
390
428
  try log.cant(battle.active(player), .Bound);
@@ -434,7 +472,7 @@ fn executeMove(
434
472
 
435
473
  var skip_can = false;
436
474
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
475
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
476
  .done => return null,
439
477
  .skip_can => skip_can = true,
440
478
  .skip_pp => skip_pp = true,
@@ -442,14 +480,23 @@ fn executeMove(
442
480
  .err => return @as(?Result, Result.Error),
443
481
  }
444
482
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
483
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
484
+ return null;
485
+ }
446
486
 
447
- return doMove(battle, player, mslot, auto, residual, log);
487
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
448
488
  }
449
489
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
490
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
491
 
452
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
492
+ fn beforeMove(
493
+ battle: anytype,
494
+ player: Player,
495
+ skip: bool,
496
+ from: ?Move,
497
+ residual: *bool,
498
+ log: anytype,
499
+ ) !BeforeMove {
453
500
  var side = battle.side(player);
454
501
  const foe = battle.foe(player);
455
502
  var active = &side.active;
@@ -477,7 +524,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
477
524
  return .done;
478
525
  }
479
526
 
480
- if (foe.active.volatiles.Binding) {
527
+ if (showdown and volatiles.Recharging) {
528
+ volatiles.Recharging = false;
529
+ try log.cant(ident, .Recharge);
530
+ return .done;
531
+ }
532
+
533
+ if (!showdown and foe.active.volatiles.Binding) {
481
534
  try log.cant(ident, .Bound);
482
535
  return .done;
483
536
  }
@@ -488,7 +541,7 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
488
541
  return .done;
489
542
  }
490
543
 
491
- if (volatiles.Recharging) {
544
+ if (!showdown and volatiles.Recharging) {
492
545
  volatiles.Recharging = false;
493
546
  try log.cant(ident, .Recharge);
494
547
  return .done;
@@ -504,9 +557,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
557
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
558
  if (showdown and try disabled(side, ident, log)) return .done;
506
559
 
507
- // Pokémon Showdown checks for Flinch *after* instead of before
560
+ if (showdown and skip or foe.active.volatiles.Binding) {
561
+ try log.cant(ident, .Bound);
562
+ return .done;
563
+ }
564
+
565
+ // Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
566
+ // until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
567
+ // from the same original hit
508
568
  if (showdown and volatiles.Flinch) {
509
- volatiles.Flinch = false;
510
569
  try log.cant(ident, .Flinch);
511
570
  return .done;
512
571
  }
@@ -528,14 +587,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
587
  } else {
529
588
  try log.activate(ident, .Confusion);
530
589
 
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) {
590
+ if (Rolls.confused(battle)) {
537
591
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
592
+ if (!volatiles.Rage) volatiles.state = 0;
539
593
  volatiles.Bide = false;
540
594
  volatiles.Thrashing = false;
541
595
  volatiles.MultiHit = false;
@@ -559,8 +613,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
613
  player
560
614
  else
561
615
  player.foe();
616
+
617
+ const uncapped = battle.last_damage;
562
618
  // Skipping adjustDamage / randomizeDamage / checkHit
563
619
  _ = try applyDamage(battle, player, target, .Confusion, log);
620
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
621
+ if (showdown) battle.last_damage = uncapped;
564
622
 
565
623
  return .done;
566
624
  }
@@ -569,22 +627,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
627
 
570
628
  if (!showdown and try disabled(side, ident, log)) return .done;
571
629
 
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
- }
630
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
631
+ if (!volatiles.Rage) volatiles.state = 0;
632
+ volatiles.Bide = false;
633
+ volatiles.Thrashing = false;
634
+ volatiles.Charging = false;
635
+ volatiles.Binding = false;
636
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
637
+ try log.cant(ident, .Paralysis);
638
+ return .done;
588
639
  }
589
640
 
590
641
  if (volatiles.Bide) {
@@ -611,7 +662,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
662
  return .done;
612
663
  }
613
664
 
665
+ const sub = showdown and foe.active.volatiles.Substitute;
614
666
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
667
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
668
+
669
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
670
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
671
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
672
+ if (showdown) {
673
+ residual.* = false;
674
+ try handleResidual(battle, player, log);
675
+ }
676
+
615
677
  return .done;
616
678
  }
617
679
 
@@ -623,8 +685,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
623
685
  try log.start(battle.active(player), .ConfusionSilent);
624
686
  }
625
687
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
626
- if (showdown and handleThrashing(battle, active)) {
627
- try log.start(battle.active(player), .ConfusionSilent);
688
+ if (showdown) {
689
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
+ // used in useMove and doesn't have the notion of skipping canMove semantics
691
+ side.last_used_move = side.last_selected_move;
692
+ if (handleThrashing(battle, active)) {
693
+ try log.start(battle.active(player), .ConfusionSilent);
694
+ }
628
695
  }
629
696
  return .skip_can;
630
697
  }
@@ -639,7 +706,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
639
706
  side.last_selected_move,
640
707
  );
641
708
  if (showdown or battle.last_damage != 0) {
709
+ const sub = showdown and foe.active.volatiles.Substitute;
642
710
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
711
+ if (battle.foe(player).stored().hp > 0 and !sub) {
712
+ try buildRage(battle, player.foe(), log);
713
+ }
643
714
  }
644
715
  return .done;
645
716
  }
@@ -654,6 +725,7 @@ fn canMove(
654
725
  auto: bool,
655
726
  skip_pp: bool,
656
727
  from: ?Move,
728
+ residual: *bool,
657
729
  log: anytype,
658
730
  ) !bool {
659
731
  var side = battle.side(player);
@@ -688,7 +760,7 @@ fn canMove(
688
760
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
689
761
 
690
762
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
763
+ try onBegin(battle, player, move, mslot, residual, log);
692
764
  return false;
693
765
  }
694
766
 
@@ -696,7 +768,7 @@ fn canMove(
696
768
  Effects.thrashing(battle, player);
697
769
  } else if (!showdown and move.effect == .Binding) {
698
770
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
699
- Effects.binding(battle, player);
771
+ Effects.binding(battle, player, false);
700
772
  }
701
773
 
702
774
  return true;
@@ -737,6 +809,7 @@ fn doMove(
737
809
  battle: anytype,
738
810
  player: Player,
739
811
  mslot: u4,
812
+ rewrap: bool,
740
813
  auto: bool,
741
814
  residual: *bool,
742
815
  log: anytype,
@@ -767,16 +840,21 @@ fn doMove(
767
840
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
841
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
842
  if (immune and move.effect != .Binding) break :miss true;
770
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
843
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
844
+ battle.last_damage = 0;
845
+ break :miss true;
846
+ }
771
847
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
848
  };
773
849
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
850
 
851
+ var late = showdown and move.effect != .Explode;
775
852
  const skip = status or immune;
776
853
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
854
  if (showdown and move.effect.isMulti()) {
778
855
  Effects.multiHit(battle, player, move);
779
856
  hits = side.active.volatiles.attacks;
857
+ late = false;
780
858
  }
781
859
 
782
860
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +888,7 @@ fn doMove(
810
888
  }
811
889
 
812
890
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
813
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
891
+ if (showdown and skip) battle.last_damage = 0;
814
892
 
815
893
  miss = if (showdown or skip)
816
894
  miss
@@ -823,9 +901,9 @@ fn doMove(
823
901
 
824
902
  if (!showdown or !miss) {
825
903
  if (move.effect == .MirrorMove) {
826
- return mirrorMove(battle, player, mslot, auto, residual, log);
904
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
827
905
  } else if (move.effect == .Metronome) {
828
- return metronome(battle, player, mslot, auto, residual, log);
906
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
829
907
  } else if (move.effect.onEnd()) {
830
908
  try onEnd(battle, player, move, log);
831
909
  return null;
@@ -840,7 +918,7 @@ fn doMove(
840
918
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
841
919
  if (ohko) {
842
920
  try log.immune(foe_ident, .OHKO);
843
- } else if (immune and !invulnerable) {
921
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
844
922
  try log.immune(foe_ident, .None);
845
923
  } else if (mist) {
846
924
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -857,15 +935,9 @@ fn doMove(
857
935
  if (showdown and side.stored().hp == 0) residual.* = false;
858
936
  } else if (move.effect == .Explode) {
859
937
  try Effects.explode(battle, player);
860
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
861
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
862
- }
863
- } else if (showdown) {
864
- if (move.effect == .Disable) {
865
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
866
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
867
- }
868
- }
938
+ try buildRage(battle, player.foe(), log);
939
+ } else if (showdown and move.effect == .Disable) {
940
+ try buildRage(battle, player.foe(), log);
869
941
  }
870
942
  return null;
871
943
  }
@@ -878,6 +950,9 @@ fn doMove(
878
950
  hits = side.active.volatiles.attacks;
879
951
  }
880
952
 
953
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
954
+ const sub = showdown and foe.active.volatiles.Substitute;
955
+
881
956
  var nullified = false;
882
957
  var hit: u4 = 0;
883
958
  while (hit < hits) {
@@ -893,9 +968,7 @@ fn doMove(
893
968
  if (hit == 0 and ohko) try log.ohko();
894
969
  hit += 1;
895
970
  if (foe.stored().hp == 0) break;
896
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
897
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
898
- }
971
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
972
  // If the substitute breaks during a multi-hit attack, the attack ends
900
973
  if (nullified) break;
901
974
  }
@@ -911,7 +984,7 @@ fn doMove(
911
984
  } else if (showdown) {
912
985
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
913
986
  if (move.effect == .Binding) {
914
- Effects.binding(battle, player);
987
+ Effects.binding(battle, player, rewrap);
915
988
  if (immune) {
916
989
  battle.last_damage = 0;
917
990
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -932,6 +1005,9 @@ fn doMove(
932
1005
 
933
1006
  if (foe.stored().hp == 0) return null;
934
1007
 
1008
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1009
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1010
+
935
1011
  if (!move.effect.isSpecial()) {
936
1012
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
1013
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +1019,9 @@ fn doMove(
943
1019
  if (move.effect == .Twineedle) {
944
1020
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
1021
  } else if (move.effect == .Disable) {
946
- return Effects.disable(battle, player, move, log);
1022
+ const result = try Effects.disable(battle, player, move, log);
1023
+ if (showdown) try buildRage(battle, player.foe(), log);
1024
+ return result;
947
1025
  } else {
948
1026
  try moveEffect(battle, player, move, log);
949
1027
  }
@@ -961,8 +1039,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
1039
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
1040
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
1041
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1042
+ return Rolls.criticalHit(battle, chance);
966
1043
  }
967
1044
 
968
1045
  fn calcDamage(
@@ -1066,49 +1143,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1143
 
1067
1144
  fn randomizeDamage(battle: anytype) void {
1068
1145
  if (battle.last_damage <= 1) return;
1069
-
1070
- const random = if (showdown)
1071
- battle.rng.range(u8, 217, 256)
1072
- else loop: {
1073
- while (true) {
1074
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1075
- if (r >= 217) break :loop r;
1076
- }
1077
- };
1078
-
1146
+ const random = Rolls.damage(battle);
1079
1147
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1148
  }
1081
1149
 
1082
1150
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1151
  const side = battle.side(player);
1152
+ const foe = battle.foe(player);
1084
1153
 
1085
1154
  if (!try checkHit(battle, player, move, log)) return null;
1086
1155
 
1087
1156
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1157
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1158
  .SeismicToss, .NightShade => side.stored().level,
1090
1159
  .SonicBoom => 20,
1091
1160
  .DragonRage => 40,
1092
1161
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1162
  .Psywave => power: {
1094
1163
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1095
- if (showdown) {
1096
- break :power battle.rng.range(u8, 0, max);
1097
- } else {
1098
- // GLITCH: Psywave infinite glitch loop
1099
- if (max <= 1) return Result.Error;
1100
- while (true) {
1101
- const r = battle.rng.next();
1102
- if (r < max) break :power r;
1103
- }
1104
- }
1164
+ // GLITCH: Psywave infinite glitch loop
1165
+ if (!showdown and max <= 1) return Result.Error;
1166
+ break :power Rolls.psywave(battle, max);
1105
1167
  },
1106
1168
  else => unreachable,
1107
1169
  };
1108
1170
 
1109
1171
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1172
 
1173
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1174
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1175
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1176
+
1112
1177
  return null;
1113
1178
  }
1114
1179
 
@@ -1156,7 +1221,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1156
1221
  // Pokémon Showdown calls checkHit before Counter
1157
1222
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1158
1223
 
1224
+ const sub = showdown and foe.active.volatiles.Substitute;
1159
1225
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1226
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1160
1227
  return null;
1161
1228
  }
1162
1229
 
@@ -1200,6 +1267,7 @@ fn mirrorMove(
1200
1267
  battle: anytype,
1201
1268
  player: Player,
1202
1269
  mslot: u4,
1270
+ rewrap: bool,
1203
1271
  auto: bool,
1204
1272
  residual: *bool,
1205
1273
  log: anytype,
@@ -1216,37 +1284,26 @@ fn mirrorMove(
1216
1284
 
1217
1285
  incrementPP(side, mslot);
1218
1286
 
1219
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1220
- return doMove(battle, player, mslot, auto, residual, log);
1287
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1288
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1221
1289
  }
1222
1290
 
1223
1291
  fn metronome(
1224
1292
  battle: anytype,
1225
1293
  player: Player,
1226
1294
  mslot: u4,
1295
+ rewrap: bool,
1227
1296
  auto: bool,
1228
1297
  residual: *bool,
1229
1298
  log: anytype,
1230
1299
  ) !?Result {
1231
1300
  var side = battle.side(player);
1232
1301
 
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
-
1302
+ side.last_selected_move = Rolls.metronome(battle);
1246
1303
  incrementPP(side, mslot);
1247
1304
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
- return doMove(battle, player, mslot, auto, residual, log);
1305
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1306
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
1307
  }
1251
1308
 
1252
1309
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1278,7 +1335,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1278
1335
  if (move.effect == .Swift) return true;
1279
1336
  if (foe.active.volatiles.Invulnerable) break :miss true;
1280
1337
  }
1281
- if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1338
+ if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1339
+ (showdown and foe.active.volatiles.Substitute)))
1340
+ {
1282
1341
  immune.* = true;
1283
1342
  if (showdown) return false;
1284
1343
  break :miss true;
@@ -1298,23 +1357,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1298
1357
  }
1299
1358
 
1300
1359
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
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));
1360
+ const state = side.active.volatiles.state;
1361
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1362
+ const overwritten = overwrite and state > 0;
1363
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1364
+
1365
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1305
1366
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1306
1367
  accuracy = accuracy * boost[0] / boost[1];
1307
1368
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1308
1369
  accuracy = accuracy * boost[0] / boost[1];
1309
1370
  accuracy = @min(255, @max(1, accuracy));
1310
1371
 
1311
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1372
+ // Pokémon Showdown only overwrites if the volatile is present
1373
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1374
+ if (overwrite) side.active.volatiles.state = accuracy;
1312
1375
 
1313
1376
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1314
- break :miss if (showdown)
1315
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1316
- else
1317
- battle.rng.next() >= accuracy;
1377
+ break :miss !Rolls.hit(battle, accuracy);
1318
1378
  };
1319
1379
 
1320
1380
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1429,6 +1489,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1429
1489
  }
1430
1490
 
1431
1491
  if (volatiles.LeechSeed) {
1492
+ var foe = battle.foe(player);
1493
+ var foe_stored = foe.stored();
1494
+ const foe_ident = battle.active(player.foe());
1495
+
1496
+ if (foe_stored.hp == 0) {
1497
+ assert(showdown);
1498
+ return;
1499
+ }
1500
+
1432
1501
  var damage = @max(stored.stats.hp / 16, 1);
1433
1502
 
1434
1503
  // GLITCH: Leech Seed + Toxic glitch
@@ -1440,12 +1509,12 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1440
1509
  const amount = @min(damage, stored.hp);
1441
1510
  stored.hp -= amount;
1442
1511
 
1443
- var foe = battle.foe(player);
1444
- var foe_stored = foe.stored();
1445
- const foe_ident = battle.active(player.foe());
1446
-
1447
1512
  // As above, Pokémon Showdown uses damageOf but its not relevant
1448
- if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1513
+ if (amount > 0) {
1514
+ try log.damage(ident, stored, .LeechSeed);
1515
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1516
+ if (showdown) battle.last_damage = damage;
1517
+ }
1449
1518
 
1450
1519
  const before = foe_stored.hp;
1451
1520
  // Uncapped damage is added back to the foe
@@ -1480,16 +1549,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1480
1549
 
1481
1550
  fn checkEBC(battle: anytype) bool {
1482
1551
  for (battle.sides, 0..) |side, i| {
1552
+ const foe = battle.sides[~@intCast(u1, i)];
1553
+
1483
1554
  var foe_all_ghosts = true;
1484
1555
  var foe_all_transform = true;
1556
+ for (foe.order, 0..) |id, j| {
1557
+ if (id == 0) break;
1558
+ const active = j == 0;
1559
+ const pokemon = foe.pokemon[id - 1];
1485
1560
 
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);
1561
+ const ghost = pokemon.hp == 0 or
1562
+ (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1490
1563
  foe_all_ghosts = foe_all_ghosts and ghost;
1491
1564
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1492
- for (pokemon.moves) |m| {
1565
+ for (if (active) foe.active.moves else pokemon.moves) |m| {
1493
1566
  if (m.id == .None) break :transform true;
1494
1567
  if (m.id != .Transform) break :transform false;
1495
1568
  }
@@ -1497,10 +1570,14 @@ fn checkEBC(battle: anytype) bool {
1497
1570
  };
1498
1571
  }
1499
1572
 
1500
- for (side.pokemon) |pokemon| {
1573
+ for (side.order, 0..) |id, j| {
1574
+ if (id == 0) break;
1575
+ const active = j == 0;
1576
+ const pokemon = side.pokemon[id - 1];
1577
+
1501
1578
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1502
1579
  const transform = foe_all_transform and transform: {
1503
- for (pokemon.moves) |m| {
1580
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1504
1581
  if (m.id == .None) break :transform true;
1505
1582
  if (m.id != .Transform) break :transform false;
1506
1583
  }
@@ -1508,7 +1585,7 @@ fn checkEBC(battle: anytype) bool {
1508
1585
  };
1509
1586
  if (transform) continue;
1510
1587
  const no_pp = foe_all_ghosts and no_pp: {
1511
- for (pokemon.moves) |m| {
1588
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1512
1589
  if (m.pp != 0) break :no_pp false;
1513
1590
  }
1514
1591
  break :no_pp true;
@@ -1524,7 +1601,50 @@ fn checkEBC(battle: anytype) bool {
1524
1601
  return false;
1525
1602
  }
1526
1603
 
1527
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1604
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1605
+ if (side.active.volatiles.disabled_move != 0) {
1606
+ // A Pokémon that transforms after being disabled may end up with less move slots
1607
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1608
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1609
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1610
+ m
1611
+ else
1612
+ side.last_selected_move;
1613
+ if (m != .None and m == last) {
1614
+ side.active.volatiles.Charging = false;
1615
+ try log.disabled(ident, last);
1616
+ return true;
1617
+ }
1618
+ }
1619
+ return false;
1620
+ }
1621
+
1622
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1623
+ const side = battle.side(who);
1624
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1625
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1626
+ }
1627
+ }
1628
+
1629
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1630
+ var volatiles = &active.volatiles;
1631
+ assert(volatiles.Thrashing);
1632
+ if (volatiles.attacks > 0) return false;
1633
+
1634
+ volatiles.Thrashing = false;
1635
+ volatiles.Confusion = true;
1636
+ volatiles.confusion = Rolls.confusionDuration(battle);
1637
+ return true;
1638
+ }
1639
+
1640
+ inline fn onBegin(
1641
+ battle: anytype,
1642
+ player: Player,
1643
+ move: Move.Data,
1644
+ mslot: u8,
1645
+ residual: *bool,
1646
+ log: anytype,
1647
+ ) !void {
1528
1648
  assert(move.effect.onBegin());
1529
1649
  return switch (move.effect) {
1530
1650
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1540,7 +1660,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1540
1660
  .Poison => Effects.poison(battle, player, move, log),
1541
1661
  .Reflect => Effects.reflect(battle, player, log),
1542
1662
  .Splash => Effects.splash(battle, player, log),
1543
- .Substitute => Effects.substitute(battle, player, log),
1663
+ .Substitute => Effects.substitute(battle, player, residual, log),
1544
1664
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1545
1665
  .Transform => Effects.transform(battle, player, log),
1546
1666
  else => unreachable,
@@ -1607,10 +1727,7 @@ pub const Effects = struct {
1607
1727
  side.active.volatiles.Bide = true;
1608
1728
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1609
1729
  side.active.volatiles.state = 0;
1610
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1611
- battle.rng.range(u4, 2, 4)
1612
- else
1613
- (battle.rng.next() & 1) + 2);
1730
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1614
1731
 
1615
1732
  try log.start(battle.active(player), .Bide);
1616
1733
  }
@@ -1632,14 +1749,8 @@ pub const Effects = struct {
1632
1749
  return;
1633
1750
  }
1634
1751
 
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;
1752
+ if (foe.active.types.includes(move.type)) return;
1753
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1643
1754
 
1644
1755
  foe_stored.status = Status.init(.BRN);
1645
1756
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1663,11 +1774,7 @@ pub const Effects = struct {
1663
1774
  const sub = foe.active.volatiles.Substitute;
1664
1775
 
1665
1776
  if (move.effect == .ConfusionChance) {
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;
1777
+ if (!Rolls.confusionChance(battle)) return;
1671
1778
  } else {
1672
1779
  if (showdown) {
1673
1780
  if (!try checkHit(battle, player, move, log)) {
@@ -1686,10 +1793,7 @@ pub const Effects = struct {
1686
1793
 
1687
1794
  if (foe.active.volatiles.Confusion) return;
1688
1795
  foe.active.volatiles.Confusion = true;
1689
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1690
- battle.rng.range(u8, 2, 6)
1691
- else
1692
- (battle.rng.next() & 3) + 2);
1796
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1693
1797
 
1694
1798
  try log.start(battle.active(player.foe()), .Confusion);
1695
1799
  }
@@ -1739,11 +1843,8 @@ pub const Effects = struct {
1739
1843
  return Result.Error;
1740
1844
  }
1741
1845
 
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);
1846
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1847
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1747
1848
 
1748
1849
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1749
1850
  return null;
@@ -1753,6 +1854,11 @@ pub const Effects = struct {
1753
1854
  var side = battle.side(player);
1754
1855
  var stored = side.stored();
1755
1856
 
1857
+ if (battle.last_damage == 0) {
1858
+ assert(showdown);
1859
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1860
+ }
1861
+
1756
1862
  const drain = @max(battle.last_damage / 2, 1);
1757
1863
  battle.last_damage = drain;
1758
1864
 
@@ -1777,15 +1883,7 @@ pub const Effects = struct {
1777
1883
  var volatiles = &battle.foe(player).active.volatiles;
1778
1884
 
1779
1885
  if (volatiles.Substitute) 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;
1886
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1789
1887
 
1790
1888
  volatiles.Flinch = true;
1791
1889
  volatiles.Recharging = false;
@@ -1811,12 +1909,8 @@ pub const Effects = struct {
1811
1909
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1812
1910
  }
1813
1911
 
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
-
1912
+ if (foe.active.types.includes(move.type)) return;
1913
+ if (!Rolls.secondaryChance(battle, true)) return;
1820
1914
  // Freeze Clause Mod
1821
1915
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1822
1916
 
@@ -1961,7 +2055,7 @@ pub const Effects = struct {
1961
2055
  }
1962
2056
  if (!try checkHit(battle, player, move, log)) return;
1963
2057
 
1964
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2058
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1965
2059
  side.active.move(oslot).id = foe.active.move(rslot).id;
1966
2060
 
1967
2061
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1982,7 +2076,8 @@ pub const Effects = struct {
1982
2076
  assert(!side.active.volatiles.MultiHit);
1983
2077
  side.active.volatiles.MultiHit = true;
1984
2078
 
1985
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2079
+ side.active.volatiles.attacks =
2080
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1986
2081
  }
1987
2082
 
1988
2083
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2026,14 +2121,8 @@ pub const Effects = struct {
2026
2121
  }
2027
2122
 
2028
2123
  // Body Slam can't paralyze a Normal type Pokémon
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;
2124
+ if (foe.active.types.includes(move.type)) return;
2125
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2037
2126
 
2038
2127
  foe_stored.status = Status.init(.PAR);
2039
2128
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2063,7 +2152,7 @@ pub const Effects = struct {
2063
2152
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2064
2153
  if (!showdown)
2065
2154
  .Poison
2066
- else if (toxic == foe.active.volatiles.Toxic)
2155
+ else if (toxic == (foe_stored.status == Status.TOX))
2067
2156
  if (toxic) .Toxic else .Poison
2068
2157
  else
2069
2158
  .None
@@ -2076,16 +2165,7 @@ pub const Effects = struct {
2076
2165
 
2077
2166
  if (move.effect == .Poison) {
2078
2167
  if (!showdown and !try checkHit(battle, player, move, log)) 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
- }
2168
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2089
2169
 
2090
2170
  foe_stored.status = Status.init(.PSN);
2091
2171
  if (toxic) {
@@ -2099,10 +2179,8 @@ pub const Effects = struct {
2099
2179
 
2100
2180
  fn rage(battle: anytype, player: Player) !void {
2101
2181
  var volatiles = &battle.side(player).active.volatiles;
2102
- if (volatiles.Rage) return;
2103
- volatiles.Rage = true;
2104
2182
  assert(!volatiles.Bide);
2105
- volatiles.state = 0;
2183
+ volatiles.Rage = true;
2106
2184
  }
2107
2185
 
2108
2186
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2154,16 +2232,7 @@ pub const Effects = struct {
2154
2232
  }
2155
2233
  foe.active.volatiles.Recharging = false;
2156
2234
 
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);
2235
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2167
2236
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2168
2237
  }
2169
2238
 
@@ -2171,7 +2240,7 @@ pub const Effects = struct {
2171
2240
  try log.activate(battle.active(player), .Splash);
2172
2241
  }
2173
2242
 
2174
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2243
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2175
2244
  var side = battle.side(player);
2176
2245
  if (side.active.volatiles.Substitute) {
2177
2246
  try log.fail(battle.active(player), .Substitute);
@@ -2196,14 +2265,18 @@ pub const Effects = struct {
2196
2265
  side.active.volatiles.substitute = hp + 1;
2197
2266
  side.active.volatiles.Substitute = true;
2198
2267
  try log.start(battle.active(player), .Substitute);
2199
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2268
+ if (hp > 0) {
2269
+ try log.damage(battle.active(player), side.stored(), .None);
2270
+ if (showdown and side.stored().hp == 0) residual.* = false;
2271
+ }
2200
2272
  }
2201
2273
 
2202
2274
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2203
2275
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2204
2276
 
2205
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2277
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2206
2278
  _ = try checkHit(battle, player, move, log);
2279
+ battle.last_damage = 0;
2207
2280
  }
2208
2281
 
2209
2282
  fn thrashing(battle: anytype, player: Player) void {
@@ -2212,11 +2285,7 @@ pub const Effects = struct {
2212
2285
  assert(!volatiles.Bide);
2213
2286
 
2214
2287
  volatiles.Thrashing = true;
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);
2288
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2220
2289
  }
2221
2290
 
2222
2291
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2248,7 +2317,7 @@ pub const Effects = struct {
2248
2317
  try log.transform(battle.active(player), foe_ident);
2249
2318
  }
2250
2319
 
2251
- fn binding(battle: anytype, player: Player) void {
2320
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2252
2321
  var side = battle.side(player);
2253
2322
  var foe = battle.foe(player);
2254
2323
 
@@ -2256,9 +2325,11 @@ pub const Effects = struct {
2256
2325
  side.active.volatiles.Binding = true;
2257
2326
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2258
2327
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2259
- if (!showdown) foe.active.volatiles.Recharging = false;
2328
+ if (!showdown) {
2329
+ foe.active.volatiles.Recharging = false;
2330
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2260
2331
 
2261
- side.active.volatiles.attacks = distribution(battle) - 1;
2332
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2262
2333
  }
2263
2334
 
2264
2335
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2285,9 +2356,11 @@ pub const Effects = struct {
2285
2356
  return try log.fail(ident, .None);
2286
2357
  }
2287
2358
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2288
- const stat = unmodifiedStats(battle, player).atk;
2359
+ const stat = unmodifiedStats(battle, side).atk;
2289
2360
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2290
2361
  try log.boost(ident, reason, n);
2362
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2363
+ if (showdown and move.effect == .Rage) return;
2291
2364
  },
2292
2365
  .DefenseUp1, .DefenseUp2 => {
2293
2366
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2303,7 +2376,7 @@ pub const Effects = struct {
2303
2376
  return try log.fail(ident, .None);
2304
2377
  }
2305
2378
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2306
- const stat = unmodifiedStats(battle, player).def;
2379
+ const stat = unmodifiedStats(battle, side).def;
2307
2380
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2308
2381
  try log.boost(ident, .Defense, n);
2309
2382
  },
@@ -2320,7 +2393,7 @@ pub const Effects = struct {
2320
2393
  return try log.fail(ident, .None);
2321
2394
  }
2322
2395
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2323
- const stat = unmodifiedStats(battle, player).spe;
2396
+ const stat = unmodifiedStats(battle, side).spe;
2324
2397
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2325
2398
  try log.boost(ident, .Speed, 2);
2326
2399
  },
@@ -2340,7 +2413,7 @@ pub const Effects = struct {
2340
2413
  return try log.fail(ident, .None);
2341
2414
  }
2342
2415
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2343
- const stat = unmodifiedStats(battle, player).spc;
2416
+ const stat = unmodifiedStats(battle, side).spc;
2344
2417
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2418
  try log.boost(ident, .SpecialAttack, n);
2346
2419
  try log.boost(ident, .SpecialDefense, n);
@@ -2367,11 +2440,7 @@ pub const Effects = struct {
2367
2440
  }
2368
2441
 
2369
2442
  if (move.effect.isStatDownChance()) {
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;
2443
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2375
2444
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2376
2445
  return; // checkHit already checks for Invulnerable
2377
2446
  }
@@ -2393,7 +2462,7 @@ pub const Effects = struct {
2393
2462
  return try log.fail(foe_ident, .None);
2394
2463
  }
2395
2464
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2396
- const stat = unmodifiedStats(battle, player.foe()).atk;
2465
+ const stat = unmodifiedStats(battle, foe).atk;
2397
2466
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2398
2467
  try log.boost(foe_ident, .Attack, -1);
2399
2468
  },
@@ -2411,7 +2480,7 @@ pub const Effects = struct {
2411
2480
  return try log.fail(foe_ident, .None);
2412
2481
  }
2413
2482
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2414
- const stat = unmodifiedStats(battle, player.foe()).def;
2483
+ const stat = unmodifiedStats(battle, foe).def;
2415
2484
  stats.def = @max(1, stat * mod[0] / mod[1]);
2416
2485
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2417
2486
  },
@@ -2428,7 +2497,7 @@ pub const Effects = struct {
2428
2497
  return try log.fail(foe_ident, .None);
2429
2498
  }
2430
2499
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2431
- const stat = unmodifiedStats(battle, player.foe()).spe;
2500
+ const stat = unmodifiedStats(battle, foe).spe;
2432
2501
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2433
2502
  try log.boost(foe_ident, .Speed, -1);
2434
2503
  assert(boosts.spe >= -6);
@@ -2448,7 +2517,7 @@ pub const Effects = struct {
2448
2517
  return try log.fail(foe_ident, .None);
2449
2518
  }
2450
2519
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2451
- const stat = unmodifiedStats(battle, player.foe()).spc;
2520
+ const stat = unmodifiedStats(battle, foe).spc;
2452
2521
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2453
2522
  try log.boost(foe_ident, .SpecialAttack, -1);
2454
2523
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2467,8 +2536,7 @@ pub const Effects = struct {
2467
2536
  }
2468
2537
  };
2469
2538
 
2470
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
- const side = battle.side(who);
2539
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2472
2540
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2473
2541
  const id = ID.from(side.active.volatiles.transform);
2474
2542
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2482,7 +2550,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2482
2550
  }
2483
2551
  }
2484
2552
 
2485
- fn isForced(active: ActivePokemon) bool {
2553
+ inline fn isForced(active: ActivePokemon) bool {
2486
2554
  return active.volatiles.Recharging or active.volatiles.Rage or
2487
2555
  active.volatiles.Thrashing or active.volatiles.Charging;
2488
2556
  }
@@ -2514,13 +2582,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2514
2582
  volatiles.LeechSeed = false;
2515
2583
  try log.end(ident, .LeechSeed);
2516
2584
  }
2517
- if (volatiles.Toxic) {
2585
+ if (!showdown and volatiles.Toxic) {
2518
2586
  volatiles.Toxic = false;
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
- }
2587
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2524
2588
  }
2525
2589
  if (volatiles.LightScreen) {
2526
2590
  volatiles.LightScreen = false;
@@ -2530,70 +2594,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2530
2594
  volatiles.Reflect = false;
2531
2595
  try log.end(ident, .Reflect);
2532
2596
  }
2597
+ if (showdown and volatiles.Toxic) {
2598
+ volatiles.Toxic = false;
2599
+ // Pokémon Showdown erroneously clears the toxic counter
2600
+ volatiles.toxic = 0;
2601
+ try log.end(ident, .Toxic);
2602
+ }
2533
2603
  }
2534
2604
 
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;
2605
+ pub const Rolls = struct {
2606
+ inline fn speedTie(battle: anytype) bool {
2607
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2608
+ return battle.rng.next() < Gen12.percent(50) + 1;
2609
+ }
2610
+
2611
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2612
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2613
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2614
+ }
2615
+
2616
+ inline fn damage(battle: anytype) u8 {
2617
+ if (showdown) return battle.rng.range(u8, 217, 256);
2618
+ while (true) {
2619
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2620
+ if (r >= 217) return r;
2543
2621
  }
2544
2622
  }
2545
- return false;
2546
- }
2547
2623
 
2548
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
- var volatiles = &active.volatiles;
2550
- assert(volatiles.Thrashing);
2551
- if (volatiles.attacks > 0) return false;
2624
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2625
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2626
+ return battle.rng.next() < accuracy;
2627
+ }
2552
2628
 
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);
2629
+ inline fn confusionDuration(battle: anytype) u3 {
2630
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2631
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2632
+ }
2559
2633
 
2560
- return true;
2561
- }
2634
+ inline fn confused(battle: anytype) bool {
2635
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2636
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2637
+ }
2562
2638
 
2563
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2639
+ inline fn paralyzed(battle: anytype) bool {
2640
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2641
+ return battle.rng.next() < Gen12.percent(25);
2642
+ }
2564
2643
 
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
- }
2644
+ inline fn confusionChance(battle: anytype) bool {
2645
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2646
+ return battle.rng.next() < Gen12.percent(10);
2647
+ }
2570
2648
 
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;
2649
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2650
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2651
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2652
+ }
2653
+
2654
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2655
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2656
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2657
+ }
2658
+
2659
+ inline fn sleepDuration(battle: anytype) u3 {
2660
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2661
+ while (true) {
2662
+ const r = battle.rng.next() & 7;
2663
+ if (r != 0) return @intCast(u3, r);
2664
+ }
2665
+ }
2666
+
2667
+ inline fn bideThrashDuration(battle: anytype) u3 {
2668
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2669
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2670
+ }
2671
+
2672
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2673
+ if (showdown) {
2674
+ if (check_pp == 0) {
2675
+ var i: usize = moves.len;
2676
+ while (i > 0) {
2677
+ i -= 1;
2678
+ if (moves[i].id != .None) {
2679
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2680
+ }
2681
+ }
2682
+ } else {
2683
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2684
+ var i: usize = 0;
2685
+ while (i < moves.len and r > 0) : (i += 1) {
2686
+ if (moves[i].pp > 0) {
2687
+ r -= 1;
2688
+ if (r == 0) break;
2689
+ }
2586
2690
  }
2691
+ return @intCast(u4, i + 1);
2587
2692
  }
2588
- return @intCast(u4, i + 1);
2693
+ }
2694
+
2695
+ while (true) {
2696
+ const r = @intCast(u4, battle.rng.next() & 3);
2697
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2589
2698
  }
2590
2699
  }
2591
2700
 
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;
2701
+ inline fn disableDuration(battle: anytype) u4 {
2702
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2703
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2595
2704
  }
2596
- }
2705
+
2706
+ inline fn metronome(battle: anytype) Move {
2707
+ if (showdown) {
2708
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2709
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2710
+ return @intToEnum(Move, r + mod);
2711
+ }
2712
+ while (true) {
2713
+ const r = battle.rng.next();
2714
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2715
+ if (r >= @enumToInt(Move.Struggle)) continue;
2716
+ return @intToEnum(Move, r);
2717
+ }
2718
+ }
2719
+
2720
+ inline fn psywave(battle: anytype, max: u8) u8 {
2721
+ if (showdown) return battle.rng.range(u8, 0, max);
2722
+ while (true) {
2723
+ const r = battle.rng.next();
2724
+ if (r < max) return r;
2725
+ }
2726
+ }
2727
+
2728
+ inline fn distribution(battle: anytype) u3 {
2729
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2730
+ const r = (battle.rng.next() & 3);
2731
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2732
+ }
2733
+
2734
+ inline fn unboost(battle: anytype) bool {
2735
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2736
+ return battle.rng.next() < Gen12.percent(33) + 1;
2737
+ }
2738
+ };
2597
2739
 
2598
2740
  test "RNG agreement" {
2599
2741
  if (!showdown) return;
@@ -2693,9 +2835,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2693
2835
  while (slot <= 4) : (slot += 1) {
2694
2836
  const m = active.moves[slot - 1];
2695
2837
  if (m.id == .None) break;
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;
2838
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2839
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2840
+ const struggle =
2841
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2842
+ const s = if (struggle) 0 else slot;
2699
2843
  out[n] = .{ .type = .Move, .data = s };
2700
2844
  n += 1;
2701
2845
  return n;