@pkmn/engine 0.1.0-dev.7fe22fc0 → 0.1.0-dev.84cc1a02

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,13 +486,7 @@ fn beforeMove(
524
486
  return .done;
525
487
  }
526
488
 
527
- if (showdown and volatiles.Recharging) {
528
- volatiles.Recharging = false;
529
- try log.cant(ident, .Recharge);
530
- return .done;
531
- }
532
-
533
- if (!showdown and foe.active.volatiles.Binding) {
489
+ if (foe.active.volatiles.Binding) {
534
490
  try log.cant(ident, .Bound);
535
491
  return .done;
536
492
  }
@@ -541,7 +497,7 @@ fn beforeMove(
541
497
  return .done;
542
498
  }
543
499
 
544
- if (!showdown and volatiles.Recharging) {
500
+ if (volatiles.Recharging) {
545
501
  volatiles.Recharging = false;
546
502
  try log.cant(ident, .Recharge);
547
503
  return .done;
@@ -557,15 +513,9 @@ fn beforeMove(
557
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
558
514
  if (showdown and try disabled(side, ident, log)) return .done;
559
515
 
560
- if (showdown and skip or foe.active.volatiles.Binding) {
561
- try log.cant(ident, .Bound);
562
- return .done;
563
- }
564
-
565
- // Pokémon Showdown checks for Flinch *after* instead of before... and doesn't clear Flinch
566
- // until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
567
- // from the same original hit
516
+ // Pokémon Showdown checks for Flinch *after* instead of before
568
517
  if (showdown and volatiles.Flinch) {
518
+ volatiles.Flinch = false;
569
519
  try log.cant(ident, .Flinch);
570
520
  return .done;
571
521
  }
@@ -587,7 +537,12 @@ fn beforeMove(
587
537
  } else {
588
538
  try log.activate(ident, .Confusion);
589
539
 
590
- 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) {
591
546
  assert(!volatiles.MultiHit);
592
547
  if (!volatiles.Rage) volatiles.state = 0;
593
548
  volatiles.Bide = false;
@@ -613,12 +568,8 @@ fn beforeMove(
613
568
  player
614
569
  else
615
570
  player.foe();
616
-
617
- const uncapped = battle.last_damage;
618
571
  // Skipping adjustDamage / randomizeDamage / checkHit
619
572
  _ = try applyDamage(battle, player, target, .Confusion, log);
620
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
621
- if (showdown) battle.last_damage = uncapped;
622
573
 
623
574
  return .done;
624
575
  }
@@ -627,15 +578,22 @@ fn beforeMove(
627
578
 
628
579
  if (!showdown and try disabled(side, ident, log)) return .done;
629
580
 
630
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
631
- if (!volatiles.Rage) volatiles.state = 0;
632
- volatiles.Bide = false;
633
- volatiles.Thrashing = false;
634
- volatiles.Charging = false;
635
- volatiles.Binding = false;
636
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
637
- try log.cant(ident, .Paralysis);
638
- return .done;
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
+ }
639
597
  }
640
598
 
641
599
  if (volatiles.Bide) {
@@ -662,9 +620,8 @@ fn beforeMove(
662
620
  return .done;
663
621
  }
664
622
 
665
- const sub = showdown and foe.active.volatiles.Substitute;
666
623
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
667
- 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);
668
625
 
669
626
  // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
670
627
  // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
@@ -685,13 +642,8 @@ fn beforeMove(
685
642
  try log.start(battle.active(player), .ConfusionSilent);
686
643
  }
687
644
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
688
- if (showdown) {
689
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
690
- // used in useMove and doesn't have the notion of skipping canMove semantics
691
- side.last_used_move = side.last_selected_move;
692
- if (handleThrashing(battle, active)) {
693
- try log.start(battle.active(player), .ConfusionSilent);
694
- }
645
+ if (showdown and handleThrashing(battle, active)) {
646
+ try log.start(battle.active(player), .ConfusionSilent);
695
647
  }
696
648
  return .skip_can;
697
649
  }
@@ -706,11 +658,7 @@ fn beforeMove(
706
658
  side.last_selected_move,
707
659
  );
708
660
  if (showdown or battle.last_damage != 0) {
709
- const sub = showdown and foe.active.volatiles.Substitute;
710
661
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
711
- if (battle.foe(player).stored().hp > 0 and !sub) {
712
- try buildRage(battle, player.foe(), log);
713
- }
714
662
  }
715
663
  return .done;
716
664
  }
@@ -768,7 +716,7 @@ fn canMove(
768
716
  Effects.thrashing(battle, player);
769
717
  } else if (!showdown and move.effect == .Binding) {
770
718
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
771
- Effects.binding(battle, player, false);
719
+ Effects.binding(battle, player);
772
720
  }
773
721
 
774
722
  return true;
@@ -809,7 +757,6 @@ fn doMove(
809
757
  battle: anytype,
810
758
  player: Player,
811
759
  mslot: u4,
812
- rewrap: bool,
813
760
  auto: bool,
814
761
  residual: *bool,
815
762
  log: anytype,
@@ -840,15 +787,12 @@ fn doMove(
840
787
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
841
788
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
842
789
  if (immune and move.effect != .Binding) break :miss true;
843
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
844
- battle.last_damage = 0;
845
- break :miss true;
846
- }
790
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
847
791
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
848
792
  };
849
793
  assert(!immune or miss or (showdown and move.effect == .Binding));
850
794
 
851
- var late = showdown and move.effect != .Explode;
795
+ var late = showdown;
852
796
  const skip = status or immune;
853
797
  if ((!showdown or (!skip or counter)) and !miss) blk: {
854
798
  if (showdown and move.effect.isMulti()) {
@@ -901,9 +845,9 @@ fn doMove(
901
845
 
902
846
  if (!showdown or !miss) {
903
847
  if (move.effect == .MirrorMove) {
904
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
848
+ return mirrorMove(battle, player, mslot, auto, residual, log);
905
849
  } else if (move.effect == .Metronome) {
906
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
850
+ return metronome(battle, player, mslot, auto, residual, log);
907
851
  } else if (move.effect.onEnd()) {
908
852
  try onEnd(battle, player, move, log);
909
853
  return null;
@@ -918,7 +862,7 @@ fn doMove(
918
862
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
919
863
  if (ohko) {
920
864
  try log.immune(foe_ident, .OHKO);
921
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
865
+ } else if (immune and !invulnerable) {
922
866
  try log.immune(foe_ident, .None);
923
867
  } else if (mist) {
924
868
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -950,9 +894,6 @@ fn doMove(
950
894
  hits = side.active.volatiles.attacks;
951
895
  }
952
896
 
953
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
954
- const sub = showdown and foe.active.volatiles.Substitute;
955
-
956
897
  var nullified = false;
957
898
  var hit: u4 = 0;
958
899
  while (hit < hits) {
@@ -968,7 +909,7 @@ fn doMove(
968
909
  if (hit == 0 and ohko) try log.ohko();
969
910
  hit += 1;
970
911
  if (foe.stored().hp == 0) break;
971
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
912
+ if (!late) try buildRage(battle, player.foe(), log);
972
913
  // If the substitute breaks during a multi-hit attack, the attack ends
973
914
  if (nullified) break;
974
915
  }
@@ -984,7 +925,7 @@ fn doMove(
984
925
  } else if (showdown) {
985
926
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
986
927
  if (move.effect == .Binding) {
987
- Effects.binding(battle, player, rewrap);
928
+ Effects.binding(battle, player);
988
929
  if (immune) {
989
930
  battle.last_damage = 0;
990
931
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -1005,8 +946,8 @@ fn doMove(
1005
946
 
1006
947
  if (foe.stored().hp == 0) return null;
1007
948
 
1008
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1009
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
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);
1010
951
 
1011
952
  if (!move.effect.isSpecial()) {
1012
953
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1039,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1039
980
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1040
981
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1041
982
 
1042
- 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;
1043
985
  }
1044
986
 
1045
987
  fn calcDamage(
@@ -1143,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1143
1085
 
1144
1086
  fn randomizeDamage(battle: anytype) void {
1145
1087
  if (battle.last_damage <= 1) return;
1146
- 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
+
1147
1098
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1148
1099
  }
1149
1100
 
1150
1101
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1151
1102
  const side = battle.side(player);
1152
- const foe = battle.foe(player);
1153
1103
 
1154
1104
  if (!try checkHit(battle, player, move, log)) return null;
1155
1105
 
1156
1106
  battle.last_damage = switch (side.last_selected_move) {
1157
- .SuperFang => @max(foe.stored().hp / 2, 1),
1107
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1158
1108
  .SeismicToss, .NightShade => side.stored().level,
1159
1109
  .SonicBoom => 20,
1160
1110
  .DragonRage => 40,
1161
1111
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1162
1112
  .Psywave => power: {
1163
1113
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1164
- // GLITCH: Psywave infinite glitch loop
1165
- if (!showdown and max <= 1) return Result.Error;
1166
- 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
+ }
1167
1124
  },
1168
1125
  else => unreachable,
1169
1126
  };
1170
1127
 
1171
1128
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1172
1129
 
1173
- const sub = showdown and foe.active.volatiles.Substitute;
1174
1130
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1175
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1131
+ if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
1176
1132
 
1177
1133
  return null;
1178
1134
  }
@@ -1221,9 +1177,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1221
1177
  // Pokémon Showdown calls checkHit before Counter
1222
1178
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1223
1179
 
1224
- const sub = showdown and foe.active.volatiles.Substitute;
1225
1180
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1226
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1227
1181
  return null;
1228
1182
  }
1229
1183
 
@@ -1267,7 +1221,6 @@ fn mirrorMove(
1267
1221
  battle: anytype,
1268
1222
  player: Player,
1269
1223
  mslot: u4,
1270
- rewrap: bool,
1271
1224
  auto: bool,
1272
1225
  residual: *bool,
1273
1226
  log: anytype,
@@ -1285,25 +1238,36 @@ fn mirrorMove(
1285
1238
  incrementPP(side, mslot);
1286
1239
 
1287
1240
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1288
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1241
+ return doMove(battle, player, mslot, auto, residual, log);
1289
1242
  }
1290
1243
 
1291
1244
  fn metronome(
1292
1245
  battle: anytype,
1293
1246
  player: Player,
1294
1247
  mslot: u4,
1295
- rewrap: bool,
1296
1248
  auto: bool,
1297
1249
  residual: *bool,
1298
1250
  log: anytype,
1299
1251
  ) !?Result {
1300
1252
  var side = battle.side(player);
1301
1253
 
1302
- 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
+
1303
1267
  incrementPP(side, mslot);
1304
1268
 
1305
1269
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1306
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1270
+ return doMove(battle, player, mslot, auto, residual, log);
1307
1271
  }
1308
1272
 
1309
1273
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1357,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1357
1321
  }
1358
1322
 
1359
1323
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1360
- const state = side.active.volatiles.state;
1361
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1362
- const overwritten = overwrite and state > 0;
1363
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1364
-
1365
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
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));
1366
1328
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1367
1329
  accuracy = accuracy * boost[0] / boost[1];
1368
1330
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1369
1331
  accuracy = accuracy * boost[0] / boost[1];
1370
1332
  accuracy = @min(255, @max(1, accuracy));
1371
1333
 
1372
- // Pokémon Showdown only overwrites if the volatile is present
1373
- if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1374
- if (overwrite) side.active.volatiles.state = accuracy;
1334
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1375
1335
 
1376
1336
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1377
- 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;
1378
1341
  };
1379
1342
 
1380
1343
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1506,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1506
1469
  damage *= volatiles.toxic;
1507
1470
  }
1508
1471
 
1472
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1473
+ if (showdown) battle.last_damage = damage;
1474
+
1509
1475
  const amount = @min(damage, stored.hp);
1510
1476
  stored.hp -= amount;
1511
1477
 
1512
1478
  // As above, Pokémon Showdown uses damageOf but its not relevant
1513
- if (amount > 0) {
1514
- try log.damage(ident, stored, .LeechSeed);
1515
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1516
- if (showdown) battle.last_damage = damage;
1517
- }
1479
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1518
1480
 
1519
1481
  const before = foe_stored.hp;
1520
1482
  // Uncapped damage is added back to the foe
@@ -1601,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
1601
1563
  return false;
1602
1564
  }
1603
1565
 
1604
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1605
- if (side.active.volatiles.disabled_move != 0) {
1606
- // A Pokémon that transforms after being disabled may end up with less move slots
1607
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1608
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1609
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1610
- m
1611
- else
1612
- side.last_selected_move;
1613
- if (m != .None and m == last) {
1614
- side.active.volatiles.Charging = false;
1615
- try log.disabled(ident, last);
1616
- return true;
1617
- }
1618
- }
1619
- return false;
1620
- }
1621
-
1622
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1623
- const side = battle.side(who);
1624
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1625
- try Effects.boost(battle, who, Move.get(.Rage), log);
1626
- }
1627
- }
1628
-
1629
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1630
- var volatiles = &active.volatiles;
1631
- assert(volatiles.Thrashing);
1632
- if (volatiles.attacks > 0) return false;
1633
-
1634
- volatiles.Thrashing = false;
1635
- volatiles.Confusion = true;
1636
- volatiles.confusion = Rolls.confusionDuration(battle);
1637
- return true;
1638
- }
1639
-
1640
1566
  inline fn onBegin(
1641
1567
  battle: anytype,
1642
1568
  player: Player,
@@ -1727,7 +1653,10 @@ pub const Effects = struct {
1727
1653
  side.active.volatiles.Bide = true;
1728
1654
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1729
1655
  side.active.volatiles.state = 0;
1730
- 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);
1731
1660
 
1732
1661
  try log.start(battle.active(player), .Bide);
1733
1662
  }
@@ -1749,8 +1678,14 @@ pub const Effects = struct {
1749
1678
  return;
1750
1679
  }
1751
1680
 
1752
- if (foe.active.types.includes(move.type)) return;
1753
- 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;
1754
1689
 
1755
1690
  foe_stored.status = Status.init(.BRN);
1756
1691
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1774,7 +1709,11 @@ pub const Effects = struct {
1774
1709
  const sub = foe.active.volatiles.Substitute;
1775
1710
 
1776
1711
  if (move.effect == .ConfusionChance) {
1777
- 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;
1778
1717
  } else {
1779
1718
  if (showdown) {
1780
1719
  if (!try checkHit(battle, player, move, log)) {
@@ -1793,7 +1732,10 @@ pub const Effects = struct {
1793
1732
 
1794
1733
  if (foe.active.volatiles.Confusion) return;
1795
1734
  foe.active.volatiles.Confusion = true;
1796
- 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);
1797
1739
 
1798
1740
  try log.start(battle.active(player.foe()), .Confusion);
1799
1741
  }
@@ -1843,8 +1785,11 @@ pub const Effects = struct {
1843
1785
  return Result.Error;
1844
1786
  }
1845
1787
 
1846
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1847
- 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);
1848
1793
 
1849
1794
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1850
1795
  return null;
@@ -1856,7 +1801,7 @@ pub const Effects = struct {
1856
1801
 
1857
1802
  if (battle.last_damage == 0) {
1858
1803
  assert(showdown);
1859
- if (!battle.foe(player).active.volatiles.Substitute) return;
1804
+ return;
1860
1805
  }
1861
1806
 
1862
1807
  const drain = @max(battle.last_damage / 2, 1);
@@ -1883,7 +1828,15 @@ pub const Effects = struct {
1883
1828
  var volatiles = &battle.foe(player).active.volatiles;
1884
1829
 
1885
1830
  if (volatiles.Substitute) return;
1886
- 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;
1887
1840
 
1888
1841
  volatiles.Flinch = true;
1889
1842
  volatiles.Recharging = false;
@@ -1909,8 +1862,12 @@ pub const Effects = struct {
1909
1862
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1910
1863
  }
1911
1864
 
1912
- if (foe.active.types.includes(move.type)) return;
1913
- 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
+
1914
1871
  // Freeze Clause Mod
1915
1872
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1916
1873
 
@@ -2055,7 +2012,7 @@ pub const Effects = struct {
2055
2012
  }
2056
2013
  if (!try checkHit(battle, player, move, log)) return;
2057
2014
 
2058
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2015
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2059
2016
  side.active.move(oslot).id = foe.active.move(rslot).id;
2060
2017
 
2061
2018
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2076,8 +2033,7 @@ pub const Effects = struct {
2076
2033
  assert(!side.active.volatiles.MultiHit);
2077
2034
  side.active.volatiles.MultiHit = true;
2078
2035
 
2079
- side.active.volatiles.attacks =
2080
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2036
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2081
2037
  }
2082
2038
 
2083
2039
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2121,8 +2077,14 @@ pub const Effects = struct {
2121
2077
  }
2122
2078
 
2123
2079
  // Body Slam can't paralyze a Normal type Pokémon
2124
- if (foe.active.types.includes(move.type)) return;
2125
- 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;
2126
2088
 
2127
2089
  foe_stored.status = Status.init(.PAR);
2128
2090
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2165,7 +2127,16 @@ pub const Effects = struct {
2165
2127
 
2166
2128
  if (move.effect == .Poison) {
2167
2129
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2168
- } 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
+ }
2169
2140
 
2170
2141
  foe_stored.status = Status.init(.PSN);
2171
2142
  if (toxic) {
@@ -2179,8 +2150,10 @@ pub const Effects = struct {
2179
2150
 
2180
2151
  fn rage(battle: anytype, player: Player) !void {
2181
2152
  var volatiles = &battle.side(player).active.volatiles;
2182
- assert(!volatiles.Bide);
2153
+ if (volatiles.Rage) return;
2183
2154
  volatiles.Rage = true;
2155
+ assert(!volatiles.Bide);
2156
+ volatiles.state = 0;
2184
2157
  }
2185
2158
 
2186
2159
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2232,7 +2205,16 @@ pub const Effects = struct {
2232
2205
  }
2233
2206
  foe.active.volatiles.Recharging = false;
2234
2207
 
2235
- 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);
2236
2218
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2237
2219
  }
2238
2220
 
@@ -2285,7 +2267,11 @@ pub const Effects = struct {
2285
2267
  assert(!volatiles.Bide);
2286
2268
 
2287
2269
  volatiles.Thrashing = true;
2288
- 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);
2289
2275
  }
2290
2276
 
2291
2277
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2317,7 +2303,7 @@ pub const Effects = struct {
2317
2303
  try log.transform(battle.active(player), foe_ident);
2318
2304
  }
2319
2305
 
2320
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2306
+ fn binding(battle: anytype, player: Player) void {
2321
2307
  var side = battle.side(player);
2322
2308
  var foe = battle.foe(player);
2323
2309
 
@@ -2325,11 +2311,9 @@ pub const Effects = struct {
2325
2311
  side.active.volatiles.Binding = true;
2326
2312
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2327
2313
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2328
- if (!showdown) {
2329
- foe.active.volatiles.Recharging = false;
2330
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2314
+ if (!showdown) foe.active.volatiles.Recharging = false;
2331
2315
 
2332
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2316
+ side.active.volatiles.attacks = distribution(battle) - 1;
2333
2317
  }
2334
2318
 
2335
2319
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2356,11 +2340,9 @@ pub const Effects = struct {
2356
2340
  return try log.fail(ident, .None);
2357
2341
  }
2358
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2359
- const stat = unmodifiedStats(battle, side).atk;
2343
+ const stat = unmodifiedStats(battle, player).atk;
2360
2344
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2361
2345
  try log.boost(ident, reason, n);
2362
- // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2363
- if (showdown and move.effect == .Rage) return;
2364
2346
  },
2365
2347
  .DefenseUp1, .DefenseUp2 => {
2366
2348
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2376,7 +2358,7 @@ pub const Effects = struct {
2376
2358
  return try log.fail(ident, .None);
2377
2359
  }
2378
2360
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2379
- const stat = unmodifiedStats(battle, side).def;
2361
+ const stat = unmodifiedStats(battle, player).def;
2380
2362
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2381
2363
  try log.boost(ident, .Defense, n);
2382
2364
  },
@@ -2393,7 +2375,7 @@ pub const Effects = struct {
2393
2375
  return try log.fail(ident, .None);
2394
2376
  }
2395
2377
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2396
- const stat = unmodifiedStats(battle, side).spe;
2378
+ const stat = unmodifiedStats(battle, player).spe;
2397
2379
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2398
2380
  try log.boost(ident, .Speed, 2);
2399
2381
  },
@@ -2413,7 +2395,7 @@ pub const Effects = struct {
2413
2395
  return try log.fail(ident, .None);
2414
2396
  }
2415
2397
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2416
- const stat = unmodifiedStats(battle, side).spc;
2398
+ const stat = unmodifiedStats(battle, player).spc;
2417
2399
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2418
2400
  try log.boost(ident, .SpecialAttack, n);
2419
2401
  try log.boost(ident, .SpecialDefense, n);
@@ -2440,7 +2422,11 @@ pub const Effects = struct {
2440
2422
  }
2441
2423
 
2442
2424
  if (move.effect.isStatDownChance()) {
2443
- 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;
2444
2430
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2445
2431
  return; // checkHit already checks for Invulnerable
2446
2432
  }
@@ -2462,7 +2448,7 @@ pub const Effects = struct {
2462
2448
  return try log.fail(foe_ident, .None);
2463
2449
  }
2464
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2465
- const stat = unmodifiedStats(battle, foe).atk;
2451
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2466
2452
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2467
2453
  try log.boost(foe_ident, .Attack, -1);
2468
2454
  },
@@ -2480,7 +2466,7 @@ pub const Effects = struct {
2480
2466
  return try log.fail(foe_ident, .None);
2481
2467
  }
2482
2468
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2483
- const stat = unmodifiedStats(battle, foe).def;
2469
+ const stat = unmodifiedStats(battle, player.foe()).def;
2484
2470
  stats.def = @max(1, stat * mod[0] / mod[1]);
2485
2471
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2486
2472
  },
@@ -2497,7 +2483,7 @@ pub const Effects = struct {
2497
2483
  return try log.fail(foe_ident, .None);
2498
2484
  }
2499
2485
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2500
- const stat = unmodifiedStats(battle, foe).spe;
2486
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2501
2487
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2502
2488
  try log.boost(foe_ident, .Speed, -1);
2503
2489
  assert(boosts.spe >= -6);
@@ -2517,7 +2503,7 @@ pub const Effects = struct {
2517
2503
  return try log.fail(foe_ident, .None);
2518
2504
  }
2519
2505
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2520
- const stat = unmodifiedStats(battle, foe).spc;
2506
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2521
2507
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2522
2508
  try log.boost(foe_ident, .SpecialAttack, -1);
2523
2509
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2536,7 +2522,8 @@ pub const Effects = struct {
2536
2522
  }
2537
2523
  };
2538
2524
 
2539
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2525
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2526
+ const side = battle.side(who);
2540
2527
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2541
2528
  const id = ID.from(side.active.volatiles.transform);
2542
2529
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2602,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2602
2589
  }
2603
2590
  }
2604
2591
 
2605
- pub const Rolls = struct {
2606
- inline fn speedTie(battle: anytype) bool {
2607
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2608
- return battle.rng.next() < Gen12.percent(50) + 1;
2609
- }
2610
-
2611
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2612
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2613
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2614
- }
2615
-
2616
- inline fn damage(battle: anytype) u8 {
2617
- if (showdown) return battle.rng.range(u8, 217, 256);
2618
- while (true) {
2619
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2620
- if (r >= 217) return r;
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;
2621
2600
  }
2622
2601
  }
2602
+ return false;
2603
+ }
2623
2604
 
