@pkmn/engine 0.1.0-dev.02b7f810 → 0.1.0-dev.14e79917

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) {
@@ -163,9 +180,9 @@ fn selectMove(
163
180
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
164
181
  if (foe_choice.type == .Switch) {
165
182
  const slot = if (player == .P1)
166
- battle.last_selected_indexes.p1
183
+ battle.last_moves.p1_index
167
184
  else
168
- battle.last_selected_indexes.p2;
185
+ battle.last_moves.p2_index;
169
186
  const last = side.active.move(@intCast(u4, slot));
170
187
  if (last.id == .Metronome) side.last_selected_move = last.id;
171
188
  if (last.id == .MirrorMove) return Result.Error;
@@ -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 {
@@ -219,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
219
237
 
220
238
  side.last_selected_move = move.id;
221
239
  if (player == .P1) {
222
- battle.last_selected_indexes.p1 = @intCast(u4, c.data);
240
+ battle.last_moves.p1_index = @intCast(u3, c.data);
223
241
  } else {
224
- battle.last_selected_indexes.p2 = @intCast(u4, c.data);
242
+ battle.last_moves.p2_index = @intCast(u3, c.data);
225
243
  }
226
244
  }
227
245
  }
@@ -241,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
241
259
  side.order[slot - 1] = out;
242
260
 
243
261
  if (player == .P1) {
244
- battle.last_selected_indexes.p1 = 1;
262
+ battle.last_moves.p1_index = 1;
245
263
  } else {
246
- battle.last_selected_indexes.p2 = 1;
264
+ battle.last_moves.p2_index = 1;
247
265
  }
248
266
 
249
267
  side.last_used_move = .None;
@@ -300,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
300
318
  const spe2 = battle.side(.P2).active.stats.spe;
301
319
  if (spe1 == spe2) {
302
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
303
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
304
-
305
- const p1 = if (showdown)
306
- battle.rng.range(u8, 0, 2) == 0
307
- else
308
- battle.rng.next() < Gen12.percent(50) + 1;
321
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
+ battle.rng.advance(1);
323
+ }
309
324
 
325
+ const p1 = Rolls.speedTie(battle);
310
326
  if (!showdown) return if (p1) .P1 else .P2;
311
327
 
312
328
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -316,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
316
332
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
317
333
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
318
334
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
319
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
335
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
320
336
  // won the original coin flip
321
337
  if (t1 and t2) return .P1;
322
338
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -329,9 +345,13 @@ fn doTurn(
329
345
  battle: anytype,
330
346
  player: Player,
331
347
  player_choice: Choice,
348
+ player_rewrap: bool,
349
+ player_skip: bool,
332
350
  player_from: ?Move,
333
351
  foe_player: Player,
334
352
  foe_choice: Choice,
353
+ foe_rewrap: bool,
354
+ foe_skip: bool,
335
355
  foe_from: ?Move,
336
356
  log: anytype,
337
357
  ) !?Result {
@@ -339,7 +359,16 @@ fn doTurn(
339
359
 
340
360
  var residual = true;
341
361
  var replace = battle.side(player).stored().hp == 0;
342
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
362
+ if (try executeMove(
363
+ battle,
364
+ player,
365
+ player_choice,
366
+ player_rewrap,
367
+ player_skip,
368
+ player_from,
369
+ &residual,
370
+ log,
371
+ )) |r| return r;
343
372
  if (!replace) {
344
373
  if (player_choice.type != .Switch) {
345
374
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -350,7 +379,16 @@ fn doTurn(
350
379
 
351
380
  residual = true;
352
381
  replace = battle.side(foe_player).stored().hp == 0;
353
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
382
+ if (try executeMove(
383
+ battle,
384
+ foe_player,
385
+ foe_choice,
386
+ foe_rewrap,
387
+ foe_skip,
388
+ foe_from,
389
+ &residual,
390
+ log,
391
+ )) |r| return r;
354
392
  if (!replace) {
355
393
  if (foe_choice.type != .Switch) {
356
394
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -373,6 +411,8 @@ fn executeMove(
373
411
  battle: anytype,
374
412
  player: Player,
375
413
  choice: Choice,
414
+ rewrap: bool,
415
+ skip: bool,
376
416
  from: ?Move,
377
417
  residual: *bool,
378
418
  log: anytype,
@@ -384,9 +424,10 @@ fn executeMove(
384
424
  return null;
385
425
  }
386
426
 
427
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
428
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
429
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
387
430
  if (side.last_selected_move == .SKIP_TURN) {
388
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
389
- // Binding instead gets handled in beforeMove after sleep and freeze
390
431
  assert(!showdown);
391
432
  if (battle.foe(player).active.volatiles.Binding) {
392
433
  try log.cant(battle.active(player), .Bound);
@@ -405,9 +446,9 @@ fn executeMove(
405
446
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
406
447
  assert(!showdown);
407
448
  mslot = @intCast(u4, if (player == .P1)
408
- battle.last_selected_indexes.p1
449
+ battle.last_moves.p1_index
409
450
  else
410
- battle.last_selected_indexes.p2);
451
+ battle.last_moves.p2_index);
411
452
  const stored = side.stored();
412
453
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
413
454
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -429,14 +470,14 @@ fn executeMove(
429
470
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
430
471
  assert(mslot == 1);
431
472
  mslot = @intCast(u4, if (player == .P1)
432
- battle.last_selected_indexes.p1
473
+ battle.last_moves.p1_index
433
474
  else
434
- battle.last_selected_indexes.p2);
475
+ battle.last_moves.p2_index);
435
476
  }
436
477
 
437
478
  var skip_can = false;
438
479
  var skip_pp = false;
439
- switch (try beforeMove(battle, player, from, residual, log)) {
480
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
440
481
  .done => return null,
441
482
  .skip_can => skip_can = true,
442
483
  .skip_pp => skip_pp = true,
@@ -448,14 +489,15 @@ fn executeMove(
448
489
  return null;
449
490
  }
450
491
 
451
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
452
493
  }
453
494
 
454
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
455
496
 
456
497
  fn beforeMove(
457
498
  battle: anytype,
458
499
  player: Player,
500
+ skip: bool,
459
501
  from: ?Move,
460
502
  residual: *bool,
461
503
  log: anytype,
@@ -487,13 +529,15 @@ fn beforeMove(
487
529
  return .done;
488
530
  }
489
531
 
490
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
491
533
  try log.cant(ident, .Bound);
492
534
  return .done;
493
535
  }
494
536
 
495
- if (!showdown and volatiles.Flinch) {
496
- volatiles.Flinch = false;
537
+ if (volatiles.Flinch) {
538
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
539
+ // Pokémon can sometimes flinch multiple times from the same original hit
540
+ if (!showdown) volatiles.Flinch = false;
497
541
  try log.cant(ident, .Flinch);
498
542
  return .done;
499
543
  }
@@ -514,13 +558,6 @@ fn beforeMove(
514
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
515
559
  if (showdown and try disabled(side, ident, log)) return .done;
516
560
 
517
- // Pokémon Showdown checks for Flinch *after* instead of before
518
- if (showdown and volatiles.Flinch) {
519
- volatiles.Flinch = false;
520
- try log.cant(ident, .Flinch);
521
- return .done;
522
- }
523
-
524
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
525
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
526
563
  if (side.last_selected_move == .None) {
@@ -538,12 +575,7 @@ fn beforeMove(
538
575
  } else {
539
576
  try log.activate(ident, .Confusion);
540
577
 
541
- const confused = if (showdown)
542
- !battle.rng.chance(u8, 128, 256)
543
- else
544
- battle.rng.next() >= Gen12.percent(50) + 1;
545
-
546
- if (confused) {
578
+ if (Rolls.confused(battle)) {
547
579
  assert(!volatiles.MultiHit);
548
580
  if (!volatiles.Rage) volatiles.state = 0;
549
581
  volatiles.Bide = false;
@@ -573,7 +605,7 @@ fn beforeMove(
573
605
  const uncapped = battle.last_damage;
574
606
  // Skipping adjustDamage / randomizeDamage / checkHit
575
607
  _ = try applyDamage(battle, player, target, .Confusion, log);
576
- // Pokémon Showdown thinks that Confusion damage is uncapped ¯\_(ツ)_/¯
608
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
577
609
  if (showdown) battle.last_damage = uncapped;
578
610
 
579
611
  return .done;
@@ -583,28 +615,28 @@ fn beforeMove(
583
615
 
584
616
  if (!showdown and try disabled(side, ident, log)) return .done;
585
617
 
586
- if (Status.is(stored.status, .PAR)) {
587
- const paralyzed = if (showdown)
588
- battle.rng.chance(u8, 63, 256)
589
- else
590
- battle.rng.next() < Gen12.percent(25);
591
-
592
- if (paralyzed) {
593
- if (!volatiles.Rage) volatiles.state = 0;
594
- volatiles.Bide = false;
595
- volatiles.Thrashing = false;
596
- volatiles.Charging = false;
597
- volatiles.Binding = false;
598
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
599
- try log.cant(ident, .Paralysis);
600
- return .done;
601
- }
618
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
619
+ if (!volatiles.Rage) volatiles.state = 0;
620
+ volatiles.Bide = false;
621
+ volatiles.Thrashing = false;
622
+ volatiles.Charging = false;
623
+ volatiles.Binding = false;
624
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
625
+ try log.cant(ident, .Paralysis);
626
+ return .done;
602
627
  }
603
628
 
604
629
  if (volatiles.Bide) {
605
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
606
631
 
607
- volatiles.state +%= battle.last_damage;
632
+ if (showdown) {
633
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
+ // addition because 65535 is sufficient to faint any Pokémon anyway
636
+ volatiles.state +|= battle.last_damage;
637
+ } else {
638
+ volatiles.state +%= battle.last_damage;
639
+ }
608
640
 
609
641
  assert(volatiles.attacks > 0);
610
642
 
@@ -648,8 +680,13 @@ fn beforeMove(
648
680
  try log.start(battle.active(player), .ConfusionSilent);
649
681
  }
650
682
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
651
- if (showdown and handleThrashing(battle, active)) {
652
- try log.start(battle.active(player), .ConfusionSilent);
683
+ if (showdown) {
684
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
685
+ // used in useMove and doesn't have the notion of skipping canMove semantics
686
+ side.last_used_move = side.last_selected_move;
687
+ if (handleThrashing(battle, active)) {
688
+ try log.start(battle.active(player), .ConfusionSilent);
689
+ }
653
690
  }
654
691
  return .skip_can;
655
692
  }
@@ -664,7 +701,11 @@ fn beforeMove(
664
701
  side.last_selected_move,
665
702
  );
666
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
667
705
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
706
+ if (battle.foe(player).stored().hp > 0 and !sub) {
707
+ try buildRage(battle, player.foe(), log);
708
+ }
668
709
  }
669
710
  return .done;
670
711
  }
@@ -696,6 +737,7 @@ fn canMove(
696
737
  side.active.volatiles.Invulnerable = false;
697
738
  } else if (move.effect == .Charge) {
698
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
699
741
  try Effects.charge(battle, player, log);
700
742
  return false;
701
743
  }
@@ -712,6 +754,7 @@ fn canMove(
712
754
  // regrettable, but this information is more "nice to have" than required
713
755
  const f = if (from != null and from.? == .None) null else from;
714
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
715
758
 
716
759
  if (move.effect.onBegin()) {
717
760
  try onBegin(battle, player, move, mslot, residual, log);
@@ -722,12 +765,27 @@ fn canMove(
722
765
  Effects.thrashing(battle, player);
723
766
  } else if (!showdown and move.effect == .Binding) {
724
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
725
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
726
769
  }
727
770
 
728
771
  return true;
729
772
  }
730
773
 
774
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
+ // in cases where an early return happens the data for a players last selected move does not get
777
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
+ // as the desync results from actually having two separate battle states that subtly disagree
780
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
+ (move.type == .Normal or move.type == .Fighting);
782
+ if (player == .P1) {
783
+ battle.last_moves.p1_counterable = @boolToInt(counterable);
784
+ } else {
785
+ battle.last_moves.p2_counterable = @boolToInt(counterable);
786
+ }
787
+ }
788
+
731
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
732
790
  if (side.last_selected_move == .Struggle) return;
733
791
 
@@ -763,6 +821,7 @@ fn doMove(
763
821
  battle: anytype,
764
822
  player: Player,
765
823
  mslot: u4,
824
+ rewrap: bool,
766
825
  auto: bool,
767
826
  residual: *bool,
768
827
  log: anytype,
@@ -854,9 +913,9 @@ fn doMove(
854
913
 
855
914
  if (!showdown or !miss) {
856
915
  if (move.effect == .MirrorMove) {
857
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
858
917
  } else if (move.effect == .Metronome) {
859
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
860
919
  } else if (move.effect.onEnd()) {
861
920
  try onEnd(battle, player, move, log);
862
921
  return null;
@@ -871,7 +930,7 @@ fn doMove(
871
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
872
931
  if (ohko) {
873
932
  try log.immune(foe_ident, .OHKO);
874
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
875
934
  try log.immune(foe_ident, .None);
876
935
  } else if (mist) {
877
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -937,11 +996,17 @@ fn doMove(
937
996
  } else if (showdown) {
938
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
939
998
  if (move.effect == .Binding) {
940
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
941
1000
  if (immune) {
942
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
943
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
944
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1004
+ if (sub) {
1005
+ try log.activate(battle.active(player.foe()), .Substitute);
1006
+ } else {
1007
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try buildRage(battle, player.foe(), log);
1009
+ }
945
1010
  return null;
946
1011
  }
947
1012
  }
@@ -992,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
992
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
993
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
994
1059
 
995
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
996
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
997
1061
  }
998
1062
 
999
1063
  fn calcDamage(
@@ -1097,16 +1161,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1097
1161
 
1098
1162
  fn randomizeDamage(battle: anytype) void {
1099
1163
  if (battle.last_damage <= 1) return;
1100
-
1101
- const random = if (showdown)
1102
- battle.rng.range(u8, 217, 256)
1103
- else loop: {
1104
- while (true) {
1105
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1106
- if (r >= 217) break :loop r;
1107
- }
1108
- };
1109
-
1164
+ const random = Rolls.damage(battle);
1110
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1111
1166
  }
1112
1167
 
@@ -1124,16 +1179,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1124
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1125
1180
  .Psywave => power: {
1126
1181
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1127
- if (showdown) {
1128
- break :power battle.rng.range(u8, 0, max);
1129
- } else {
1130
- // GLITCH: Psywave infinite glitch loop
1131
- if (max <= 1) return Result.Error;
1132
- while (true) {
1133
- const r = battle.rng.next();
1134
- if (r < max) break :power r;
1135
- }
1136
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1137
1185
  },
1138
1186
  else => unreachable,
1139
1187
  };
@@ -1155,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1155
1203
  return null;
1156
1204
  }
1157
1205
 
1158
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1159
- const foe_last_used_move =
1160
- Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1206
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1161
1207
  const foe_last_selected_move =
1162
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1163
- .Counter
1209
+ .Splash
1164
1210
  else
1165
1211
  foe.last_selected_move);
1166
1212
 
1167
- const used = foe_last_used_move.bp > 0 and
1168
- foe.last_used_move != .Counter and
1169
- (foe_last_used_move.type == .Normal or
1170
- foe_last_used_move.type == .Fighting);
1213
+ const used = (if (player.foe() == .P1)
1214
+ battle.last_moves.p1_counterable
1215
+ else
1216
+ battle.last_moves.p2_counterable) != 0;
1171
1217
 
1172
1218
  const selected = foe_last_selected_move.bp > 0 and
1173
1219
  foe.last_selected_move != .Counter and
@@ -1186,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1186
1232
  return null;
1187
1233
  }
1188
1234
 
1189
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1190
1236
 
1191
1237
  // Pokémon Showdown calls checkHit before Counter
1192
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1193
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1194
1241
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1242
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1195
1243
  return null;
1196
1244
  }
1197
1245
 
@@ -1235,6 +1283,7 @@ fn mirrorMove(
1235
1283
  battle: anytype,
1236
1284
  player: Player,
1237
1285
  mslot: u4,
1286
+ rewrap: bool,
1238
1287
  auto: bool,
1239
1288
  residual: *bool,
1240
1289
  log: anytype,
@@ -1247,41 +1296,30 @@ fn mirrorMove(
1247
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1248
1297
  try log.fail(battle.active(player), .None);
1249
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1250
1301
  }
1251
1302
 
1252
- incrementPP(side, mslot);
1253
-
1254
1303
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1255
- return doMove(battle, player, mslot, auto, residual, log);
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1256
1305
  }
1257
1306
 
1258
1307
  fn metronome(
1259
1308
  battle: anytype,
1260
1309
  player: Player,
1261
1310
  mslot: u4,
1311
+ rewrap: bool,
1262
1312
  auto: bool,
1263
1313
  residual: *bool,
1264
1314
  log: anytype,
1265
1315
  ) !?Result {
1266
1316
  var side = battle.side(player);
1267
1317
 
1268
- side.last_selected_move = if (showdown) blk: {
1269
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1270
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1271
- break :blk @intToEnum(Move, r + mod);
1272
- } else loop: {
1273
- while (true) {
1274
- const r = battle.rng.next();
1275
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1276
- if (r >= @enumToInt(Move.Struggle)) continue;
1277
- break :loop @intToEnum(Move, r);
1278
- }
1279
- };
1280
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1281
1319
  incrementPP(side, mslot);
1282
1320
 
1283
1321
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1284
- return doMove(battle, player, mslot, auto, residual, log);
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1285
1323
  }
1286
1324
 
1287
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1352,10 +1390,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1352
1390
  if (overwrite) side.active.volatiles.state = accuracy;
1353
1391
 
1354
1392
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1355
- break :miss if (showdown)
1356
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1357
- else
1358
- battle.rng.next() >= accuracy;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1359
1394
  };
1360
1395
 
1361
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1491,11 +1526,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1491
1526
  stored.hp -= amount;
1492
1527
 
1493
1528
  // As above, Pokémon Showdown uses damageOf but its not relevant
1494
- if (amount > 0) {
1495
- try log.damage(ident, stored, .LeechSeed);
1496
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1497
- if (showdown) battle.last_damage = damage;
1498
- }
1529
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1499
1530
 
1500
1531
  const before = foe_stored.hp;
1501
1532
  // Uncapped damage is added back to the foe
@@ -1509,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1509
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1510
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1511
1542
 
1543
+ battle.turn += 1;
1544
+
1512
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1513
1546
  try log.tie();
1514
1547
  return Result.Tie;
1515
1548
  }
1516
1549
 
1517
- battle.turn += 1;
1518
-
1519
1550
  if (showdown and battle.turn >= 1000) {
1520
1551
  try log.tie();
1521
1552
  return Result.Tie;
@@ -1582,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
1582
1613
  return false;
1583
1614
  }
1584
1615
 
1616
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1617
+ if (side.active.volatiles.disabled_move != 0) {
1618
+ // A Pokémon that transforms after being disabled may end up with less move slots
1619
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1620
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1621
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1622
+ m
1623
+ else
1624
+ side.last_selected_move;
1625
+ if (m != .None and m == last) {
1626
+ side.active.volatiles.Charging = false;
1627
+ try log.disabled(ident, last);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1635
+ const side = battle.side(who);
1636
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1637
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1638
+ }
1639
+ }
1640
+
1641
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1642
+ var volatiles = &active.volatiles;
1643
+ assert(volatiles.Thrashing);
1644
+ if (volatiles.attacks > 0) return false;
1645
+
1646
+ volatiles.Thrashing = false;
1647
+ volatiles.Confusion = true;
1648
+ volatiles.confusion = Rolls.confusionDuration(battle);
1649
+ return true;
1650
+ }
1651
+
1585
1652
  inline fn onBegin(
1586
1653
  battle: anytype,
1587
1654
  player: Player,
@@ -1672,10 +1739,7 @@ pub const Effects = struct {
1672
1739
  side.active.volatiles.Bide = true;
1673
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1674
1741
  side.active.volatiles.state = 0;
1675
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1676
- battle.rng.range(u4, 2, 4)
1677
- else
1678
- (battle.rng.next() & 1) + 2);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1679
1743
 
1680
1744
  try log.start(battle.active(player), .Bide);
1681
1745
  }
@@ -1697,14 +1761,8 @@ pub const Effects = struct {
1697
1761
  return;
1698
1762
  }
1699
1763
 
1700
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1701
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1702
- else
1703
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1704
- Gen12.percent(10)
1705
- else
1706
- Gen12.percent(30));
1707
- if (!chance) return;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1708
1766
 
1709
1767
  foe_stored.status = Status.init(.BRN);
1710
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1728,11 +1786,7 @@ pub const Effects = struct {
1728
1786
  const sub = foe.active.volatiles.Substitute;
1729
1787
 
1730
1788
  if (move.effect == .ConfusionChance) {
1731
- const chance = if (showdown)
1732
- battle.rng.chance(u8, 25, 256)
1733
- else
1734
- battle.rng.next() < Gen12.percent(10);
1735
- if (!chance) return;
1789
+ if (!Rolls.confusionChance(battle)) return;
1736
1790
  } else {
1737
1791
  if (showdown) {
1738
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1751,10 +1805,7 @@ pub const Effects = struct {
1751
1805
 
1752
1806
  if (foe.active.volatiles.Confusion) return;
1753
1807
  foe.active.volatiles.Confusion = true;
1754
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1755
- battle.rng.range(u8, 2, 6)
1756
- else
1757
- (battle.rng.next() & 3) + 2);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1758
1809
 
1759
1810
  try log.start(battle.active(player.foe()), .Confusion);
1760
1811
  }
@@ -1804,11 +1855,8 @@ pub const Effects = struct {
1804
1855
  return Result.Error;
1805
1856
  }
1806
1857
 
1807
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1808
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1809
- battle.rng.range(u8, 1, 9)
1810
- else
1811
- (battle.rng.next() & 7) + 1);
1858
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1812
1860
 
1813
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1814
1862
  return null;
@@ -1847,15 +1895,7 @@ pub const Effects = struct {
1847
1895
  var volatiles = &battle.foe(player).active.volatiles;
1848
1896
 
1849
1897
  if (volatiles.Substitute) return;
1850
-
1851
- const chance = if (showdown)
1852
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1853
- else
1854
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1855
- Gen12.percent(10)
1856
- else
1857
- Gen12.percent(30));
1858
- if (!chance) return;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1859
1899
 
1860
1900
  volatiles.Flinch = true;
1861
1901
  volatiles.Recharging = false;
@@ -1881,12 +1921,8 @@ pub const Effects = struct {
1881
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1882
1922
  }
1883
1923
 
1884
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1885
- battle.rng.chance(u8, 26, 256)
1886
- else
1887
- battle.rng.next() < 1 + Gen12.percent(10);
1888
- if (!chance) return;
1889
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1890
1926
  // Freeze Clause Mod
1891
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1892
1928
 
@@ -2023,15 +2059,15 @@ pub const Effects = struct {
2023
2059
  }
2024
2060
  break :has_mimic false;
2025
2061
  };
2026
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2027
- // trigger |-miss| instead of |-fail|
2028
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2062
+ if (!has_mimic) {
2063
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2064
+ if (!try checkHit(battle, player, move, log)) return;
2029
2065
  return try log.fail(battle.active(player.foe()), .None);
2030
2066
  }
2031
2067
  }
2032
2068
  if (!try checkHit(battle, player, move, log)) return;
2033
2069
 
2034
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2035
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
2036
2072
 
2037
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2052,7 +2088,8 @@ pub const Effects = struct {
2052
2088
  assert(!side.active.volatiles.MultiHit);
2053
2089
  side.active.volatiles.MultiHit = true;
2054
2090
 
2055
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2091
+ side.active.volatiles.attacks =
2092
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2056
2093
  }
2057
2094
 
2058
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2096,14 +2133,8 @@ pub const Effects = struct {
2096
2133
  }
2097
2134
 
2098
2135
  // Body Slam can't paralyze a Normal type Pokémon
2099
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2100
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2101
- else
2102
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2103
- Gen12.percent(10)
2104
- else
2105
- Gen12.percent(30));
2106
- if (!chance) return;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2107
2138
 
2108
2139
  foe_stored.status = Status.init(.PAR);
2109
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2146,16 +2177,7 @@ pub const Effects = struct {
2146
2177
 
2147
2178
  if (move.effect == .Poison) {
2148
2179
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2149
- } else {
2150
- const chance = if (showdown)
2151
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2152
- else
2153
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2154
- Gen12.percent(20)
2155
- else
2156
- Gen12.percent(40));
2157
- if (!chance) return;
2158
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2159
2181
 
2160
2182
  foe_stored.status = Status.init(.PSN);
2161
2183
  if (toxic) {
@@ -2177,6 +2199,9 @@ pub const Effects = struct {
2177
2199
  var side = battle.side(player);
2178
2200
  var stored = side.stored();
2179
2201
 
2202
+ assert(showdown or battle.last_damage > 0);
2203
+ if (showdown and battle.last_damage == 0) return;
2204
+
2180
2205
  const damage = @intCast(i16, @max(battle.last_damage /
2181
2206
  @as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
2182
2207
  stored.hp = @intCast(u16, @max(@intCast(i16, stored.hp) - damage, 0));
@@ -2222,16 +2247,7 @@ pub const Effects = struct {
2222
2247
  }
2223
2248
  foe.active.volatiles.Recharging = false;
2224
2249
 
2225
- const duration = @intCast(u3, if (showdown)
2226
- battle.rng.range(u8, 1, 8)
2227
- else loop: {
2228
- while (true) {
2229
- const r = battle.rng.next() & 7;
2230
- if (r != 0) break :loop r;
2231
- }
2232
- });
2233
-
2234
- foe_stored.status = Status.slp(duration);
2250
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2235
2251
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2236
2252
  }
2237
2253
 
@@ -2284,10 +2300,7 @@ pub const Effects = struct {
2284
2300
  assert(!volatiles.Bide);
2285
2301
 
2286
2302
  volatiles.Thrashing = true;
2287
- volatiles.attacks = @intCast(u3, if (showdown)
2288
- battle.rng.range(u8, 2, 4)
2289
- else
2290
- (battle.rng.next() & 1) + 2);
2303
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2291
2304
  }
2292
2305
 
2293
2306
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2319,7 +2332,7 @@ pub const Effects = struct {
2319
2332
  try log.transform(battle.active(player), foe_ident);
2320
2333
  }
2321
2334
 
2322
- fn binding(battle: anytype, player: Player) void {
2335
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2323
2336
  var side = battle.side(player);
2324
2337
  var foe = battle.foe(player);
2325
2338
 
@@ -2327,9 +2340,11 @@ pub const Effects = struct {
2327
2340
  side.active.volatiles.Binding = true;
2328
2341
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2329
2342
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2330
- if (!showdown) foe.active.volatiles.Recharging = false;
2343
+ if (!showdown) {
2344
+ foe.active.volatiles.Recharging = false;
2345
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2331
2346
 
2332
- side.active.volatiles.attacks = distribution(battle) - 1;
2347
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2333
2348
  }
2334
2349
 
2335
2350
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2356,7 +2371,7 @@ pub const Effects = struct {
2356
2371
  return try log.fail(ident, .None);
2357
2372
  }
2358
2373
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2359
- const stat = unmodifiedStats(battle, player).atk;
2374
+ const stat = unmodifiedStats(battle, side).atk;
2360
2375
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2361
2376
  try log.boost(ident, reason, n);
2362
2377
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2376,7 +2391,7 @@ pub const Effects = struct {
2376
2391
  return try log.fail(ident, .None);
2377
2392
  }
2378
2393
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2379
- const stat = unmodifiedStats(battle, player).def;
2394
+ const stat = unmodifiedStats(battle, side).def;
2380
2395
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2381
2396
  try log.boost(ident, .Defense, n);
2382
2397
  },
@@ -2393,7 +2408,7 @@ pub const Effects = struct {
2393
2408
  return try log.fail(ident, .None);
2394
2409
  }
2395
2410
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2396
- const stat = unmodifiedStats(battle, player).spe;
2411
+ const stat = unmodifiedStats(battle, side).spe;
2397
2412
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2398
2413
  try log.boost(ident, .Speed, 2);
2399
2414
  },
@@ -2413,7 +2428,7 @@ pub const Effects = struct {
2413
2428
  return try log.fail(ident, .None);
2414
2429
  }
2415
2430
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2416
- const stat = unmodifiedStats(battle, player).spc;
2431
+ const stat = unmodifiedStats(battle, side).spc;
2417
2432
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2418
2433
  try log.boost(ident, .SpecialAttack, n);
2419
2434
  try log.boost(ident, .SpecialDefense, n);
@@ -2434,28 +2449,24 @@ pub const Effects = struct {
2434
2449
  fn unboost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2435
2450
  var foe = battle.foe(player);
2436
2451
  const foe_ident = battle.active(player.foe());
2452
+ const secondary = move.effect.isStatDownChance();
2437
2453
 
2438
- if (foe.active.volatiles.Substitute) {
2439
- return if (!move.effect.isStatDownChance()) log.fail(foe_ident, .None);
2440
- }
2454
+ if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
2441
2455
 
2442
- if (move.effect.isStatDownChance()) {
2443
- const chance = if (showdown)
2444
- battle.rng.chance(u8, 85, 256)
2445
- else
2446
- battle.rng.next() < Gen12.percent(33) + 1;
2447
- if (!chance or foe.active.volatiles.Invulnerable) return;
2456
+ if (secondary) {
2457
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2448
2458
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2449
2459
  return; // checkHit already checks for Invulnerable
2450
2460
  }
2451
2461
 
2452
2462
  var stats = &foe.active.stats;
2453
2463
  var boosts = &foe.active.boosts;
2464
+ const fail = showdown or !secondary;
2454
2465
 
2455
2466
  switch (move.effect) {
2456
2467
  .AttackDown1, .AttackDownChance => {
2457
2468
  assert(boosts.atk >= -6 and boosts.atk <= 6);
2458
- if (boosts.atk == -6) return try log.fail(foe_ident, .None);
2469
+ if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
2459
2470
  boosts.atk = @intCast(i4, @max(-6, @as(i8, boosts.atk) - 1));
2460
2471
  if (stats.atk == 1) {
2461
2472
  boosts.atk += 1;
@@ -2466,13 +2477,13 @@ pub const Effects = struct {
2466
2477
  return try log.fail(foe_ident, .None);
2467
2478
  }
2468
2479
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2469
- const stat = unmodifiedStats(battle, player.foe()).atk;
2480
+ const stat = unmodifiedStats(battle, foe).atk;
2470
2481
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2471
2482
  try log.boost(foe_ident, .Attack, -1);
2472
2483
  },
2473
2484
  .DefenseDown1, .DefenseDown2, .DefenseDownChance => {
2474
2485
  assert(boosts.def >= -6 and boosts.def <= 6);
2475
- if (boosts.def == -6) return try log.fail(foe_ident, .None);
2486
+ if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
2476
2487
  const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
2477
2488
  boosts.def = @intCast(i4, @max(-6, @as(i8, boosts.def) - n));
2478
2489
  if (stats.def == 1) {
@@ -2484,13 +2495,13 @@ pub const Effects = struct {
2484
2495
  return try log.fail(foe_ident, .None);
2485
2496
  }
2486
2497
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2487
- const stat = unmodifiedStats(battle, player.foe()).def;
2498
+ const stat = unmodifiedStats(battle, foe).def;
2488
2499
  stats.def = @max(1, stat * mod[0] / mod[1]);
2489
2500
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2490
2501
  },
2491
2502
  .SpeedDown1, .SpeedDownChance => {
2492
2503
  assert(boosts.spe >= -6 and boosts.spe <= 6);
2493
- if (boosts.spe == -6) return try log.fail(foe_ident, .None);
2504
+ if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
2494
2505
  boosts.spe = @intCast(i4, @max(-6, @as(i8, boosts.spe) - 1));
2495
2506
  if (stats.spe == 1) {
2496
2507
  boosts.spe += 1;
@@ -2501,14 +2512,14 @@ pub const Effects = struct {
2501
2512
  return try log.fail(foe_ident, .None);
2502
2513
  }
2503
2514
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2504
- const stat = unmodifiedStats(battle, player.foe()).spe;
2515
+ const stat = unmodifiedStats(battle, foe).spe;
2505
2516
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2506
2517
  try log.boost(foe_ident, .Speed, -1);
2507
2518
  assert(boosts.spe >= -6);
2508
2519
  },
2509
2520
  .SpecialDownChance => {
2510
2521
  assert(boosts.spc >= -6 and boosts.spc <= 6);
2511
- if (boosts.spc == -6) return try log.fail(foe_ident, .None);
2522
+ if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
2512
2523
  boosts.spc = @intCast(i4, @max(-6, @as(i8, boosts.spc) - 1));
2513
2524
  if (stats.spc == 1) {
2514
2525
  boosts.spc += 1;
@@ -2521,14 +2532,14 @@ pub const Effects = struct {
2521
2532
  return try log.fail(foe_ident, .None);
2522
2533
  }
2523
2534
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2524
- const stat = unmodifiedStats(battle, player.foe()).spc;
2535
+ const stat = unmodifiedStats(battle, foe).spc;
2525
2536
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2526
2537
  try log.boost(foe_ident, .SpecialAttack, -1);
2527
2538
  try log.boost(foe_ident, .SpecialDefense, -1);
2528
2539
  },
2529
2540
  .AccuracyDown1 => {
2530
2541
  assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
2531
- if (boosts.accuracy == -6) return try log.fail(foe_ident, .None);
2542
+ if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
2532
2543
  boosts.accuracy = @intCast(i4, @max(-6, @as(i8, boosts.accuracy) - 1));
2533
2544
  try log.boost(foe_ident, .Accuracy, -1);
2534
2545
  },
@@ -2540,8 +2551,7 @@ pub const Effects = struct {
2540
2551
  }
2541
2552
  };
2542
2553
 
2543
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2544
- const side = battle.side(who);
2554
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2545
2555
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2546
2556
  const id = ID.from(side.active.volatiles.transform);
2547
2557
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2607,80 +2617,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2607
2617
  }
2608
2618
  }
2609
2619
 
2610
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2611
- if (side.active.volatiles.disabled_move != 0) {
2612
- // A Pokémon that transforms after being disabled may end up with less move slots
2613
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2614
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2615
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2616
- m
2617
- else
2618
- side.last_selected_move;
2619
- if (m != .None and m == last) {
2620
- side.active.volatiles.Charging = false;
2621
- try log.disabled(ident, last);
2622
- return true;
2620
+ pub const Rolls = struct {
2621
+ inline fn speedTie(battle: anytype) bool {
2622
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2623
+ return battle.rng.next() < Gen12.percent(50) + 1;
2624
+ }
2625
+
2626
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2627
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2628
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2629
+ }
2630
+
2631
+ inline fn damage(battle: anytype) u8 {
2632
+ if (showdown) return battle.rng.range(u8, 217, 256);
2633
+ while (true) {
2634
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2635
+ if (r >= 217) return r;
2623
2636
  }
2624
2637
  }
2625
- return false;
2626
- }
2627
2638
 
2628
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2629
- const side = battle.side(who);
2630
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2631
- try Effects.boost(battle, who, Move.get(.Rage), log);
2639
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2640
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2641
+ return battle.rng.next() < accuracy;
2632
2642
  }
2633
- }
2634
2643
 
2635
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2636
- var volatiles = &active.volatiles;
2637
- assert(volatiles.Thrashing);
2638
- if (volatiles.attacks > 0) return false;
2644
+ inline fn confusionDuration(battle: anytype) u3 {
2645
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2646
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2647
+ }
2639
2648
 
2640
- volatiles.Thrashing = false;
2641
- volatiles.Confusion = true;
2642
- volatiles.confusion = @intCast(u3, if (showdown)
2643
- battle.rng.range(u8, 2, 6)
2644
- else
2645
- (battle.rng.next() & 3) + 2);
2649
+ inline fn confused(battle: anytype) bool {
2650
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2651
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2652
+ }
2646
2653
 
2647
- return true;
2648
- }
2654
+ inline fn paralyzed(battle: anytype) bool {
2655
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2656
+ return battle.rng.next() < Gen12.percent(25);
2657
+ }
2649
2658
 
2650
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2659
+ inline fn confusionChance(battle: anytype) bool {
2660
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2661
+ return battle.rng.next() < Gen12.percent(10);
2662
+ }
2651
2663
 
2652
- fn distribution(battle: anytype) u3 {
2653
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2654
- const r = (battle.rng.next() & 3);
2655
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2656
- }
2664
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2665
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2666
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2667
+ }
2657
2668
 
2658
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2659
- if (showdown) {
2660
- if (check_pp == 0) {
2661
- var i: usize = moves.len;
2662
- while (i > 0) {
2663
- i -= 1;
2664
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2665
- }
2666
- } else {
2667
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2668
- var i: usize = 0;
2669
- while (i < moves.len and r > 0) : (i += 1) {
2670
- if (moves[i].pp > 0) {
2671
- r -= 1;
2672
- if (r == 0) break;
2669
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2670
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2671
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2672
+ }
2673
+
2674
+ inline fn sleepDuration(battle: anytype) u3 {
2675
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2676
+ while (true) {
2677
+ const r = battle.rng.next() & 7;
2678
+ if (r != 0) return @intCast(u3, r);
2679
+ }
2680
+ }
2681
+
2682
+ inline fn bideThrashDuration(battle: anytype) u3 {
2683
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2684
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2685
+ }
2686
+
2687
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2688
+ if (showdown) {
2689
+ if (check_pp == 0) {
2690
+ var i: usize = moves.len;
2691
+ while (i > 0) {
2692
+ i -= 1;
2693
+ if (moves[i].id != .None) {
2694
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2695
+ }
2696
+ }
2697
+ } else {
2698
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2699
+ var i: usize = 0;
2700
+ while (i < moves.len and r > 0) : (i += 1) {
2701
+ if (moves[i].pp > 0) {
2702
+ r -= 1;
2703
+ if (r == 0) break;
2704
+ }
2673
2705
  }
2706
+ return @intCast(u4, i + 1);
2674
2707
  }
2675
- return @intCast(u4, i + 1);
2708
+ }
2709
+
2710
+ while (true) {
2711
+ const r = @intCast(u4, battle.rng.next() & 3);
2712
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2676
2713
  }
2677
2714
  }
2678
2715
 
2679
- while (true) {
2680
- const r = @intCast(u4, rand.next() & 3);
2681
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2716
+ inline fn disableDuration(battle: anytype) u4 {
2717
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2718
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2682
2719
  }
2683
- }
2720
+
2721
+ inline fn metronome(battle: anytype) Move {
2722
+ if (showdown) {
2723
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2724
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2725
+ return @intToEnum(Move, r + mod);
2726
+ }
2727
+ while (true) {
2728
+ const r = battle.rng.next();
2729
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2730
+ if (r >= @enumToInt(Move.Struggle)) continue;
2731
+ return @intToEnum(Move, r);
2732
+ }
2733
+ }
2734
+
2735
+ inline fn psywave(battle: anytype, max: u8) u8 {
2736
+ if (showdown) return battle.rng.range(u8, 0, max);
2737
+ while (true) {
2738
+ const r = battle.rng.next();
2739
+ if (r < max) return r;
2740
+ }
2741
+ }
2742
+
2743
+ inline fn distribution(battle: anytype) u3 {
2744
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2745
+ const r = (battle.rng.next() & 3);
2746
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2747
+ }
2748
+
2749
+ inline fn unboost(battle: anytype) bool {
2750
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2751
+ return battle.rng.next() < Gen12.percent(33) + 1;
2752
+ }
2753
+ };
2684
2754
 
2685
2755
  test "RNG agreement" {
2686
2756
  if (!showdown) return;