@pkmn/engine 0.1.0-dev.47feedd9 → 0.1.0-dev.53335a32

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,34 +56,30 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
-
61
59
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
60
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
61
  if (battle.turn == 0) return start(battle, log);
64
62
 
65
- var s1 = false;
66
- var s2 = false;
67
63
  var f1: ?Move = null;
68
64
  var f2: ?Move = null;
69
65
 
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;
66
+ if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
+ if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
78
68
 
79
69
  if (turnOrder(battle, c1, c2) == .P1) {
80
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
70
+ if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
81
71
  } else {
82
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
72
+ if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
83
73
  }
84
74
 
85
- if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
- if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
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
+ }
87
83
 
88
84
  return endTurn(battle, log);
89
85
  }
@@ -116,7 +112,6 @@ fn selectMove(
116
112
  player: Player,
117
113
  choice: Choice,
118
114
  foe_choice: Choice,
119
- skip_turn: *bool,
120
115
  from: *?Move,
121
116
  ) ?Result {
122
117
  if (choice.type == .Pass) return null;
@@ -132,20 +127,14 @@ fn selectMove(
132
127
  if (volatiles.Recharging) return null;
133
128
  if (volatiles.Rage) {
134
129
  from.* = side.last_used_move;
135
- if (showdown) {
136
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
137
- saveMove(battle, player, null);
138
- }
130
+ if (showdown) saveMove(battle, player, null);
139
131
  return null;
140
132
  }
141
133
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
142
134
  if (!showdown) volatiles.Flinch = false;
143
135
  if (volatiles.Thrashing or volatiles.Charging) {
144
136
  from.* = side.last_selected_move;
145
- if (showdown) {
146
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
- saveMove(battle, player, null);
148
- }
137
+ if (showdown) saveMove(battle, player, null);
149
138
  return null;
150
139
  }
151
140
 
@@ -155,10 +144,7 @@ fn selectMove(
155
144
  // pre-move select
156
145
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
157
146
  assert(showdown or choice.data == 0);
158
- if (showdown) {
159
- if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
160
- saveMove(battle, player, choice);
161
- }
147
+ if (showdown) saveMove(battle, player, choice);
162
148
  return null;
163
149
  }
164
150
  if (volatiles.Binding) {
@@ -189,7 +175,6 @@ fn selectMove(
189
175
  }
190
176
 
191
177
  if (battle.foe(player).active.volatiles.Binding) {
192
- skip_turn.* = true;
193
178
  if (showdown) {
194
179
  saveMove(battle, player, choice);
195
180
  } else {
@@ -200,7 +185,6 @@ fn selectMove(
200
185
  }
201
186
 
202
187
  // move select
203
- volatiles.state = 0;
204
188
  if (choice.data == 0) {
205
189
  const struggle = ok: {
206
190
  for (side.active.moves, 0..) |move, i| {
@@ -317,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
317
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
318
302
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
319
303
 
320
- const p1 = Rolls.speedTie(battle);
304
+ const p1 = if (showdown)
305
+ battle.rng.range(u8, 0, 2) == 0
306
+ else
307
+ battle.rng.next() < Gen12.percent(50) + 1;
308
+
321
309
  if (!showdown) return if (p1) .P1 else .P2;
322
310
 
323
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -327,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
327
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
328
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
329
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
330
- // that out and enforce host ordering of events, so P1 just goes first regardless of who
318
+ // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
331
319
  // won the original coin flip
332
320
  if (t1 and t2) return .P1;
333
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -340,13 +328,9 @@ fn doTurn(
340
328
  battle: anytype,
341
329
  player: Player,
342
330
  player_choice: Choice,
343
- player_rewrap: bool,
344
- player_skip: bool,
345
331
  player_from: ?Move,
346
332
  foe_player: Player,
347
333
  foe_choice: Choice,
348
- foe_rewrap: bool,
349
- foe_skip: bool,
350
334
  foe_from: ?Move,
351
335
  log: anytype,
352
336
  ) !?Result {
@@ -354,16 +338,7 @@ fn doTurn(
354
338
 
355
339
  var residual = true;
356
340
  var replace = battle.side(player).stored().hp == 0;
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;
341
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
367
342
  if (!replace) {
368
343
  if (player_choice.type != .Switch) {
369
344
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -374,16 +349,7 @@ fn doTurn(
374
349
 
375
350
  residual = true;
376
351
  replace = battle.side(foe_player).stored().hp == 0;
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
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
387
353
  if (!replace) {
388
354
  if (foe_choice.type != .Switch) {
389
355
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -406,8 +372,6 @@ fn executeMove(
406
372
  battle: anytype,
407
373
  player: Player,
408
374
  choice: Choice,
409
- rewrap: bool,
410
- skip: bool,
411
375
  from: ?Move,
412
376
  residual: *bool,
413
377
  log: anytype,
@@ -419,10 +383,9 @@ fn executeMove(
419
383
  return null;
420
384
  }
421
385
 
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
425
386
  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
426
389
  assert(!showdown);
427
390
  if (battle.foe(player).active.volatiles.Binding) {
428
391
  try log.cant(battle.active(player), .Bound);
@@ -472,7 +435,7 @@ fn executeMove(
472
435
 
473
436
  var skip_can = false;
474
437
  var skip_pp = false;
475
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
+ switch (try beforeMove(battle, player, from, residual, log)) {
476
439
  .done => return null,
477
440
  .skip_can => skip_can = true,
478
441
  .skip_pp => skip_pp = true,
@@ -484,15 +447,14 @@ fn executeMove(
484
447
  return null;
485
448
  }
486
449
 
487
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
+ return doMove(battle, player, mslot, auto, residual, log);
488
451
  }
489
452
 
490
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
491
454
 
492
455
  fn beforeMove(
493
456
  battle: anytype,
494
457
  player: Player,
495
- skip: bool,
496
458
  from: ?Move,
497
459
  residual: *bool,
498
460
  log: anytype,
@@ -524,15 +486,13 @@ fn beforeMove(
524
486
  return .done;
525
487
  }
526
488
 
527
- if (skip or foe.active.volatiles.Binding) {
489
+ if (foe.active.volatiles.Binding) {
528
490
  try log.cant(ident, .Bound);
529
491
  return .done;
530
492
  }
531
493
 
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;
494
+ if (!showdown and volatiles.Flinch) {
495
+ volatiles.Flinch = false;
536
496
  try log.cant(ident, .Flinch);
537
497
  return .done;
538
498
  }
@@ -553,6 +513,13 @@ fn beforeMove(
553
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
554
514
  if (showdown and try disabled(side, ident, log)) return .done;
555
515
 
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
+
556
523
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
557
524
  // before the side had a selected a move - we simply need to assume this leads to a desync
558
525
  if (side.last_selected_move == .None) {
@@ -570,7 +537,12 @@ fn beforeMove(
570
537
  } else {
571
538
  try log.activate(ident, .Confusion);
572
539
 
573
- if (Rolls.confused(battle)) {
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) {
574
546
  assert(!volatiles.MultiHit);
575
547
  if (!volatiles.Rage) volatiles.state = 0;
576
548
  volatiles.Bide = false;
@@ -596,12 +568,8 @@ fn beforeMove(
596
568
  player
597
569
  else
598
570
  player.foe();
599
-
600
- const uncapped = battle.last_damage;
601
571
  // Skipping adjustDamage / randomizeDamage / checkHit
602
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
603
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
604
- if (showdown) battle.last_damage = uncapped;
605
573
 
606
574
  return .done;
607
575
  }
@@ -610,15 +578,22 @@ fn beforeMove(
610
578
 
611
579
  if (!showdown and try disabled(side, ident, log)) return .done;
612
580
 
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;
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
+ }
622
597
  }
623
598
 
624
599
  if (volatiles.Bide) {
@@ -645,9 +620,8 @@ fn beforeMove(
645
620
  return .done;
646
621
  }
647
622
 
648
- const sub = showdown and foe.active.volatiles.Substitute;
649
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
650
- if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
624
+ if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
651
625
 
652
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
653
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -668,13 +642,8 @@ fn beforeMove(
668
642
  try log.start(battle.active(player), .ConfusionSilent);
669
643
  }
670
644
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
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
- }
645
+ if (showdown and handleThrashing(battle, active)) {
646
+ try log.start(battle.active(player), .ConfusionSilent);
678
647
  }
679
648
  return .skip_can;
680
649
  }
@@ -689,11 +658,7 @@ fn beforeMove(
689
658
  side.last_selected_move,
690
659
  );
691
660
  if (showdown or battle.last_damage != 0) {
692
- const sub = showdown and foe.active.volatiles.Substitute;
693
661
  _ = 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
- }
697
662
  }
698
663
  return .done;
699
664
  }
@@ -751,7 +716,7 @@ fn canMove(
751
716
  Effects.thrashing(battle, player);
752
717
  } else if (!showdown and move.effect == .Binding) {
753
718
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
754
- Effects.binding(battle, player, false);
719
+ Effects.binding(battle, player);
755
720
  }
756
721
 
757
722
  return true;
@@ -792,7 +757,6 @@ fn doMove(
792
757
  battle: anytype,
793
758
  player: Player,
794
759
  mslot: u4,
795
- rewrap: bool,
796
760
  auto: bool,
797
761
  residual: *bool,
798
762
  log: anytype,
@@ -823,15 +787,12 @@ fn doMove(
823
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
824
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
825
789
  if (immune and move.effect != .Binding) 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
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
830
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
831
792
  };
832
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
833
794
 
834
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
835
796
  const skip = status or immune;
836
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
837
798
  if (showdown and move.effect.isMulti()) {
@@ -884,9 +845,9 @@ fn doMove(
884
845
 
885
846
  if (!showdown or !miss) {
886
847
  if (move.effect == .MirrorMove) {
887
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
848
+ return mirrorMove(battle, player, mslot, auto, residual, log);
888
849
  } else if (move.effect == .Metronome) {
889
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
850
+ return metronome(battle, player, mslot, auto, residual, log);
890
851
  } else if (move.effect.onEnd()) {
891
852
  try onEnd(battle, player, move, log);
892
853
  return null;
@@ -901,7 +862,7 @@ fn doMove(
901
862
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
902
863
  if (ohko) {
903
864
  try log.immune(foe_ident, .OHKO);
904
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
865
+ } else if (immune and !invulnerable) {
905
866
  try log.immune(foe_ident, .None);
906
867
  } else if (mist) {
907
868
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -933,9 +894,6 @@ fn doMove(
933
894
  hits = side.active.volatiles.attacks;
934
895
  }
935
896
 
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
-
939
897
  var nullified = false;
940
898
  var hit: u4 = 0;
941
899
  while (hit < hits) {
@@ -951,7 +909,7 @@ fn doMove(
951
909
  if (hit == 0 and ohko) try log.ohko();
952
910
  hit += 1;
953
911
  if (foe.stored().hp == 0) break;
954
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
955
913
  // If the substitute breaks during a multi-hit attack, the attack ends
956
914
  if (nullified) break;
957
915
  }
@@ -967,7 +925,7 @@ fn doMove(
967
925
  } else if (showdown) {
968
926
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
969
927
  if (move.effect == .Binding) {
970
- Effects.binding(battle, player, rewrap);
928
+ Effects.binding(battle, player);
971
929
  if (immune) {
972
930
  battle.last_damage = 0;
973
931
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -988,8 +946,8 @@ fn doMove(
988
946
 
989
947
  if (foe.stored().hp == 0) return null;
990
948
 
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);
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);
993
951
 
994
952
  if (!move.effect.isSpecial()) {
995
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1022,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1022
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1023
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1024
982
 
1025
- return Rolls.criticalHit(battle, chance);
983
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1026
985
  }
1027
986
 
1028
987
  fn calcDamage(
@@ -1126,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1126
1085
 
1127
1086
  fn randomizeDamage(battle: anytype) void {
1128
1087
  if (battle.last_damage <= 1) return;
1129
- const random = Rolls.damage(battle);
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
+
1130
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1131
1099
  }
1132
1100
 
1133
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1134
1102
  const side = battle.side(player);
1135
- const foe = battle.foe(player);
1136
1103
 
1137
1104
  if (!try checkHit(battle, player, move, log)) return null;
1138
1105
 
1139
1106
  battle.last_damage = switch (side.last_selected_move) {
1140
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1141
1108
  .SeismicToss, .NightShade => side.stored().level,
1142
1109
  .SonicBoom => 20,
1143
1110
  .DragonRage => 40,
1144
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1145
1112
  .Psywave => power: {
1146
1113
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1147
- // GLITCH: Psywave infinite glitch loop
1148
- if (!showdown and max <= 1) return Result.Error;
1149
- break :power Rolls.psywave(battle, max);
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
+ }
1150
1124
  },
1151
1125
  else => unreachable,
1152
1126
  };
1153
1127
 
1154
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1155
1129
 
1156
- const sub = showdown and foe.active.volatiles.Substitute;
1157
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1158
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1159
1132
 
1160
1133
  return null;
1161
1134
  }
@@ -1204,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1204
1177
  // Pokémon Showdown calls checkHit before Counter
1205
1178
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1206
1179
 
1207
- const sub = showdown and foe.active.volatiles.Substitute;
1208
1180
  _ = 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);
1210
1181
  return null;
1211
1182
  }
1212
1183
 
@@ -1250,7 +1221,6 @@ fn mirrorMove(
1250
1221
  battle: anytype,
1251
1222
  player: Player,
1252
1223
  mslot: u4,
1253
- rewrap: bool,
1254
1224
  auto: bool,
1255
1225
  residual: *bool,
1256
1226
  log: anytype,
@@ -1263,30 +1233,41 @@ fn mirrorMove(
1263
1233
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1264
1234
  try log.fail(battle.active(player), .None);
1265
1235
  return null;
1266
- } else if (!showdown or foe.last_used_move != .Struggle) {
1267
- incrementPP(side, mslot);
1268
1236
  }
1269
1237
 
1238
+ incrementPP(side, mslot);
1239
+
1270
1240
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1271
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1241
+ return doMove(battle, player, mslot, auto, residual, log);
1272
1242
  }
1273
1243
 
1274
1244
  fn metronome(
1275
1245
  battle: anytype,
1276
1246
  player: Player,
1277
1247
  mslot: u4,
1278
- rewrap: bool,
1279
1248
  auto: bool,
1280
1249
  residual: *bool,
1281
1250
  log: anytype,
1282
1251
  ) !?Result {
1283
1252
  var side = battle.side(player);
1284
1253
 
1285
- side.last_selected_move = Rolls.metronome(battle);
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
+
1286
1267
  incrementPP(side, mslot);
1287
1268
 
1288
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1289
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1270
+ return doMove(battle, player, mslot, auto, residual, log);
1290
1271
  }
1291
1272
 
1292
1273
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1340,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1340
1321
  }
1341
1322
 
1342
1323
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
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));
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));
1349
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1350
1329
  accuracy = accuracy * boost[0] / boost[1];
1351
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1352
1331
  accuracy = accuracy * boost[0] / boost[1];
1353
1332
  accuracy = @min(255, @max(1, accuracy));
1354
1333
 
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;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1358
1335
 
1359
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1360
- break :miss !Rolls.hit(battle, accuracy);
1337
+ break :miss if (showdown)
1338
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
+ else
1340
+ battle.rng.next() >= accuracy;
1361
1341
  };
1362
1342
 
1363
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1489,6 +1469,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1489
1469
  damage *= volatiles.toxic;
1490
1470
  }
1491
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1492
1475
  const amount = @min(damage, stored.hp);
1493
1476
  stored.hp -= amount;
1494
1477
 
@@ -1580,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1580
1563
  return false;
1581
1564
  }
1582
1565
 
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
-
1619
1566
  inline fn onBegin(
1620
1567
  battle: anytype,
1621
1568
  player: Player,
@@ -1706,7 +1653,10 @@ pub const Effects = struct {
1706
1653
  side.active.volatiles.Bide = true;
1707
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1708
1655
  side.active.volatiles.state = 0;
1709
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1656
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
+ battle.rng.range(u4, 2, 4)
1658
+ else
1659
+ (battle.rng.next() & 1) + 2);
1710
1660
 
1711
1661
  try log.start(battle.active(player), .Bide);
1712
1662
  }
@@ -1728,8 +1678,14 @@ pub const Effects = struct {
1728
1678
  return;
1729
1679
  }
1730
1680
 
1731
- if (foe.active.types.includes(move.type)) return;
1732
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
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;
1733
1689
 
1734
1690
  foe_stored.status = Status.init(.BRN);
1735
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1753,7 +1709,11 @@ pub const Effects = struct {
1753
1709
  const sub = foe.active.volatiles.Substitute;
1754
1710
 
1755
1711
  if (move.effect == .ConfusionChance) {
1756
- if (!Rolls.confusionChance(battle)) return;
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;
1757
1717
  } else {
1758
1718
  if (showdown) {
1759
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1772,7 +1732,10 @@ pub const Effects = struct {
1772
1732
 
1773
1733
  if (foe.active.volatiles.Confusion) return;
1774
1734
  foe.active.volatiles.Confusion = true;
1775
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1735
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
+ battle.rng.range(u8, 2, 6)
1737
+ else
1738
+ (battle.rng.next() & 3) + 2);
1776
1739
 
1777
1740
  try log.start(battle.active(player.foe()), .Confusion);
1778
1741
  }
@@ -1822,8 +1785,11 @@ pub const Effects = struct {
1822
1785
  return Result.Error;
1823
1786
  }
1824
1787
 
1825
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1826
- volatiles.disabled_duration = Rolls.disableDuration(battle);
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);
1827
1793
 
1828
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1829
1795
  return null;
@@ -1835,7 +1801,7 @@ pub const Effects = struct {
1835
1801
 
1836
1802
  if (battle.last_damage == 0) {
1837
1803
  assert(showdown);
1838
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1839
1805
  }
1840
1806
 
1841
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1862,7 +1828,15 @@ pub const Effects = struct {
1862
1828
  var volatiles = &battle.foe(player).active.volatiles;
1863
1829
 
1864
1830
  if (volatiles.Substitute) return;
1865
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) 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;
1866
1840
 
1867
1841
  volatiles.Flinch = true;
1868
1842
  volatiles.Recharging = false;
@@ -1888,8 +1862,12 @@ pub const Effects = struct {
1888
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1889
1863
  }
1890
1864
 
1891
- if (foe.active.types.includes(move.type)) return;
1892
- if (!Rolls.secondaryChance(battle, true)) return;
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
+
1893
1871
  // Freeze Clause Mod
1894
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1895
1873
 
@@ -2026,15 +2004,15 @@ pub const Effects = struct {
2026
2004
  }
2027
2005
  break :has_mimic false;
2028
2006
  };
2029
- if (!has_mimic) {
2030
- // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2031
- if (!try checkHit(battle, player, move, log)) return;
2007
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2008
+ // trigger |-miss| instead of |-fail|
2009
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2032
2010
  return try log.fail(battle.active(player.foe()), .None);
2033
2011
  }
2034
2012
  }
2035
2013
  if (!try checkHit(battle, player, move, log)) return;
2036
2014
 
2037
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2038
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
2039
2017
 
2040
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2055,8 +2033,7 @@ pub const Effects = struct {
2055
2033
  assert(!side.active.volatiles.MultiHit);
2056
2034
  side.active.volatiles.MultiHit = true;
2057
2035
 
2058
- side.active.volatiles.attacks =
2059
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2060
2037
  }
2061
2038
 
2062
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2100,8 +2077,14 @@ pub const Effects = struct {
2100
2077
  }
2101
2078
 
2102
2079
  // Body Slam can't paralyze a Normal type Pokémon
2103
- if (foe.active.types.includes(move.type)) return;
2104
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
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;
2105
2088
 
2106
2089
  foe_stored.status = Status.init(.PAR);
2107
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2144,7 +2127,16 @@ pub const Effects = struct {
2144
2127
 
2145
2128
  if (move.effect == .Poison) {
2146
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2147
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) 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
+ }
2148
2140
 
2149
2141
  foe_stored.status = Status.init(.PSN);
2150
2142
  if (toxic) {
@@ -2158,8 +2150,10 @@ pub const Effects = struct {
2158
2150
 
2159
2151
  fn rage(battle: anytype, player: Player) !void {
2160
2152
  var volatiles = &battle.side(player).active.volatiles;
2161
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2162
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2163
2157
  }
2164
2158
 
2165
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2211,7 +2205,16 @@ pub const Effects = struct {
2211
2205
  }
2212
2206
  foe.active.volatiles.Recharging = false;
2213
2207
 
2214
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
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);
2215
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2216
2219
  }
2217
2220
 
@@ -2264,7 +2267,11 @@ pub const Effects = struct {
2264
2267
  assert(!volatiles.Bide);
2265
2268
 
2266
2269
  volatiles.Thrashing = true;
2267
- volatiles.attacks = Rolls.bideThrashDuration(battle);
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);
2268
2275
  }
2269
2276
 
2270
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2296,7 +2303,7 @@ pub const Effects = struct {
2296
2303
  try log.transform(battle.active(player), foe_ident);
2297
2304
  }
2298
2305
 
2299
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2306
+ fn binding(battle: anytype, player: Player) void {
2300
2307
  var side = battle.side(player);
2301
2308
  var foe = battle.foe(player);
2302
2309
 
@@ -2304,11 +2311,9 @@ pub const Effects = struct {
2304
2311
  side.active.volatiles.Binding = true;
2305
2312
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2306
2313
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2307
- if (!showdown) {
2308
- foe.active.volatiles.Recharging = false;
2309
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2314
+ if (!showdown) foe.active.volatiles.Recharging = false;
2310
2315
 
2311
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2312
2317
  }
2313
2318
 
2314
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2335,11 +2340,9 @@ pub const Effects = struct {
2335
2340
  return try log.fail(ident, .None);
2336
2341
  }
2337
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2338
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2339
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2340
2345
  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;
2343
2346
  },
2344
2347
  .DefenseUp1, .DefenseUp2 => {
2345
2348
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2355,7 +2358,7 @@ pub const Effects = struct {
2355
2358
  return try log.fail(ident, .None);
2356
2359
  }
2357
2360
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2358
- const stat = unmodifiedStats(battle, side).def;
2361
+ const stat = unmodifiedStats(battle, player).def;
2359
2362
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2363
  try log.boost(ident, .Defense, n);
2361
2364
  },
@@ -2372,7 +2375,7 @@ pub const Effects = struct {
2372
2375
  return try log.fail(ident, .None);
2373
2376
  }
2374
2377
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2375
- const stat = unmodifiedStats(battle, side).spe;
2378
+ const stat = unmodifiedStats(battle, player).spe;
2376
2379
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2377
2380
  try log.boost(ident, .Speed, 2);
2378
2381
  },
@@ -2392,7 +2395,7 @@ pub const Effects = struct {
2392
2395
  return try log.fail(ident, .None);
2393
2396
  }
2394
2397
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2395
- const stat = unmodifiedStats(battle, side).spc;
2398
+ const stat = unmodifiedStats(battle, player).spc;
2396
2399
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2400
  try log.boost(ident, .SpecialAttack, n);
2398
2401
  try log.boost(ident, .SpecialDefense, n);
@@ -2419,7 +2422,11 @@ pub const Effects = struct {
2419
2422
  }
2420
2423
 
2421
2424
  if (move.effect.isStatDownChance()) {
2422
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
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;
2423
2430
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2424
2431
  return; // checkHit already checks for Invulnerable
2425
2432
  }
@@ -2441,7 +2448,7 @@ pub const Effects = struct {
2441
2448
  return try log.fail(foe_ident, .None);
2442
2449
  }
2443
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2444
- const stat = unmodifiedStats(battle, foe).atk;
2451
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2445
2452
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2446
2453
  try log.boost(foe_ident, .Attack, -1);
2447
2454
  },
@@ -2459,7 +2466,7 @@ pub const Effects = struct {
2459
2466
  return try log.fail(foe_ident, .None);
2460
2467
  }
2461
2468
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2462
- const stat = unmodifiedStats(battle, foe).def;
2469
+ const stat = unmodifiedStats(battle, player.foe()).def;
2463
2470
  stats.def = @max(1, stat * mod[0] / mod[1]);
2464
2471
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2465
2472
  },
@@ -2476,7 +2483,7 @@ pub const Effects = struct {
2476
2483
  return try log.fail(foe_ident, .None);
2477
2484
  }
2478
2485
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2479
- const stat = unmodifiedStats(battle, foe).spe;
2486
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2480
2487
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2481
2488
  try log.boost(foe_ident, .Speed, -1);
2482
2489
  assert(boosts.spe >= -6);
@@ -2496,7 +2503,7 @@ pub const Effects = struct {
2496
2503
  return try log.fail(foe_ident, .None);
2497
2504
  }
2498
2505
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2499
- const stat = unmodifiedStats(battle, foe).spc;
2506
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2500
2507
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2501
2508
  try log.boost(foe_ident, .SpecialAttack, -1);
2502
2509
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2515,7 +2522,8 @@ pub const Effects = struct {
2515
2522
  }
2516
2523
  };
2517
2524
 
2518
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2525
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
+ const side = battle.side(who);
2519
2527
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2520
2528
  const id = ID.from(side.active.volatiles.transform);
2521
2529
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2581,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2581
2589
  }
2582
2590
  }
2583
2591
 
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;
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;
2600
2600
  }
2601
2601
  }
2602
+ return false;
2603
+ }
2602
2604
 
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;
2606
- }
2607
-
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
- }
2612
-
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
- }
2617
-
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);
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);
2621
2609
  }
2610
+ }
2622
2611
 
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
- }
2612
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
+ var volatiles = &active.volatiles;
2614
+ assert(volatiles.Thrashing);
2615
+ if (volatiles.attacks > 0) return false;
2627
2616
 
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
- }
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);
2632
2623
 
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
- }
2624
+ return true;
2625
+ }
2637
2626
 
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
- }
2627
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2645
2628
 
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
- }
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
+ }
2650
2634
 
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
- }
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;
2669
2650
  }
2670
- return @intCast(u4, i + 1);
2671
2651
  }
2652
+ return @intCast(u4, i + 1);
2672
2653
  }
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;
2677
- }
2678
- }
2679
-
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);
2683
- }
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
2654
  }
2712
2655
 
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;
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;
2716
2659
  }
2717
- };
2660
+ }
2718
2661
 
2719
2662
  test "RNG agreement" {
2720
2663
  if (!showdown) return;
@@ -2814,11 +2757,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2814
2757
  while (slot <= 4) : (slot += 1) {
2815
2758
  const m = active.moves[slot - 1];
2816
2759
  if (m.id == .None) break;
2817
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2818
- // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2819
- const struggle =
2820
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2821
- const s = if (struggle) 0 else slot;
2760
+ if (m.id == side.last_selected_move) {
2761
+ // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2762
+ const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2822
2763
  out[n] = .{ .type = .Move, .data = s };
2823
2764
  n += 1;
2824
2765
  return n;