2624
- inline fn hit(battle: anytype, accuracy: u16) bool {
2625
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2626
- return battle.rng.next() < accuracy;
2627
- }
2628
-
2629
- inline fn confusionDuration(battle: anytype) u3 {
2630
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2631
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2632
- }
2633
-
2634
- inline fn confused(battle: anytype) bool {
2635
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2636
- return battle.rng.next() >= Gen12.percent(50) + 1;
2637
- }
2638
-
2639
- inline fn paralyzed(battle: anytype) bool {
2640
- if (showdown) return battle.rng.chance(u8, 63, 256);
2641
- return battle.rng.next() < Gen12.percent(25);
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);
2642
2609
  }
2610
+ }
2643
2611
 
2644
- inline fn confusionChance(battle: anytype) bool {
2645
- if (showdown) return battle.rng.chance(u8, 25, 256);
2646
- return battle.rng.next() < Gen12.percent(10);
2647
- }
2612
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2613
+ var volatiles = &active.volatiles;
2614
+ assert(volatiles.Thrashing);
2615
+ if (volatiles.attacks > 0) return false;
2648
2616
 
2649
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2650
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2651
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2652
- }
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);
2653
2623
 
2654
- inline fn poisonChance(battle: anytype, low: bool) bool {
2655
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2656
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2657
- }
2624
+ return true;
2625
+ }
2658
2626
 
