@pkmn/engine 0.1.0-dev.b9fb6eab → 0.1.0-dev.cf6f5d2c

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;
@@ -124,17 +129,26 @@ fn selectMove(
124
129
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
125
130
 
126
131
  // pre-battle menu
127
- if (volatiles.Recharging) return null;
132
+ if (volatiles.Recharging) {
133
+ if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
+ return null;
135
+ }
128
136
  if (volatiles.Rage) {
129
137
  from.* = side.last_used_move;
130
- if (showdown) saveMove(battle, player, null);
138
+ if (showdown) {
139
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
+ saveMove(battle, player, null);
141
+ }
131
142
  return null;
132
143
  }
133
144
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
134
145
  if (!showdown) volatiles.Flinch = false;
135
146
  if (volatiles.Thrashing or volatiles.Charging) {
136
147
  from.* = side.last_selected_move;
137
- if (showdown) saveMove(battle, player, null);
148
+ if (showdown) {
149
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
+ saveMove(battle, player, null);
151
+ }
138
152
  return null;
139
153
  }
140
154
 
@@ -144,7 +158,10 @@ fn selectMove(
144
158
  // pre-move select
145
159
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
146
160
  assert(showdown or choice.data == 0);
147
- if (showdown) saveMove(battle, player, choice);
161
+ if (showdown) {
162
+ if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
+ saveMove(battle, player, choice);
164
+ }
148
165
  return null;
149
166
  }
150
167
  if (volatiles.Binding) {
@@ -175,6 +192,7 @@ fn selectMove(
175
192
  }
176
193
 
177
194
  if (battle.foe(player).active.volatiles.Binding) {
195
+ skip_turn.* = true;
178
196
  if (showdown) {
179
197
  saveMove(battle, player, choice);
180
198
  } else {
@@ -185,6 +203,7 @@ fn selectMove(
185
203
  }
186
204
 
187
205
  // move select
206
+ volatiles.state = 0;
188
207
  if (choice.data == 0) {
189
208
  const struggle = ok: {
190
209
  for (side.active.moves, 0..) |move, i| {
@@ -301,11 +320,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
321
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
322
 
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
308
-
323
+ const p1 = Rolls.speedTie(battle);
309
324
  if (!showdown) return if (p1) .P1 else .P2;
310
325
 
311
326
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +330,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
330
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
331
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
332
  // 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
333
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
334
  // won the original coin flip
320
335
  if (t1 and t2) return .P1;
321
336
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +343,13 @@ fn doTurn(
328
343
  battle: anytype,
329
344
  player: Player,
330
345
  player_choice: Choice,
346
+ player_rewrap: bool,
347
+ player_skip: bool,
331
348
  player_from: ?Move,
332
349
  foe_player: Player,
333
350
  foe_choice: Choice,
351
+ foe_rewrap: bool,
352
+ foe_skip: bool,
334
353
  foe_from: ?Move,
335
354
  log: anytype,
336
355
  ) !?Result {
@@ -338,7 +357,16 @@ fn doTurn(
338
357
 
339
358
  var residual = true;
340
359
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
360
+ if (try executeMove(
361
+ battle,
362
+ player,
363
+ player_choice,
364
+ player_rewrap,
365
+ player_skip,
366
+ player_from,
367
+ &residual,
368
+ log,
369
+ )) |r| return r;
342
370
  if (!replace) {
343
371
  if (player_choice.type != .Switch) {
344
372
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -349,7 +377,16 @@ fn doTurn(
349
377
 
350
378
  residual = true;
351
379
  replace = battle.side(foe_player).stored().hp == 0;
352
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
380
+ if (try executeMove(
381
+ battle,
382
+ foe_player,
383
+ foe_choice,
384
+ foe_rewrap,
385
+ foe_skip,
386
+ foe_from,
387
+ &residual,
388
+ log,
389
+ )) |r| return r;
353
390
  if (!replace) {
354
391
  if (foe_choice.type != .Switch) {
355
392
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -372,6 +409,8 @@ fn executeMove(
372
409
  battle: anytype,
373
410
  player: Player,
374
411
  choice: Choice,
412
+ rewrap: bool,
413
+ skip: bool,
375
414
  from: ?Move,
376
415
  residual: *bool,
377
416
  log: anytype,
@@ -383,9 +422,10 @@ fn executeMove(
383
422
  return null;
384
423
  }
385
424
 
425
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
426
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
427
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
386
428
  if (side.last_selected_move == .SKIP_TURN) {
387
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
388
- // Binding instead gets handled in beforeMove after sleep and freeze
389
429
  assert(!showdown);
390
430
  if (battle.foe(player).active.volatiles.Binding) {
391
431
  try log.cant(battle.active(player), .Bound);
@@ -435,7 +475,7 @@ fn executeMove(
435
475
 
436
476
  var skip_can = false;
437
477
  var skip_pp = false;
438
- switch (try beforeMove(battle, player, from, residual, log)) {
478
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
439
479
  .done => return null,
440
480
  .skip_can => skip_can = true,
441
481
  .skip_pp => skip_pp = true,
@@ -447,14 +487,15 @@ fn executeMove(
447
487
  return null;
448
488
  }
449
489
 
450
- return doMove(battle, player, mslot, auto, residual, log);
490
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
451
491
  }
452
492
 
453
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
493
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
454
494
 
455
495
  fn beforeMove(
456
496
  battle: anytype,
457
497
  player: Player,
498
+ skip: bool,
458
499
  from: ?Move,
459
500
  residual: *bool,
460
501
  log: anytype,
@@ -486,13 +527,15 @@ fn beforeMove(
486
527
  return .done;
487
528
  }
488
529
 
489
- if (foe.active.volatiles.Binding) {
530
+ if (skip or foe.active.volatiles.Binding) {
490
531
  try log.cant(ident, .Bound);
491
532
  return .done;
492
533
  }
493
534
 
494
- if (!showdown and volatiles.Flinch) {
495
- volatiles.Flinch = false;
535
+ if (volatiles.Flinch) {
536
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
537
+ // Pokémon can sometimes flinch multiple times from the same original hit
538
+ if (!showdown) volatiles.Flinch = false;
496
539
  try log.cant(ident, .Flinch);
497
540
  return .done;
498
541
  }
@@ -513,13 +556,6 @@ fn beforeMove(
513
556
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
514
557
  if (showdown and try disabled(side, ident, log)) return .done;
515
558
 
516
- // Pokémon Showdown checks for Flinch *after* instead of before
517
- if (showdown and volatiles.Flinch) {
518
- volatiles.Flinch = false;
519
- try log.cant(ident, .Flinch);
520
- return .done;
521
- }
522
-
523
559
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
524
560
  // before the side had a selected a move - we simply need to assume this leads to a desync
525
561
  if (side.last_selected_move == .None) {
@@ -537,12 +573,7 @@ fn beforeMove(
537
573
  } else {
538
574
  try log.activate(ident, .Confusion);
539
575
 
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) {
576
+ if (Rolls.confused(battle)) {
546
577
  assert(!volatiles.MultiHit);
547
578
  if (!volatiles.Rage) volatiles.state = 0;
548
579
  volatiles.Bide = false;
@@ -568,8 +599,12 @@ fn beforeMove(
568
599
  player
569
600
  else
570
601
  player.foe();
602
+
603
+ const uncapped = battle.last_damage;
571
604
  // Skipping adjustDamage / randomizeDamage / checkHit
572
605
  _ = try applyDamage(battle, player, target, .Confusion, log);
606
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
+ if (showdown) battle.last_damage = uncapped;
573
608
 
574
609
  return .done;
575
610
  }
@@ -578,22 +613,15 @@ fn beforeMove(
578
613
 
579
614
  if (!showdown and try disabled(side, ident, log)) return .done;
580
615
 
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
- }
616
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
617
+ if (!volatiles.Rage) volatiles.state = 0;
618
+ volatiles.Bide = false;
619
+ volatiles.Thrashing = false;
620
+ volatiles.Charging = false;
621
+ volatiles.Binding = false;
622
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
+ try log.cant(ident, .Paralysis);
624
+ return .done;
597
625
  }
598
626
 
599
627
  if (volatiles.Bide) {
@@ -620,8 +648,9 @@ fn beforeMove(
620
648
  return .done;
621
649
  }
622
650
 
651
+ const sub = showdown and foe.active.volatiles.Substitute;
623
652
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
624
- if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
653
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
625
654
 
626
655
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
627
656
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -642,8 +671,13 @@ fn beforeMove(
642
671
  try log.start(battle.active(player), .ConfusionSilent);
643
672
  }
644
673
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
645
- if (showdown and handleThrashing(battle, active)) {
646
- try log.start(battle.active(player), .ConfusionSilent);
674
+ if (showdown) {
675
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
+ // used in useMove and doesn't have the notion of skipping canMove semantics
677
+ side.last_used_move = side.last_selected_move;
678
+ if (handleThrashing(battle, active)) {
679
+ try log.start(battle.active(player), .ConfusionSilent);
680
+ }
647
681
  }
648
682
  return .skip_can;
649
683
  }
@@ -658,7 +692,11 @@ fn beforeMove(
658
692
  side.last_selected_move,
659
693
  );
660
694
  if (showdown or battle.last_damage != 0) {
695
+ const sub = showdown and foe.active.volatiles.Substitute;
661
696
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
697
+ if (battle.foe(player).stored().hp > 0 and !sub) {
698
+ try buildRage(battle, player.foe(), log);
699
+ }
662
700
  }
663
701
  return .done;
664
702
  }
@@ -716,7 +754,7 @@ fn canMove(
716
754
  Effects.thrashing(battle, player);
717
755
  } else if (!showdown and move.effect == .Binding) {
718
756
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
719
- Effects.binding(battle, player);
757
+ Effects.binding(battle, player, false);
720
758
  }
721
759
 
722
760
  return true;
@@ -757,6 +795,7 @@ fn doMove(
757
795
  battle: anytype,
758
796
  player: Player,
759
797
  mslot: u4,
798
+ rewrap: bool,
760
799
  auto: bool,
761
800
  residual: *bool,
762
801
  log: anytype,
@@ -787,12 +826,15 @@ fn doMove(
787
826
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
788
827
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
789
828
  if (immune and move.effect != .Binding) break :miss true;
790
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
829
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
830
+ battle.last_damage = 0;
831
+ break :miss true;
832
+ }
791
833
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
792
834
  };
793
835
  assert(!immune or miss or (showdown and move.effect == .Binding));
794
836
 
795
- var late = showdown;
837
+ var late = showdown and move.effect != .Explode;
796
838
  const skip = status or immune;
797
839
  if ((!showdown or (!skip or counter)) and !miss) blk: {
798
840
  if (showdown and move.effect.isMulti()) {
@@ -845,9 +887,9 @@ fn doMove(
845
887
 
846
888
  if (!showdown or !miss) {
847
889
  if (move.effect == .MirrorMove) {
848
- return mirrorMove(battle, player, mslot, auto, residual, log);
890
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
849
891
  } else if (move.effect == .Metronome) {
850
- return metronome(battle, player, mslot, auto, residual, log);
892
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
851
893
  } else if (move.effect.onEnd()) {
852
894
  try onEnd(battle, player, move, log);
853
895
  return null;
@@ -862,7 +904,7 @@ fn doMove(
862
904
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
863
905
  if (ohko) {
864
906
  try log.immune(foe_ident, .OHKO);
865
- } else if (immune and !invulnerable) {
907
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
866
908
  try log.immune(foe_ident, .None);
867
909
  } else if (mist) {
868
910
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -894,6 +936,9 @@ fn doMove(
894
936
  hits = side.active.volatiles.attacks;
895
937
  }
896
938
 
939
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
940
+ const sub = showdown and foe.active.volatiles.Substitute;
941
+
897
942
  var nullified = false;
898
943
  var hit: u4 = 0;
899
944
  while (hit < hits) {
@@ -909,7 +954,7 @@ fn doMove(
909
954
  if (hit == 0 and ohko) try log.ohko();
910
955
  hit += 1;
911
956
  if (foe.stored().hp == 0) break;
912
- if (!late) try buildRage(battle, player.foe(), log);
957
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
913
958
  // If the substitute breaks during a multi-hit attack, the attack ends
914
959
  if (nullified) break;
915
960
  }
@@ -925,11 +970,17 @@ fn doMove(
925
970
  } else if (showdown) {
926
971
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
927
972
  if (move.effect == .Binding) {
928
- Effects.binding(battle, player);
973
+ Effects.binding(battle, player, rewrap);
929
974
  if (immune) {
930
975
  battle.last_damage = 0;
976
+ assert(foe.stored().hp > 0);
931
977
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
932
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
978
+ if (sub) {
979
+ try log.activate(battle.active(player.foe()), .Substitute);
980
+ } else {
981
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
982
+ try buildRage(battle, player.foe(), log);
983
+ }
933
984
  return null;
934
985
  }
935
986
  }
@@ -946,8 +997,8 @@ fn doMove(
946
997
 
947
998
  if (foe.stored().hp == 0) return null;
948
999
 
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);
1000
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1001
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
951
1002
 
952
1003
  if (!move.effect.isSpecial()) {
953
1004
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -980,8 +1031,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
980
1031
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
981
1032
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
982
1033
 
983
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
984
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1034
+ return Rolls.criticalHit(battle, chance);
985
1035
  }
986
1036
 
987
1037
  fn calcDamage(
@@ -1085,50 +1135,36 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1085
1135
 
1086
1136
  fn randomizeDamage(battle: anytype) void {
1087
1137
  if (battle.last_damage <= 1) return;
1088
-
1089
- const random = if (showdown)
1090
- battle.rng.range(u8, 217, 256)
1091
- else loop: {
1092
- while (true) {
1093
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1094
- if (r >= 217) break :loop r;
1095
- }
1096
- };
1097
-
1138
+ const random = Rolls.damage(battle);
1098
1139
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1099
1140
  }
1100
1141
 
1101
1142
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1102
1143
  const side = battle.side(player);
1144
+ const foe = battle.foe(player);
1103
1145
 
1104
1146
  if (!try checkHit(battle, player, move, log)) return null;
1105
1147
 
1106
1148
  battle.last_damage = switch (side.last_selected_move) {
1107
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1149
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1108
1150
  .SeismicToss, .NightShade => side.stored().level,
1109
1151
  .SonicBoom => 20,
1110
1152
  .DragonRage => 40,
1111
1153
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1112
1154
  .Psywave => power: {
1113
1155
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1114
- if (showdown) {
1115
- break :power battle.rng.range(u8, 0, max);
1116
- } else {
1117
- // GLITCH: Psywave infinite glitch loop
1118
- if (max <= 1) return Result.Error;
1119
- while (true) {
1120
- const r = battle.rng.next();
1121
- if (r < max) break :power r;
1122
- }
1123
- }
1156
+ // GLITCH: Psywave infinite glitch loop
1157
+ if (!showdown and max <= 1) return Result.Error;
1158
+ break :power Rolls.psywave(battle, max);
1124
1159
  },
1125
1160
  else => unreachable,
1126
1161
  };
1127
1162
 
1128
1163
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1129
1164
 
1165
+ const sub = showdown and foe.active.volatiles.Substitute;
1130
1166
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1131
- if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1167
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1132
1168
 
1133
1169
  return null;
1134
1170
  }
@@ -1177,7 +1213,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1177
1213
  // Pokémon Showdown calls checkHit before Counter
1178
1214
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1179
1215
 
1216
+ const sub = showdown and foe.active.volatiles.Substitute;
1180
1217
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1218
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1181
1219
  return null;
1182
1220
  }
1183
1221
 
@@ -1221,6 +1259,7 @@ fn mirrorMove(
1221
1259
  battle: anytype,
1222
1260
  player: Player,
1223
1261
  mslot: u4,
1262
+ rewrap: bool,
1224
1263
  auto: bool,
1225
1264
  residual: *bool,
1226
1265
  log: anytype,
@@ -1233,41 +1272,30 @@ fn mirrorMove(
1233
1272
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1234
1273
  try log.fail(battle.active(player), .None);
1235
1274
  return null;
1275
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1276
+ incrementPP(side, mslot);
1236
1277
  }
1237
1278
 
1238
- incrementPP(side, mslot);
1239
-
1240
1279
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1241
- return doMove(battle, player, mslot, auto, residual, log);
1280
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1242
1281
  }
1243
1282
 
1244
1283
  fn metronome(
1245
1284
  battle: anytype,
1246
1285
  player: Player,
1247
1286
  mslot: u4,
1287
+ rewrap: bool,
1248
1288
  auto: bool,
1249
1289
  residual: *bool,
1250
1290
  log: anytype,
1251
1291
  ) !?Result {
1252
1292
  var side = battle.side(player);
1253
1293
 
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
-
1294
+ side.last_selected_move = Rolls.metronome(battle);
1267
1295
  incrementPP(side, mslot);
1268
1296
 
1269
1297
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1270
- return doMove(battle, player, mslot, auto, residual, log);
1298
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1271
1299
  }
1272
1300
 
1273
1301
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1321,23 +1349,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1321
1349
  }
1322
1350
 
1323
1351
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1324
- var state = &side.active.volatiles.state;
1325
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1326
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1327
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1352
+ const state = side.active.volatiles.state;
1353
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1354
+ const overwritten = overwrite and state > 0;
1355
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1356
+
1357
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1328
1358
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1329
1359
  accuracy = accuracy * boost[0] / boost[1];
1330
1360
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1331
1361
  accuracy = accuracy * boost[0] / boost[1];
1332
1362
  accuracy = @min(255, @max(1, accuracy));
1333
1363
 
1334
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1364
+ // Pokémon Showdown only overwrites if the volatile is present
1365
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1366
+ if (overwrite) side.active.volatiles.state = accuracy;
1335
1367
 
1336
1368
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1337
- break :miss if (showdown)
1338
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1339
- else
1340
- battle.rng.next() >= accuracy;
1369
+ break :miss !Rolls.hit(battle, accuracy);
1341
1370
  };
1342
1371
 
1343
1372
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1469,9 +1498,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1469
1498
  damage *= volatiles.toxic;
1470
1499
  }
1471
1500
 
1472
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
- if (showdown) battle.last_damage = damage;
1474
-
1475
1501
  const amount = @min(damage, stored.hp);
1476
1502
  stored.hp -= amount;
1477
1503
 
@@ -1563,6 +1589,42 @@ fn checkEBC(battle: anytype) bool {
1563
1589
  return false;
1564
1590
  }
1565
1591
 
1592
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1593
+ if (side.active.volatiles.disabled_move != 0) {
1594
+ // A Pokémon that transforms after being disabled may end up with less move slots
1595
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1596
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1597
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1598
+ m
1599
+ else
1600
+ side.last_selected_move;
1601
+ if (m != .None and m == last) {
1602
+ side.active.volatiles.Charging = false;
1603
+ try log.disabled(ident, last);
1604
+ return true;
1605
+ }
1606
+ }
1607
+ return false;
1608
+ }
1609
+
1610
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1611
+ const side = battle.side(who);
1612
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1613
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1614
+ }
1615
+ }
1616
+
1617
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1618
+ var volatiles = &active.volatiles;
1619
+ assert(volatiles.Thrashing);
1620
+ if (volatiles.attacks > 0) return false;
1621
+
1622
+ volatiles.Thrashing = false;
1623
+ volatiles.Confusion = true;
1624
+ volatiles.confusion = Rolls.confusionDuration(battle);
1625
+ return true;
1626
+ }
1627
+
1566
1628
  inline fn onBegin(
1567
1629
  battle: anytype,
1568
1630
  player: Player,
@@ -1653,10 +1715,7 @@ pub const Effects = struct {
1653
1715
  side.active.volatiles.Bide = true;
1654
1716
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1655
1717
  side.active.volatiles.state = 0;
1656
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1657
- battle.rng.range(u4, 2, 4)
1658
- else
1659
- (battle.rng.next() & 1) + 2);
1718
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1660
1719
 
1661
1720
  try log.start(battle.active(player), .Bide);
1662
1721
  }
@@ -1678,14 +1737,8 @@ pub const Effects = struct {
1678
1737
  return;
1679
1738
  }
1680
1739
 
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;
1740
+ if (foe.active.types.includes(move.type)) return;
1741
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1689
1742
 
1690
1743
  foe_stored.status = Status.init(.BRN);
1691
1744
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1709,11 +1762,7 @@ pub const Effects = struct {
1709
1762
  const sub = foe.active.volatiles.Substitute;
1710
1763
 
1711
1764
  if (move.effect == .ConfusionChance) {
1712
- const chance = if (showdown)
1713
- battle.rng.chance(u8, 25, 256)
1714
- else
1715
- battle.rng.next() < Gen12.percent(10);
1716
- if (!chance) return;
1765
+ if (!Rolls.confusionChance(battle)) return;
1717
1766
  } else {
1718
1767
  if (showdown) {
1719
1768
  if (!try checkHit(battle, player, move, log)) {
@@ -1732,10 +1781,7 @@ pub const Effects = struct {
1732
1781
 
1733
1782
  if (foe.active.volatiles.Confusion) return;
1734
1783
  foe.active.volatiles.Confusion = true;
1735
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1736
- battle.rng.range(u8, 2, 6)
1737
- else
1738
- (battle.rng.next() & 3) + 2);
1784
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1739
1785
 
1740
1786
  try log.start(battle.active(player.foe()), .Confusion);
1741
1787
  }
@@ -1785,11 +1831,8 @@ pub const Effects = struct {
1785
1831
  return Result.Error;
1786
1832
  }
1787
1833
 
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);
1834
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1835
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1793
1836
 
1794
1837
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1795
1838
  return null;
@@ -1801,7 +1844,7 @@ pub const Effects = struct {
1801
1844
 
1802
1845
  if (battle.last_damage == 0) {
1803
1846
  assert(showdown);
1804
- return;
1847
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1805
1848
  }
1806
1849
 
1807
1850
  const drain = @max(battle.last_damage / 2, 1);
@@ -1828,15 +1871,7 @@ pub const Effects = struct {
1828
1871
  var volatiles = &battle.foe(player).active.volatiles;
1829
1872
 
1830
1873
  if (volatiles.Substitute) return;
1831
-
1832
- const chance = if (showdown)
1833
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1834
- else
1835
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1836
- Gen12.percent(10)
1837
- else
1838
- Gen12.percent(30));
1839
- if (!chance) return;
1874
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1840
1875
 
1841
1876
  volatiles.Flinch = true;
1842
1877
  volatiles.Recharging = false;
@@ -1862,12 +1897,8 @@ pub const Effects = struct {
1862
1897
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1863
1898
  }
1864
1899
 
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
-
1900
+ if (foe.active.types.includes(move.type)) return;
1901
+ if (!Rolls.secondaryChance(battle, true)) return;
1871
1902
  // Freeze Clause Mod
1872
1903
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1873
1904
 
@@ -2004,15 +2035,15 @@ pub const Effects = struct {
2004
2035
  }
2005
2036
  break :has_mimic false;
2006
2037
  };
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) {
2038
+ if (!has_mimic) {
2039
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2040
+ if (!try checkHit(battle, player, move, log)) return;
2010
2041
  return try log.fail(battle.active(player.foe()), .None);
2011
2042
  }
2012
2043
  }
2013
2044
  if (!try checkHit(battle, player, move, log)) return;
2014
2045
 
2015
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2046
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2016
2047
  side.active.move(oslot).id = foe.active.move(rslot).id;
2017
2048
 
2018
2049
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2033,7 +2064,8 @@ pub const Effects = struct {
2033
2064
  assert(!side.active.volatiles.MultiHit);
2034
2065
  side.active.volatiles.MultiHit = true;
2035
2066
 
2036
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2067
+ side.active.volatiles.attacks =
2068
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2037
2069
  }
2038
2070
 
2039
2071
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2077,14 +2109,8 @@ pub const Effects = struct {
2077
2109
  }
2078
2110
 
2079
2111
  // Body Slam can't paralyze a Normal type Pokémon
2080
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2081
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2082
- else
2083
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2084
- Gen12.percent(10)
2085
- else
2086
- Gen12.percent(30));
2087
- if (!chance) return;
2112
+ if (foe.active.types.includes(move.type)) return;
2113
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2088
2114
 
2089
2115
  foe_stored.status = Status.init(.PAR);
2090
2116
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2127,16 +2153,7 @@ pub const Effects = struct {
2127
2153
 
2128
2154
  if (move.effect == .Poison) {
2129
2155
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2130
- } else {
2131
- const chance = if (showdown)
2132
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2133
- else
2134
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2135
- Gen12.percent(20)
2136
- else
2137
- Gen12.percent(40));
2138
- if (!chance) return;
2139
- }
2156
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2140
2157
 
2141
2158
  foe_stored.status = Status.init(.PSN);
2142
2159
  if (toxic) {
@@ -2150,10 +2167,8 @@ pub const Effects = struct {
2150
2167
 
2151
2168
  fn rage(battle: anytype, player: Player) !void {
2152
2169
  var volatiles = &battle.side(player).active.volatiles;
2153
- if (volatiles.Rage) return;
2154
- volatiles.Rage = true;
2155
2170
  assert(!volatiles.Bide);
2156
- volatiles.state = 0;
2171
+ volatiles.Rage = true;
2157
2172
  }
2158
2173
 
2159
2174
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2205,16 +2220,7 @@ pub const Effects = struct {
2205
2220
  }
2206
2221
  foe.active.volatiles.Recharging = false;
2207
2222
 
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);
2223
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2218
2224
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2219
2225
  }
2220
2226
 
@@ -2267,11 +2273,7 @@ pub const Effects = struct {
2267
2273
  assert(!volatiles.Bide);
2268
2274
 
2269
2275
  volatiles.Thrashing = true;
2270
- volatiles.state = 0;
2271
- volatiles.attacks = @intCast(u3, if (showdown)
2272
- battle.rng.range(u8, 2, 4)
2273
- else
2274
- (battle.rng.next() & 1) + 2);
2276
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2275
2277
  }
2276
2278
 
2277
2279
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2303,7 +2305,7 @@ pub const Effects = struct {
2303
2305
  try log.transform(battle.active(player), foe_ident);
2304
2306
  }
2305
2307
 
2306
- fn binding(battle: anytype, player: Player) void {
2308
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2307
2309
  var side = battle.side(player);
2308
2310
  var foe = battle.foe(player);
2309
2311
 
@@ -2311,9 +2313,11 @@ pub const Effects = struct {
2311
2313
  side.active.volatiles.Binding = true;
2312
2314
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2313
2315
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2314
- if (!showdown) foe.active.volatiles.Recharging = false;
2316
+ if (!showdown) {
2317
+ foe.active.volatiles.Recharging = false;
2318
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2315
2319
 
2316
- side.active.volatiles.attacks = distribution(battle) - 1;
2320
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2317
2321
  }
2318
2322
 
2319
2323
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2340,7 +2344,7 @@ pub const Effects = struct {
2340
2344
  return try log.fail(ident, .None);
2341
2345
  }
2342
2346
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2343
- const stat = unmodifiedStats(battle, player).atk;
2347
+ const stat = unmodifiedStats(battle, side).atk;
2344
2348
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2349
  try log.boost(ident, reason, n);
2346
2350
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2360,7 +2364,7 @@ pub const Effects = struct {
2360
2364
  return try log.fail(ident, .None);
2361
2365
  }
2362
2366
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2363
- const stat = unmodifiedStats(battle, player).def;
2367
+ const stat = unmodifiedStats(battle, side).def;
2364
2368
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2365
2369
  try log.boost(ident, .Defense, n);
2366
2370
  },
@@ -2377,7 +2381,7 @@ pub const Effects = struct {
2377
2381
  return try log.fail(ident, .None);
2378
2382
  }
2379
2383
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2380
- const stat = unmodifiedStats(battle, player).spe;
2384
+ const stat = unmodifiedStats(battle, side).spe;
2381
2385
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2382
2386
  try log.boost(ident, .Speed, 2);
2383
2387
  },
@@ -2397,7 +2401,7 @@ pub const Effects = struct {
2397
2401
  return try log.fail(ident, .None);
2398
2402
  }
2399
2403
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2400
- const stat = unmodifiedStats(battle, player).spc;
2404
+ const stat = unmodifiedStats(battle, side).spc;
2401
2405
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2402
2406
  try log.boost(ident, .SpecialAttack, n);
2403
2407
  try log.boost(ident, .SpecialDefense, n);
@@ -2424,11 +2428,7 @@ pub const Effects = struct {
2424
2428
  }
2425
2429
 
2426
2430
  if (move.effect.isStatDownChance()) {
2427
- const chance = if (showdown)
2428
- battle.rng.chance(u8, 85, 256)
2429
- else
2430
- battle.rng.next() < Gen12.percent(33) + 1;
2431
- if (!chance or foe.active.volatiles.Invulnerable) return;
2431
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2432
2432
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2433
2433
  return; // checkHit already checks for Invulnerable
2434
2434
  }
@@ -2450,7 +2450,7 @@ pub const Effects = struct {
2450
2450
  return try log.fail(foe_ident, .None);
2451
2451
  }
2452
2452
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2453
- const stat = unmodifiedStats(battle, player.foe()).atk;
2453
+ const stat = unmodifiedStats(battle, foe).atk;
2454
2454
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2455
2455
  try log.boost(foe_ident, .Attack, -1);
2456
2456
  },
@@ -2468,7 +2468,7 @@ pub const Effects = struct {
2468
2468
  return try log.fail(foe_ident, .None);
2469
2469
  }
2470
2470
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2471
- const stat = unmodifiedStats(battle, player.foe()).def;
2471
+ const stat = unmodifiedStats(battle, foe).def;
2472
2472
  stats.def = @max(1, stat * mod[0] / mod[1]);
2473
2473
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2474
2474
  },
@@ -2485,7 +2485,7 @@ pub const Effects = struct {
2485
2485
  return try log.fail(foe_ident, .None);
2486
2486
  }
2487
2487
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2488
- const stat = unmodifiedStats(battle, player.foe()).spe;
2488
+ const stat = unmodifiedStats(battle, foe).spe;
2489
2489
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2490
2490
  try log.boost(foe_ident, .Speed, -1);
2491
2491
  assert(boosts.spe >= -6);
@@ -2505,7 +2505,7 @@ pub const Effects = struct {
2505
2505
  return try log.fail(foe_ident, .None);
2506
2506
  }
2507
2507
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2508
- const stat = unmodifiedStats(battle, player.foe()).spc;
2508
+ const stat = unmodifiedStats(battle, foe).spc;
2509
2509
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2510
2510
  try log.boost(foe_ident, .SpecialAttack, -1);
2511
2511
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2524,8 +2524,7 @@ pub const Effects = struct {
2524
2524
  }
2525
2525
  };
2526
2526
 
2527
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2528
- const side = battle.side(who);
2527
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2529
2528
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2530
2529
  const id = ID.from(side.active.volatiles.transform);
2531
2530
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2591,80 +2590,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2591
2590
  }
2592
2591
  }
2593
2592
 
2594
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2595
- if (side.active.volatiles.disabled_move != 0) {
2596
- // A Pokémon that transforms after being disabled may end up with less move slots
2597
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2598
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2599
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2600
- m
2601
- else
2602
- side.last_selected_move;
2603
- if (m != .None and m == last) {
2604
- side.active.volatiles.Charging = false;
2605
- try log.disabled(ident, last);
2606
- return true;
2593
+ pub const Rolls = struct {
2594
+ inline fn speedTie(battle: anytype) bool {
2595
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2596
+ return battle.rng.next() < Gen12.percent(50) + 1;
2597
+ }
2598
+
2599
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2600
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2601
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2602
+ }
2603
+
2604
+ inline fn damage(battle: anytype) u8 {
2605
+ if (showdown) return battle.rng.range(u8, 217, 256);
2606
+ while (true) {
2607
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2608
+ if (r >= 217) return r;
2607
2609
  }
2608
2610
  }
2609
- return false;
2610
- }
2611
2611
 
2612
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2613
- const side = battle.side(who);
2614
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2615
- try Effects.boost(battle, who, Move.get(.Rage), log);
2612
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2613
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2614
+ return battle.rng.next() < accuracy;
2616
2615
  }
2617
- }
2618
2616
 
2619
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2620
- var volatiles = &active.volatiles;
2621
- assert(volatiles.Thrashing);
2622
- if (volatiles.attacks > 0) return false;
2617
+ inline fn confusionDuration(battle: anytype) u3 {
2618
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2619
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2620
+ }
2623
2621
 
2624
- volatiles.Thrashing = false;
2625
- volatiles.Confusion = true;
2626
- volatiles.confusion = @intCast(u3, if (showdown)
2627
- battle.rng.range(u8, 2, 6)
2628
- else
2629
- (battle.rng.next() & 3) + 2);
2622
+ inline fn confused(battle: anytype) bool {
2623
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2624
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2625
+ }
2630
2626
 
2631
- return true;
2632
- }
2627
+ inline fn paralyzed(battle: anytype) bool {
2628
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2629
+ return battle.rng.next() < Gen12.percent(25);
2630
+ }
2633
2631
 
2634
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2632
+ inline fn confusionChance(battle: anytype) bool {
2633
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2634
+ return battle.rng.next() < Gen12.percent(10);
2635
+ }
2635
2636
 
2636
- fn distribution(battle: anytype) u3 {
2637
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2638
- const r = (battle.rng.next() & 3);
2639
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2640
- }
2637
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2638
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2639
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2640
+ }
2641
2641
 
2642
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2643
- if (showdown) {
2644
- if (check_pp == 0) {
2645
- var i: usize = moves.len;
2646
- while (i > 0) {
2647
- i -= 1;
2648
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2649
- }
2650
- } else {
2651
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2652
- var i: usize = 0;
2653
- while (i < moves.len and r > 0) : (i += 1) {
2654
- if (moves[i].pp > 0) {
2655
- r -= 1;
2656
- if (r == 0) break;
2642
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2643
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2644
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2645
+ }
2646
+
2647
+ inline fn sleepDuration(battle: anytype) u3 {
2648
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2649
+ while (true) {
2650
+ const r = battle.rng.next() & 7;
2651
+ if (r != 0) return @intCast(u3, r);
2652
+ }
2653
+ }
2654
+
2655
+ inline fn bideThrashDuration(battle: anytype) u3 {
2656
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2657
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2658
+ }
2659
+
2660
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2661
+ if (showdown) {
2662
+ if (check_pp == 0) {
2663
+ var i: usize = moves.len;
2664
+ while (i > 0) {
2665
+ i -= 1;
2666
+ if (moves[i].id != .None) {
2667
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2668
+ }
2657
2669
  }
2670
+ } else {
2671
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2672
+ var i: usize = 0;
2673
+ while (i < moves.len and r > 0) : (i += 1) {
2674
+ if (moves[i].pp > 0) {
2675
+ r -= 1;
2676
+ if (r == 0) break;
2677
+ }
2678
+ }
2679
+ return @intCast(u4, i + 1);
2658
2680
  }
2659
- return @intCast(u4, i + 1);
2681
+ }
2682
+
2683
+ while (true) {
2684
+ const r = @intCast(u4, battle.rng.next() & 3);
2685
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2660
2686
  }
2661
2687
  }
2662
2688
 
2663
- while (true) {
2664
- const r = @intCast(u4, rand.next() & 3);
2665
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2689
+ inline fn disableDuration(battle: anytype) u4 {
2690
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2691
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2666
2692
  }
2667
- }
2693
+
2694
+ inline fn metronome(battle: anytype) Move {
2695
+ if (showdown) {
2696
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2697
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2698
+ return @intToEnum(Move, r + mod);
2699
+ }
2700
+ while (true) {
2701
+ const r = battle.rng.next();
2702
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2703
+ if (r >= @enumToInt(Move.Struggle)) continue;
2704
+ return @intToEnum(Move, r);
2705
+ }
2706
+ }
2707
+
2708
+ inline fn psywave(battle: anytype, max: u8) u8 {
2709
+ if (showdown) return battle.rng.range(u8, 0, max);
2710
+ while (true) {
2711
+ const r = battle.rng.next();
2712
+ if (r < max) return r;
2713
+ }
2714
+ }
2715
+
2716
+ inline fn distribution(battle: anytype) u3 {
2717
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2718
+ const r = (battle.rng.next() & 3);
2719
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2720
+ }
2721
+
2722
+ inline fn unboost(battle: anytype) bool {
2723
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2724
+ return battle.rng.next() < Gen12.percent(33) + 1;
2725
+ }
2726
+ };
2668
2727
 
2669
2728
  test "RNG agreement" {
2670
2729
  if (!showdown) return;
@@ -2764,7 +2823,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2764
2823
  while (slot <= 4) : (slot += 1) {
2765
2824
  const m = active.moves[slot - 1];
2766
2825
  if (m.id == .None) break;
2767
- if (m.id == side.last_selected_move) {
2826
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2768
2827
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2769
2828
  const struggle =
2770
2829
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);