2659
- inline fn sleepDuration(battle: anytype) u3 {
2660
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2661
- while (true) {
2662
- const r = battle.rng.next() & 7;
2663
- if (r != 0) return @intCast(u3, r);
2664
- }
2665
- }
2627
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2666
2628
 
2667
- inline fn bideThrashDuration(battle: anytype) u3 {
2668
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2669
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2670
- }
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
+ }
2671
2634
 
2672
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2673
- if (showdown) {
2674
- if (check_pp == 0) {
2675
- var i: usize = moves.len;
2676
- while (i > 0) {
2677
- i -= 1;
2678
- if (moves[i].id != .None) {
2679
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2680
- }
2681
- }
2682
- } else {
2683
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2684
- var i: usize = 0;
2685
- while (i < moves.len and r > 0) : (i += 1) {
2686
- if (moves[i].pp > 0) {
2687
- r -= 1;
2688
- if (r == 0) break;
2689
- }
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;
2690
2650
  }
2691
- return @intCast(u4, i + 1);
2692
2651
  }
2652
+ return @intCast(u4, i + 1);
2693
2653
  }
2694
-
2695
- while (true) {
2696
- const r = @intCast(u4, battle.rng.next() & 3);
2697
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2698
- }
2699
- }
2700
-
2701
- inline fn disableDuration(battle: anytype) u4 {
2702
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2703
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2704
- }
2705
-
2706
- inline fn metronome(battle: anytype) Move {
2707
- if (showdown) {
2708
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2709
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2710
- return @intToEnum(Move, r + mod);
2711
- }
2712
- while (true) {
2713
- const r = battle.rng.next();
2714
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2715
- if (r >= @enumToInt(Move.Struggle)) continue;
2716
- return @intToEnum(Move, r);
2717
- }
2718
- }
2719
-
2720
- inline fn psywave(battle: anytype, max: u8) u8 {
2721
- if (showdown) return battle.rng.range(u8, 0, max);
2722
- while (true) {
2723
- const r = battle.rng.next();
2724
- if (r < max) return r;
2725
- }
2726
- }
2727
-
2728
- inline fn distribution(battle: anytype) u3 {
2729
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2730
- const r = (battle.rng.next() & 3);
2731
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2732
2654
  }
2733
2655
 
2734
- inline fn unboost(battle: anytype) bool {
2735
- if (showdown) return battle.rng.chance(u8, 85, 256);
2736
- return battle.rng.next() < Gen12.percent(33) + 1;
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;
2737
2659
  }
2738
- };
2660
+ }
2739
2661
 
2740
2662
  test "RNG agreement" {
2741
2663
  if (!showdown) return;
@@ -2835,7 +2757,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2835
2757
  while (slot <= 4) : (slot += 1) {
2836
2758
  const m = active.moves[slot - 1];
2837
2759
  if (m.id == .None) break;
2838
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2760
+ if (m.id == side.last_selected_move) {
2839
2761
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2840
2762
  const struggle =
2841
2763
  m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);