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

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;
@@ -129,26 +124,17 @@ fn selectMove(
129
124
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
130
125
 
131
126
  // pre-battle menu
132
- if (volatiles.Recharging) {
133
- if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
- return null;
135
- }
127
+ if (volatiles.Recharging) return null;
136
128
  if (volatiles.Rage) {
137
129
  from.* = side.last_used_move;
138
- if (showdown) {
139
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
- saveMove(battle, player, null);
141
- }
130
+ if (showdown) saveMove(battle, player, null);
142
131
  return null;
143
132
  }
144
133
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
145
134
  if (!showdown) volatiles.Flinch = false;
146
135
  if (volatiles.Thrashing or volatiles.Charging) {
147
136
  from.* = side.last_selected_move;
148
- if (showdown) {
149
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
- saveMove(battle, player, null);
151
- }
137
+ if (showdown) saveMove(battle, player, null);
152
138
  return null;
153
139
  }
154
140
 
@@ -158,10 +144,7 @@ fn selectMove(
158
144
  // pre-move select
159
145
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
160
146
  assert(showdown or choice.data == 0);
161
- if (showdown) {
162
- if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
- saveMove(battle, player, choice);
164
- }
147
+ if (showdown) saveMove(battle, player, choice);
165
148
  return null;
166
149
  }
167
150
  if (volatiles.Binding) {
@@ -180,9 +163,9 @@ fn selectMove(
180
163
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
181
164
  if (foe_choice.type == .Switch) {
182
165
  const slot = if (player == .P1)
183
- battle.last_moves.p1_index
166
+ battle.last_selected_indexes.p1
184
167
  else
185
- battle.last_moves.p2_index;
168
+ battle.last_selected_indexes.p2;
186
169
  const last = side.active.move(@intCast(u4, slot));
187
170
  if (last.id == .Metronome) side.last_selected_move = last.id;
188
171
  if (last.id == .MirrorMove) return Result.Error;
@@ -192,7 +175,6 @@ fn selectMove(
192
175
  }
193
176
 
194
177
  if (battle.foe(player).active.volatiles.Binding) {
195
- skip_turn.* = true;
196
178
  if (showdown) {
197
179
  saveMove(battle, player, choice);
198
180
  } else {
@@ -203,7 +185,6 @@ fn selectMove(
203
185
  }
204
186
 
205
187
  // move select
206
- volatiles.state = 0;
207
188
  if (choice.data == 0) {
208
189
  const struggle = ok: {
209
190
  for (side.active.moves, 0..) |move, i| {
@@ -237,9 +218,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
237
218
 
238
219
  side.last_selected_move = move.id;
239
220
  if (player == .P1) {
240
- battle.last_moves.p1_index = @intCast(u3, c.data);
221
+ battle.last_selected_indexes.p1 = @intCast(u4, c.data);
241
222
  } else {
242
- battle.last_moves.p2_index = @intCast(u3, c.data);
223
+ battle.last_selected_indexes.p2 = @intCast(u4, c.data);
243
224
  }
244
225
  }
245
226
  }
@@ -259,9 +240,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
259
240
  side.order[slot - 1] = out;
260
241
 
261
242
  if (player == .P1) {
262
- battle.last_moves.p1_index = 1;
243
+ battle.last_selected_indexes.p1 = 1;
263
244
  } else {
264
- battle.last_moves.p2_index = 1;
245
+ battle.last_selected_indexes.p2 = 1;
265
246
  }
266
247
 
267
248
  side.last_used_move = .None;
@@ -320,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
320
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
302
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
322
303
 
323
- 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
+
324
309
  if (!showdown) return if (p1) .P1 else .P2;
325
310
 
326
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -330,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
330
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
331
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
332
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
333
- // 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
334
319
  // won the original coin flip
335
320
  if (t1 and t2) return .P1;
336
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -343,13 +328,9 @@ fn doTurn(
343
328
  battle: anytype,
344
329
  player: Player,
345
330
  player_choice: Choice,
346
- player_rewrap: bool,
347
- player_skip: bool,
348
331
  player_from: ?Move,
349
332
  foe_player: Player,
350
333
  foe_choice: Choice,
351
- foe_rewrap: bool,
352
- foe_skip: bool,
353
334
  foe_from: ?Move,
354
335
  log: anytype,
355
336
  ) !?Result {
@@ -357,16 +338,7 @@ fn doTurn(
357
338
 
358
339
  var residual = true;
359
340
  var replace = battle.side(player).stored().hp == 0;
360
- if (try executeMove(
361
- battle,
362
- player,
363
- player_choice,
364
- player_rewrap,
365
- player_skip,
366
- player_from,
367
- &residual,
368
- log,
369
- )) |r| return r;
341
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
370
342
  if (!replace) {
371
343
  if (player_choice.type != .Switch) {
372
344
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -375,18 +347,8 @@ fn doTurn(
375
347
  if (try checkFaint(battle, player, log)) |r| return r;
376
348
  } else if (foe_choice.type == .Pass) return null;
377
349
 
378
- residual = true;
379
350
  replace = battle.side(foe_player).stored().hp == 0;
380
- if (try executeMove(
381
- battle,
382
- foe_player,
383
- foe_choice,
384
- foe_rewrap,
385
- foe_skip,
386
- foe_from,
387
- &residual,
388
- log,
389
- )) |r| return r;
351
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
390
352
  if (!replace) {
391
353
  if (foe_choice.type != .Switch) {
392
354
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -409,8 +371,6 @@ fn executeMove(
409
371
  battle: anytype,
410
372
  player: Player,
411
373
  choice: Choice,
412
- rewrap: bool,
413
- skip: bool,
414
374
  from: ?Move,
415
375
  residual: *bool,
416
376
  log: anytype,
@@ -422,10 +382,9 @@ fn executeMove(
422
382
  return null;
423
383
  }
424
384
 
425
- // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
426
- // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
427
- // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
428
385
  if (side.last_selected_move == .SKIP_TURN) {
386
+ // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
+ // Binding instead gets handled in beforeMove after sleep and freeze
429
388
  assert(!showdown);
430
389
  if (battle.foe(player).active.volatiles.Binding) {
431
390
  try log.cant(battle.active(player), .Bound);
@@ -444,9 +403,9 @@ fn executeMove(
444
403
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
445
404
  assert(!showdown);
446
405
  mslot = @intCast(u4, if (player == .P1)
447
- battle.last_moves.p1_index
406
+ battle.last_selected_indexes.p1
448
407
  else
449
- battle.last_moves.p2_index);
408
+ battle.last_selected_indexes.p2);
450
409
  const stored = side.stored();
451
410
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
452
411
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -468,14 +427,14 @@ fn executeMove(
468
427
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
469
428
  assert(mslot == 1);
470
429
  mslot = @intCast(u4, if (player == .P1)
471
- battle.last_moves.p1_index
430
+ battle.last_selected_indexes.p1
472
431
  else
473
- battle.last_moves.p2_index);
432
+ battle.last_selected_indexes.p2);
474
433
  }
475
434
 
476
435
  var skip_can = false;
477
436
  var skip_pp = false;
478
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
437
+ switch (try beforeMove(battle, player, from, log)) {
479
438
  .done => return null,
480
439
  .skip_can => skip_can = true,
481
440
  .skip_pp => skip_pp = true,
@@ -483,23 +442,14 @@ fn executeMove(
483
442
  .err => return @as(?Result, Result.Error),
484
443
  }
485
444
 
486
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
487
- return null;
488
- }
445
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
489
446
 
490
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
447
+ return doMove(battle, player, mslot, auto, residual, log);
491
448
  }
492
449
 
493
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
450
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
494
451
 
495
- fn beforeMove(
496
- battle: anytype,
497
- player: Player,
498
- skip: bool,
499
- from: ?Move,
500
- residual: *bool,
501
- log: anytype,
502
- ) !BeforeMove {
452
+ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
503
453
  var side = battle.side(player);
504
454
  const foe = battle.foe(player);
505
455
  var active = &side.active;
@@ -527,15 +477,13 @@ fn beforeMove(
527
477
  return .done;
528
478
  }
529
479
 
530
- if (skip or foe.active.volatiles.Binding) {
480
+ if (foe.active.volatiles.Binding) {
531
481
  try log.cant(ident, .Bound);
532
482
  return .done;
533
483
  }
534
484
 
535
- if (volatiles.Flinch) {
536
- // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
537
- // Pokémon can sometimes flinch multiple times from the same original hit
538
- if (!showdown) volatiles.Flinch = false;
485
+ if (!showdown and volatiles.Flinch) {
486
+ volatiles.Flinch = false;
539
487
  try log.cant(ident, .Flinch);
540
488
  return .done;
541
489
  }
@@ -556,6 +504,13 @@ fn beforeMove(
556
504
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
557
505
  if (showdown and try disabled(side, ident, log)) return .done;
558
506
 
507
+ // Pokémon Showdown checks for Flinch *after* instead of before
508
+ if (showdown and volatiles.Flinch) {
509
+ volatiles.Flinch = false;
510
+ try log.cant(ident, .Flinch);
511
+ return .done;
512
+ }
513
+
559
514
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
560
515
  // before the side had a selected a move - we simply need to assume this leads to a desync
561
516
  if (side.last_selected_move == .None) {
@@ -573,9 +528,14 @@ fn beforeMove(
573
528
  } else {
574
529
  try log.activate(ident, .Confusion);
575
530
 
576
- if (Rolls.confused(battle)) {
531
+ const confused = if (showdown)
532
+ !battle.rng.chance(u8, 128, 256)
533
+ else
534
+ battle.rng.next() >= Gen12.percent(50) + 1;
535
+
536
+ if (confused) {
577
537
  assert(!volatiles.MultiHit);
578
- if (!volatiles.Rage) volatiles.state = 0;
538
+ volatiles.state = 0;
579
539
  volatiles.Bide = false;
580
540
  volatiles.Thrashing = false;
581
541
  volatiles.MultiHit = false;
@@ -599,12 +559,8 @@ fn beforeMove(
599
559
  player
600
560
  else
601
561
  player.foe();
602
-
603
- const uncapped = battle.last_damage;
604
562
  // Skipping adjustDamage / randomizeDamage / checkHit
605
563
  _ = try applyDamage(battle, player, target, .Confusion, log);
606
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
- if (showdown) battle.last_damage = uncapped;
608
564
 
609
565
  return .done;
610
566
  }
@@ -613,15 +569,22 @@ fn beforeMove(
613
569
 
614
570
  if (!showdown and try disabled(side, ident, log)) return .done;
615
571
 
616
- if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
617
- if (!volatiles.Rage) volatiles.state = 0;
618
- volatiles.Bide = false;
619
- volatiles.Thrashing = false;
620
- volatiles.Charging = false;
621
- volatiles.Binding = false;
622
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
623
- try log.cant(ident, .Paralysis);
624
- return .done;
572
+ if (Status.is(stored.status, .PAR)) {
573
+ const paralyzed = if (showdown)
574
+ battle.rng.chance(u8, 63, 256)
575
+ else
576
+ battle.rng.next() < Gen12.percent(25);
577
+
578
+ if (paralyzed) {
579
+ volatiles.state = 0;
580
+ volatiles.Bide = false;
581
+ volatiles.Thrashing = false;
582
+ volatiles.Charging = false;
583
+ volatiles.Binding = false;
584
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
585
+ try log.cant(ident, .Paralysis);
586
+ return .done;
587
+ }
625
588
  }
626
589
 
627
590
  if (volatiles.Bide) {
@@ -648,18 +611,7 @@ fn beforeMove(
648
611
  return .done;
649
612
  }
650
613
 
651
- const sub = showdown and foe.active.volatiles.Substitute;
652
614
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
653
- if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
654
-
655
- // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
656
- // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
657
- // run residual here regardless of whether the foe fainted and opt-out of the default flow
658
- if (showdown) {
659
- residual.* = false;
660
- try handleResidual(battle, player, log);
661
- }
662
-
663
615
  return .done;
664
616
  }
665
617
 
@@ -671,13 +623,8 @@ fn beforeMove(
671
623
  try log.start(battle.active(player), .ConfusionSilent);
672
624
  }
673
625
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
674
- if (showdown) {
675
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
- // used in useMove and doesn't have the notion of skipping canMove semantics
677
- side.last_used_move = side.last_selected_move;
678
- if (handleThrashing(battle, active)) {
679
- try log.start(battle.active(player), .ConfusionSilent);
680
- }
626
+ if (showdown and handleThrashing(battle, active)) {
627
+ try log.start(battle.active(player), .ConfusionSilent);
681
628
  }
682
629
  return .skip_can;
683
630
  }
@@ -692,11 +639,7 @@ fn beforeMove(
692
639
  side.last_selected_move,
693
640
  );
694
641
  if (showdown or battle.last_damage != 0) {
695
- const sub = showdown and foe.active.volatiles.Substitute;
696
642
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
697
- if (battle.foe(player).stored().hp > 0 and !sub) {
698
- try buildRage(battle, player.foe(), log);
699
- }
700
643
  }
701
644
  return .done;
702
645
  }
@@ -711,7 +654,6 @@ fn canMove(
711
654
  auto: bool,
712
655
  skip_pp: bool,
713
656
  from: ?Move,
714
- residual: *bool,
715
657
  log: anytype,
716
658
  ) !bool {
717
659
  var side = battle.side(player);
@@ -745,21 +687,8 @@ fn canMove(
745
687
  const f = if (from != null and from.? == .None) null else from;
746
688
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
747
689
 
748
- // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
749
- // in cases where an early return happens the data for a players last selected move does not get
750
- // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
751
- // This boolean state we track here doesn't exist on the cartridge because it instead manifests
752
- // as the desync results from actually having two separate battle states that subtly disagree.
753
- const counterable = side.last_selected_move != .Counter and move.bp > 0 and
754
- (move.type == .Normal or move.type == .Fighting);
755
- if (player == .P1) {
756
- battle.last_moves.p1_counterable = @boolToInt(counterable);
757
- } else {
758
- battle.last_moves.p2_counterable = @boolToInt(counterable);
759
- }
760
-
761
690
  if (move.effect.onBegin()) {
762
- try onBegin(battle, player, move, mslot, residual, log);
691
+ try onBegin(battle, player, move, mslot, log);
763
692
  return false;
764
693
  }
765
694
 
@@ -767,7 +696,7 @@ fn canMove(
767
696
  Effects.thrashing(battle, player);
768
697
  } else if (!showdown and move.effect == .Binding) {
769
698
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
770
- Effects.binding(battle, player, false);
699
+ Effects.binding(battle, player);
771
700
  }
772
701
 
773
702
  return true;
@@ -808,7 +737,6 @@ fn doMove(
808
737
  battle: anytype,
809
738
  player: Player,
810
739
  mslot: u4,
811
- rewrap: bool,
812
740
  auto: bool,
813
741
  residual: *bool,
814
742
  log: anytype,
@@ -839,21 +767,16 @@ fn doMove(
839
767
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
840
768
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
841
769
  if (immune and move.effect != .Binding) break :miss true;
842
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
843
- battle.last_damage = 0;
844
- break :miss true;
845
- }
770
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
846
771
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
847
772
  };
848
773
  assert(!immune or miss or (showdown and move.effect == .Binding));
849
774
 
850
- var late = showdown and move.effect != .Explode;
851
775
  const skip = status or immune;
852
776
  if ((!showdown or (!skip or counter)) and !miss) blk: {
853
777
  if (showdown and move.effect.isMulti()) {
854
778
  Effects.multiHit(battle, player, move);
855
779
  hits = side.active.volatiles.attacks;
856
- late = false;
857
780
  }
858
781
 
859
782
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -887,7 +810,7 @@ fn doMove(
887
810
  }
888
811
 
889
812
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
890
- if (showdown and skip) battle.last_damage = 0;
813
+ if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
891
814
 
892
815
  miss = if (showdown or skip)
893
816
  miss
@@ -900,9 +823,9 @@ fn doMove(
900
823
 
901
824
  if (!showdown or !miss) {
902
825
  if (move.effect == .MirrorMove) {
903
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
826
+ return mirrorMove(battle, player, mslot, auto, residual, log);
904
827
  } else if (move.effect == .Metronome) {
905
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
828
+ return metronome(battle, player, mslot, auto, residual, log);
906
829
  } else if (move.effect.onEnd()) {
907
830
  try onEnd(battle, player, move, log);
908
831
  return null;
@@ -917,7 +840,7 @@ fn doMove(
917
840
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
918
841
  if (ohko) {
919
842
  try log.immune(foe_ident, .OHKO);
920
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
843
+ } else if (immune and !invulnerable) {
921
844
  try log.immune(foe_ident, .None);
922
845
  } else if (mist) {
923
846
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -934,9 +857,15 @@ fn doMove(
934
857
  if (showdown and side.stored().hp == 0) residual.* = false;
935
858
  } else if (move.effect == .Explode) {
936
859
  try Effects.explode(battle, player);
937
- try buildRage(battle, player.foe(), log);
938
- } else if (showdown and move.effect == .Disable) {
939
- try buildRage(battle, player.foe(), log);
860
+ if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
861
+ try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
862
+ }
863
+ } else if (showdown) {
864
+ if (move.effect == .Disable) {
865
+ if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
866
+ try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
867
+ }
868
+ }
940
869
  }
941
870
  return null;
942
871
  }
@@ -949,9 +878,6 @@ fn doMove(
949
878
  hits = side.active.volatiles.attacks;
950
879
  }
951
880
 
952
- // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
953
- const sub = showdown and foe.active.volatiles.Substitute;
954
-
955
881
  var nullified = false;
956
882
  var hit: u4 = 0;
957
883
  while (hit < hits) {
@@ -967,7 +893,9 @@ fn doMove(
967
893
  if (hit == 0 and ohko) try log.ohko();
968
894
  hit += 1;
969
895
  if (foe.stored().hp == 0) break;
970
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
896
+ if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
897
+ try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
898
+ }
971
899
  // If the substitute breaks during a multi-hit attack, the attack ends
972
900
  if (nullified) break;
973
901
  }
@@ -983,17 +911,11 @@ fn doMove(
983
911
  } else if (showdown) {
984
912
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
985
913
  if (move.effect == .Binding) {
986
- Effects.binding(battle, player, rewrap);
914
+ Effects.binding(battle, player);
987
915
  if (immune) {
988
916
  battle.last_damage = 0;
989
- assert(foe.stored().hp > 0);
990
917
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
991
- if (sub) {
992
- try log.activate(battle.active(player.foe()), .Substitute);
993
- } else {
994
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
995
- try buildRage(battle, player.foe(), log);
996
- }
918
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
997
919
  return null;
998
920
  }
999
921
  }
@@ -1010,9 +932,6 @@ fn doMove(
1010
932
 
1011
933
  if (foe.stored().hp == 0) return null;
1012
934
 
1013
- // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1014
- if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1015
-
1016
935
  if (!move.effect.isSpecial()) {
1017
936
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
1018
937
  // second time here (after being executed in the "always happens" block above) but that
@@ -1024,9 +943,7 @@ fn doMove(
1024
943
  if (move.effect == .Twineedle) {
1025
944
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1026
945
  } else if (move.effect == .Disable) {
1027
- const result = try Effects.disable(battle, player, move, log);
1028
- if (showdown) try buildRage(battle, player.foe(), log);
1029
- return result;
946
+ return Effects.disable(battle, player, move, log);
1030
947
  } else {
1031
948
  try moveEffect(battle, player, move, log);
1032
949
  }
@@ -1044,7 +961,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1044
961
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1045
962
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1046
963
 
1047
- return Rolls.criticalHit(battle, chance);
964
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1048
966
  }
1049
967
 
1050
968
  fn calcDamage(
@@ -1148,37 +1066,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1148
1066
 
1149
1067
  fn randomizeDamage(battle: anytype) void {
1150
1068
  if (battle.last_damage <= 1) return;
1151
- const random = Rolls.damage(battle);
1069
+
1070
+ const random = if (showdown)
1071
+ battle.rng.range(u8, 217, 256)
1072
+ else loop: {
1073
+ while (true) {
1074
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1075
+ if (r >= 217) break :loop r;
1076
+ }
1077
+ };
1078
+
1152
1079
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1153
1080
  }
1154
1081
 
1155
1082
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1156
1083
  const side = battle.side(player);
1157
- const foe = battle.foe(player);
1158
1084
 
1159
1085
  if (!try checkHit(battle, player, move, log)) return null;
1160
1086
 
1161
1087
  battle.last_damage = switch (side.last_selected_move) {
1162
- .SuperFang => @max(foe.stored().hp / 2, 1),
1088
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1163
1089
  .SeismicToss, .NightShade => side.stored().level,
1164
1090
  .SonicBoom => 20,
1165
1091
  .DragonRage => 40,
1166
1092
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1167
1093
  .Psywave => power: {
1168
1094
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1169
- // GLITCH: Psywave infinite glitch loop
1170
- if (!showdown and max <= 1) return Result.Error;
1171
- break :power Rolls.psywave(battle, max);
1095
+ if (showdown) {
1096
+ break :power battle.rng.range(u8, 0, max);
1097
+ } else {
1098
+ // GLITCH: Psywave infinite glitch loop
1099
+ if (max <= 1) return Result.Error;
1100
+ while (true) {
1101
+ const r = battle.rng.next();
1102
+ if (r < max) break :power r;
1103
+ }
1104
+ }
1172
1105
  },
1173
1106
  else => unreachable,
1174
1107
  };
1175
1108
 
1176
1109
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1177
1110
 
1178
- const sub = showdown and foe.active.volatiles.Substitute;
1179
1111
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1180
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1181
-
1182
1112
  return null;
1183
1113
  }
1184
1114
 
@@ -1190,17 +1120,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1190
1120
  return null;
1191
1121
  }
1192
1122
 
1193
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1123
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1124
+ const foe_last_used_move =
1125
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1194
1126
  const foe_last_selected_move =
1195
1127
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1196
- .Splash
1128
+ .Counter
1197
1129
  else
1198
1130
  foe.last_selected_move);
1199
1131
 
1200
- const used = (if (player.foe() == .P1)
1201
- battle.last_moves.p1_counterable
1202
- else
1203
- battle.last_moves.p2_counterable) != 0;
1132
+ const used = foe_last_used_move.bp > 0 and
1133
+ foe.last_used_move != .Counter and
1134
+ (foe_last_used_move.type == .Normal or
1135
+ foe_last_used_move.type == .Fighting);
1204
1136
 
1205
1137
  const selected = foe_last_selected_move.bp > 0 and
1206
1138
  foe.last_selected_move != .Counter and
@@ -1224,9 +1156,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1224
1156
  // Pokémon Showdown calls checkHit before Counter
1225
1157
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1226
1158
 
1227
- const sub = showdown and foe.active.volatiles.Substitute;
1228
1159
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1229
- if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1230
1160
  return null;
1231
1161
  }
1232
1162
 
@@ -1270,7 +1200,6 @@ fn mirrorMove(
1270
1200
  battle: anytype,
1271
1201
  player: Player,
1272
1202
  mslot: u4,
1273
- rewrap: bool,
1274
1203
  auto: bool,
1275
1204
  residual: *bool,
1276
1205
  log: anytype,
@@ -1283,30 +1212,41 @@ fn mirrorMove(
1283
1212
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1284
1213
  try log.fail(battle.active(player), .None);
1285
1214
  return null;
1286
- } else if (!showdown or foe.last_used_move != .Struggle) {
1287
- incrementPP(side, mslot);
1288
1215
  }
1289
1216
 
1290
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1291
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1217
+ incrementPP(side, mslot);
1218
+
1219
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1220
+ return doMove(battle, player, mslot, auto, residual, log);
1292
1221
  }
1293
1222
 
1294
1223
  fn metronome(
1295
1224
  battle: anytype,
1296
1225
  player: Player,
1297
1226
  mslot: u4,
1298
- rewrap: bool,
1299
1227
  auto: bool,
1300
1228
  residual: *bool,
1301
1229
  log: anytype,
1302
1230
  ) !?Result {
1303
1231
  var side = battle.side(player);
1304
1232
 
1305
- side.last_selected_move = Rolls.metronome(battle);
1233
+ side.last_selected_move = if (showdown) blk: {
1234
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1235
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1236
+ break :blk @intToEnum(Move, r + mod);
1237
+ } else loop: {
1238
+ while (true) {
1239
+ const r = battle.rng.next();
1240
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1241
+ if (r >= @enumToInt(Move.Struggle)) continue;
1242
+ break :loop @intToEnum(Move, r);
1243
+ }
1244
+ };
1245
+
1306
1246
  incrementPP(side, mslot);
1307
1247
 
1308
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1309
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1248
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
+ return doMove(battle, player, mslot, auto, residual, log);
1310
1250
  }
1311
1251
 
1312
1252
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1338,9 +1278,7 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1338
1278
  if (move.effect == .Swift) return true;
1339
1279
  if (foe.active.volatiles.Invulnerable) break :miss true;
1340
1280
  }
1341
- if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1342
- (showdown and foe.active.volatiles.Substitute)))
1343
- {
1281
+ if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1344
1282
  immune.* = true;
1345
1283
  if (showdown) return false;
1346
1284
  break :miss true;
@@ -1360,24 +1298,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1360
1298
  }
1361
1299
 
1362
1300
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1363
- const state = side.active.volatiles.state;
1364
- var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1365
- const overwritten = overwrite and state > 0;
1366
- assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1367
-
1368
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1301
+ var state = &side.active.volatiles.state;
1302
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1303
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1304
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1369
1305
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1370
1306
  accuracy = accuracy * boost[0] / boost[1];
1371
1307
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1372
1308
  accuracy = accuracy * boost[0] / boost[1];
1373
1309
  accuracy = @min(255, @max(1, accuracy));
1374
1310
 
1375
- // Pokémon Showdown only overwrites if the volatile is present
1376
- if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1377
- if (overwrite) side.active.volatiles.state = accuracy;
1311
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1378
1312
 
1379
1313
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1380
- break :miss !Rolls.hit(battle, accuracy);
1314
+ break :miss if (showdown)
1315
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1316
+ else
1317
+ battle.rng.next() >= accuracy;
1381
1318
  };
1382
1319
 
1383
1320
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1492,15 +1429,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1492
1429
  }
1493
1430
 
1494
1431
  if (volatiles.LeechSeed) {
1495
- var foe = battle.foe(player);
1496
- var foe_stored = foe.stored();
1497
- const foe_ident = battle.active(player.foe());
1498
-
1499
- if (foe_stored.hp == 0) {
1500
- assert(showdown);
1501
- return;
1502
- }
1503
-
1504
1432
  var damage = @max(stored.stats.hp / 16, 1);
1505
1433
 
1506
1434
  // GLITCH: Leech Seed + Toxic glitch
@@ -1512,6 +1440,10 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1512
1440
  const amount = @min(damage, stored.hp);
1513
1441
  stored.hp -= amount;
1514
1442
 
1443
+ var foe = battle.foe(player);
1444
+ var foe_stored = foe.stored();
1445
+ const foe_ident = battle.active(player.foe());
1446
+
1515
1447
  // As above, Pokémon Showdown uses damageOf but its not relevant
1516
1448
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1517
1449
 
@@ -1548,20 +1480,16 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1548
1480
 
1549
1481
  fn checkEBC(battle: anytype) bool {
1550
1482
  for (battle.sides, 0..) |side, i| {
1551
- const foe = battle.sides[~@intCast(u1, i)];
1552
-
1553
1483
  var foe_all_ghosts = true;
1554
1484
  var foe_all_transform = true;
1555
- for (foe.order, 0..) |id, j| {
1556
- if (id == 0) break;
1557
- const active = j == 0;
1558
- const pokemon = foe.pokemon[id - 1];
1559
1485
 
1560
- const ghost = pokemon.hp == 0 or
1561
- (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1486
+ for (battle.sides[~@intCast(u1, i)].pokemon) |pokemon| {
1487
+ if (pokemon.species == .None) continue;
1488
+
1489
+ const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
1562
1490
  foe_all_ghosts = foe_all_ghosts and ghost;
1563
1491
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1564
- for (if (active) foe.active.moves else pokemon.moves) |m| {
1492
+ for (pokemon.moves) |m| {
1565
1493
  if (m.id == .None) break :transform true;
1566
1494
  if (m.id != .Transform) break :transform false;
1567
1495
  }
@@ -1569,14 +1497,10 @@ fn checkEBC(battle: anytype) bool {
1569
1497
  };
1570
1498
  }
1571
1499
 
1572
- for (side.order, 0..) |id, j| {
1573
- if (id == 0) break;
1574
- const active = j == 0;
1575
- const pokemon = side.pokemon[id - 1];
1576
-
1500
+ for (side.pokemon) |pokemon| {
1577
1501
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1578
1502
  const transform = foe_all_transform and transform: {
1579
- for (if (active) side.active.moves else pokemon.moves) |m| {
1503
+ for (pokemon.moves) |m| {
1580
1504
  if (m.id == .None) break :transform true;
1581
1505
  if (m.id != .Transform) break :transform false;
1582
1506
  }
@@ -1584,7 +1508,7 @@ fn checkEBC(battle: anytype) bool {
1584
1508
  };
1585
1509
  if (transform) continue;
1586
1510
  const no_pp = foe_all_ghosts and no_pp: {
1587
- for (if (active) side.active.moves else pokemon.moves) |m| {
1511
+ for (pokemon.moves) |m| {
1588
1512
  if (m.pp != 0) break :no_pp false;
1589
1513
  }
1590
1514
  break :no_pp true;
@@ -1600,50 +1524,7 @@ fn checkEBC(battle: anytype) bool {
1600
1524
  return false;
1601
1525
  }
1602
1526
 
1603
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1604
- if (side.active.volatiles.disabled_move != 0) {
1605
- // A Pokémon that transforms after being disabled may end up with less move slots
1606
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1607
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1608
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1609
- m
1610
- else
1611
- side.last_selected_move;
1612
- if (m != .None and m == last) {
1613
- side.active.volatiles.Charging = false;
1614
- try log.disabled(ident, last);
1615
- return true;
1616
- }
1617
- }
1618
- return false;
1619
- }
1620
-
1621
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1622
- const side = battle.side(who);
1623
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1624
- try Effects.boost(battle, who, Move.get(.Rage), log);
1625
- }
1626
- }
1627
-
1628
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1629
- var volatiles = &active.volatiles;
1630
- assert(volatiles.Thrashing);
1631
- if (volatiles.attacks > 0) return false;
1632
-
1633
- volatiles.Thrashing = false;
1634
- volatiles.Confusion = true;
1635
- volatiles.confusion = Rolls.confusionDuration(battle);
1636
- return true;
1637
- }
1638
-
1639
- inline fn onBegin(
1640
- battle: anytype,
1641
- player: Player,
1642
- move: Move.Data,
1643
- mslot: u8,
1644
- residual: *bool,
1645
- log: anytype,
1646
- ) !void {
1527
+ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1647
1528
  assert(move.effect.onBegin());
1648
1529
  return switch (move.effect) {
1649
1530
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1659,7 +1540,7 @@ inline fn onBegin(
1659
1540
  .Poison => Effects.poison(battle, player, move, log),
1660
1541
  .Reflect => Effects.reflect(battle, player, log),
1661
1542
  .Splash => Effects.splash(battle, player, log),
1662
- .Substitute => Effects.substitute(battle, player, residual, log),
1543
+ .Substitute => Effects.substitute(battle, player, log),
1663
1544
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1664
1545
  .Transform => Effects.transform(battle, player, log),
1665
1546
  else => unreachable,
@@ -1726,7 +1607,10 @@ pub const Effects = struct {
1726
1607
  side.active.volatiles.Bide = true;
1727
1608
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1728
1609
  side.active.volatiles.state = 0;
1729
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1610
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1611
+ battle.rng.range(u4, 2, 4)
1612
+ else
1613
+ (battle.rng.next() & 1) + 2);
1730
1614
 
1731
1615
  try log.start(battle.active(player), .Bide);
1732
1616
  }
@@ -1748,8 +1632,14 @@ pub const Effects = struct {
1748
1632
  return;
1749
1633
  }
1750
1634
 
1751
- if (foe.active.types.includes(move.type)) return;
1752
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1635
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1636
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1637
+ else
1638
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1639
+ Gen12.percent(10)
1640
+ else
1641
+ Gen12.percent(30));
1642
+ if (!chance) return;
1753
1643
 
1754
1644
  foe_stored.status = Status.init(.BRN);
1755
1645
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1773,7 +1663,11 @@ pub const Effects = struct {
1773
1663
  const sub = foe.active.volatiles.Substitute;
1774
1664
 
1775
1665
  if (move.effect == .ConfusionChance) {
1776
- if (!Rolls.confusionChance(battle)) return;
1666
+ const chance = if (showdown)
1667
+ battle.rng.chance(u8, 25, 256)
1668
+ else
1669
+ battle.rng.next() < Gen12.percent(10);
1670
+ if (!chance) return;
1777
1671
  } else {
1778
1672
  if (showdown) {
1779
1673
  if (!try checkHit(battle, player, move, log)) {
@@ -1792,7 +1686,10 @@ pub const Effects = struct {
1792
1686
 
1793
1687
  if (foe.active.volatiles.Confusion) return;
1794
1688
  foe.active.volatiles.Confusion = true;
1795
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1689
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1690
+ battle.rng.range(u8, 2, 6)
1691
+ else
1692
+ (battle.rng.next() & 3) + 2);
1796
1693
 
1797
1694
  try log.start(battle.active(player.foe()), .Confusion);
1798
1695
  }
@@ -1842,8 +1739,11 @@ pub const Effects = struct {
1842
1739
  return Result.Error;
1843
1740
  }
1844
1741
 
1845
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1846
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1742
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1743
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1744
+ battle.rng.range(u8, 1, 9)
1745
+ else
1746
+ (battle.rng.next() & 7) + 1);
1847
1747
 
1848
1748
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1849
1749
  return null;
@@ -1853,11 +1753,6 @@ pub const Effects = struct {
1853
1753
  var side = battle.side(player);
1854
1754
  var stored = side.stored();
1855
1755
 
1856
- if (battle.last_damage == 0) {
1857
- assert(showdown);
1858
- if (!battle.foe(player).active.volatiles.Substitute) return;
1859
- }
1860
-
1861
1756
  const drain = @max(battle.last_damage / 2, 1);
1862
1757
  battle.last_damage = drain;
1863
1758
 
@@ -1882,7 +1777,15 @@ pub const Effects = struct {
1882
1777
  var volatiles = &battle.foe(player).active.volatiles;
1883
1778
 
1884
1779
  if (volatiles.Substitute) return;
1885
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1780
+
1781
+ const chance = if (showdown)
1782
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1783
+ else
1784
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1785
+ Gen12.percent(10)
1786
+ else
1787
+ Gen12.percent(30));
1788
+ if (!chance) return;
1886
1789
 
1887
1790
  volatiles.Flinch = true;
1888
1791
  volatiles.Recharging = false;
@@ -1908,8 +1811,12 @@ pub const Effects = struct {
1908
1811
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1909
1812
  }
1910
1813
 
1911
- if (foe.active.types.includes(move.type)) return;
1912
- if (!Rolls.secondaryChance(battle, true)) return;
1814
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1815
+ battle.rng.chance(u8, 26, 256)
1816
+ else
1817
+ battle.rng.next() < 1 + Gen12.percent(10);
1818
+ if (!chance) return;
1819
+
1913
1820
  // Freeze Clause Mod
1914
1821
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1915
1822
 
@@ -2046,15 +1953,15 @@ pub const Effects = struct {
2046
1953
  }
2047
1954
  break :has_mimic false;
2048
1955
  };
2049
- if (!has_mimic) {
2050
- // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2051
- if (!try checkHit(battle, player, move, log)) return;
1956
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1957
+ // trigger |-miss| instead of |-fail|
1958
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2052
1959
  return try log.fail(battle.active(player.foe()), .None);
2053
1960
  }
2054
1961
  }
2055
1962
  if (!try checkHit(battle, player, move, log)) return;
2056
1963
 
2057
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1964
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2058
1965
  side.active.move(oslot).id = foe.active.move(rslot).id;
2059
1966
 
2060
1967
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2075,8 +1982,7 @@ pub const Effects = struct {
2075
1982
  assert(!side.active.volatiles.MultiHit);
2076
1983
  side.active.volatiles.MultiHit = true;
2077
1984
 
2078
- side.active.volatiles.attacks =
2079
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1985
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2080
1986
  }
2081
1987
 
2082
1988
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2120,8 +2026,14 @@ pub const Effects = struct {
2120
2026
  }
2121
2027
 
2122
2028
  // Body Slam can't paralyze a Normal type Pokémon
2123
- if (foe.active.types.includes(move.type)) return;
2124
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2029
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2030
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2031
+ else
2032
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2033
+ Gen12.percent(10)
2034
+ else
2035
+ Gen12.percent(30));
2036
+ if (!chance) return;
2125
2037
 
2126
2038
  foe_stored.status = Status.init(.PAR);
2127
2039
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2151,7 +2063,7 @@ pub const Effects = struct {
2151
2063
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2152
2064
  if (!showdown)
2153
2065
  .Poison
2154
- else if (toxic == (foe_stored.status == Status.TOX))
2066
+ else if (toxic == foe.active.volatiles.Toxic)
2155
2067
  if (toxic) .Toxic else .Poison
2156
2068
  else
2157
2069
  .None
@@ -2164,7 +2076,16 @@ pub const Effects = struct {
2164
2076
 
2165
2077
  if (move.effect == .Poison) {
2166
2078
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2167
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2079
+ } else {
2080
+ const chance = if (showdown)
2081
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2082
+ else
2083
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2084
+ Gen12.percent(20)
2085
+ else
2086
+ Gen12.percent(40));
2087
+ if (!chance) return;
2088
+ }
2168
2089
 
2169
2090
  foe_stored.status = Status.init(.PSN);
2170
2091
  if (toxic) {
@@ -2178,8 +2099,10 @@ pub const Effects = struct {
2178
2099
 
2179
2100
  fn rage(battle: anytype, player: Player) !void {
2180
2101
  var volatiles = &battle.side(player).active.volatiles;
2181
- assert(!volatiles.Bide);
2102
+ if (volatiles.Rage) return;
2182
2103
  volatiles.Rage = true;
2104
+ assert(!volatiles.Bide);
2105
+ volatiles.state = 0;
2183
2106
  }
2184
2107
 
2185
2108
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2231,7 +2154,16 @@ pub const Effects = struct {
2231
2154
  }
2232
2155
  foe.active.volatiles.Recharging = false;
2233
2156
 
2234
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2157
+ const duration = @intCast(u3, if (showdown)
2158
+ battle.rng.range(u8, 1, 8)
2159
+ else loop: {
2160
+ while (true) {
2161
+ const r = battle.rng.next() & 7;
2162
+ if (r != 0) break :loop r;
2163
+ }
2164
+ });
2165
+
2166
+ foe_stored.status = Status.slp(duration);
2235
2167
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2236
2168
  }
2237
2169
 
@@ -2239,7 +2171,7 @@ pub const Effects = struct {
2239
2171
  try log.activate(battle.active(player), .Splash);
2240
2172
  }
2241
2173
 
2242
- fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2174
+ fn substitute(battle: anytype, player: Player, log: anytype) !void {
2243
2175
  var side = battle.side(player);
2244
2176
  if (side.active.volatiles.Substitute) {
2245
2177
  try log.fail(battle.active(player), .Substitute);
@@ -2264,18 +2196,14 @@ pub const Effects = struct {
2264
2196
  side.active.volatiles.substitute = hp + 1;
2265
2197
  side.active.volatiles.Substitute = true;
2266
2198
  try log.start(battle.active(player), .Substitute);
2267
- if (hp > 0) {
2268
- try log.damage(battle.active(player), side.stored(), .None);
2269
- if (showdown and side.stored().hp == 0) residual.* = false;
2270
- }
2199
+ if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2271
2200
  }
2272
2201
 
2273
2202
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2274
2203
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2275
2204
 
2276
- // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2205
+ // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2277
2206
  _ = try checkHit(battle, player, move, log);
2278
- battle.last_damage = 0;
2279
2207
  }
2280
2208
 
2281
2209
  fn thrashing(battle: anytype, player: Player) void {
@@ -2284,7 +2212,11 @@ pub const Effects = struct {
2284
2212
  assert(!volatiles.Bide);
2285
2213
 
2286
2214
  volatiles.Thrashing = true;
2287
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2215
+ volatiles.state = 0;
2216
+ volatiles.attacks = @intCast(u3, if (showdown)
2217
+ battle.rng.range(u8, 2, 4)
2218
+ else
2219
+ (battle.rng.next() & 1) + 2);
2288
2220
  }
2289
2221
 
2290
2222
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2316,7 +2248,7 @@ pub const Effects = struct {
2316
2248
  try log.transform(battle.active(player), foe_ident);
2317
2249
  }
2318
2250
 
2319
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2251
+ fn binding(battle: anytype, player: Player) void {
2320
2252
  var side = battle.side(player);
2321
2253
  var foe = battle.foe(player);
2322
2254
 
@@ -2324,11 +2256,9 @@ pub const Effects = struct {
2324
2256
  side.active.volatiles.Binding = true;
2325
2257
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2326
2258
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2327
- if (!showdown) {
2328
- foe.active.volatiles.Recharging = false;
2329
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2259
+ if (!showdown) foe.active.volatiles.Recharging = false;
2330
2260
 
2331
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2261
+ side.active.volatiles.attacks = distribution(battle) - 1;
2332
2262
  }
2333
2263
 
2334
2264
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2355,11 +2285,9 @@ pub const Effects = struct {
2355
2285
  return try log.fail(ident, .None);
2356
2286
  }
2357
2287
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2358
- const stat = unmodifiedStats(battle, side).atk;
2288
+ const stat = unmodifiedStats(battle, player).atk;
2359
2289
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2360
2290
  try log.boost(ident, reason, n);
2361
- // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2362
- if (showdown and move.effect == .Rage) return;
2363
2291
  },
2364
2292
  .DefenseUp1, .DefenseUp2 => {
2365
2293
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2375,7 +2303,7 @@ pub const Effects = struct {
2375
2303
  return try log.fail(ident, .None);
2376
2304
  }
2377
2305
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2378
- const stat = unmodifiedStats(battle, side).def;
2306
+ const stat = unmodifiedStats(battle, player).def;
2379
2307
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2380
2308
  try log.boost(ident, .Defense, n);
2381
2309
  },
@@ -2392,7 +2320,7 @@ pub const Effects = struct {
2392
2320
  return try log.fail(ident, .None);
2393
2321
  }
2394
2322
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2395
- const stat = unmodifiedStats(battle, side).spe;
2323
+ const stat = unmodifiedStats(battle, player).spe;
2396
2324
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2397
2325
  try log.boost(ident, .Speed, 2);
2398
2326
  },
@@ -2412,7 +2340,7 @@ pub const Effects = struct {
2412
2340
  return try log.fail(ident, .None);
2413
2341
  }
2414
2342
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2415
- const stat = unmodifiedStats(battle, side).spc;
2343
+ const stat = unmodifiedStats(battle, player).spc;
2416
2344
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2417
2345
  try log.boost(ident, .SpecialAttack, n);
2418
2346
  try log.boost(ident, .SpecialDefense, n);
@@ -2439,7 +2367,11 @@ pub const Effects = struct {
2439
2367
  }
2440
2368
 
2441
2369
  if (move.effect.isStatDownChance()) {
2442
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2370
+ const chance = if (showdown)
2371
+ battle.rng.chance(u8, 85, 256)
2372
+ else
2373
+ battle.rng.next() < Gen12.percent(33) + 1;
2374
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2443
2375
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2444
2376
  return; // checkHit already checks for Invulnerable
2445
2377
  }
@@ -2461,7 +2393,7 @@ pub const Effects = struct {
2461
2393
  return try log.fail(foe_ident, .None);
2462
2394
  }
2463
2395
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2464
- const stat = unmodifiedStats(battle, foe).atk;
2396
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2465
2397
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2466
2398
  try log.boost(foe_ident, .Attack, -1);
2467
2399
  },
@@ -2479,7 +2411,7 @@ pub const Effects = struct {
2479
2411
  return try log.fail(foe_ident, .None);
2480
2412
  }
2481
2413
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2482
- const stat = unmodifiedStats(battle, foe).def;
2414
+ const stat = unmodifiedStats(battle, player.foe()).def;
2483
2415
  stats.def = @max(1, stat * mod[0] / mod[1]);
2484
2416
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2485
2417
  },
@@ -2496,7 +2428,7 @@ pub const Effects = struct {
2496
2428
  return try log.fail(foe_ident, .None);
2497
2429
  }
2498
2430
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2499
- const stat = unmodifiedStats(battle, foe).spe;
2431
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2500
2432
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2501
2433
  try log.boost(foe_ident, .Speed, -1);
2502
2434
  assert(boosts.spe >= -6);
@@ -2516,7 +2448,7 @@ pub const Effects = struct {
2516
2448
  return try log.fail(foe_ident, .None);
2517
2449
  }
2518
2450
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2519
- const stat = unmodifiedStats(battle, foe).spc;
2451
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2520
2452
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2521
2453
  try log.boost(foe_ident, .SpecialAttack, -1);
2522
2454
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2535,7 +2467,8 @@ pub const Effects = struct {
2535
2467
  }
2536
2468
  };
2537
2469
 
2538
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2470
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
+ const side = battle.side(who);
2539
2472
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2540
2473
  const id = ID.from(side.active.volatiles.transform);
2541
2474
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2549,7 +2482,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2549
2482
  }
2550
2483
  }
2551
2484
 
2552
- inline fn isForced(active: ActivePokemon) bool {
2485
+ fn isForced(active: ActivePokemon) bool {
2553
2486
  return active.volatiles.Recharging or active.volatiles.Rage or
2554
2487
  active.volatiles.Thrashing or active.volatiles.Charging;
2555
2488
  }
@@ -2581,9 +2514,13 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2581
2514
  volatiles.LeechSeed = false;
2582
2515
  try log.end(ident, .LeechSeed);
2583
2516
  }
2584
- if (!showdown and volatiles.Toxic) {
2517
+ if (volatiles.Toxic) {
2585
2518
  volatiles.Toxic = false;
2586
- // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2519
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
2520
+ if (showdown) {
2521
+ volatiles.toxic = 0;
2522
+ try log.end(ident, .Toxic);
2523
+ }
2587
2524
  }
2588
2525
  if (volatiles.LightScreen) {
2589
2526
  volatiles.LightScreen = false;
@@ -2593,148 +2530,70 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2593
2530
  volatiles.Reflect = false;
2594
2531
  try log.end(ident, .Reflect);
2595
2532
  }
2596
- if (showdown and volatiles.Toxic) {
2597
- volatiles.Toxic = false;
2598
- // Pokémon Showdown erroneously clears the toxic counter
2599
- volatiles.toxic = 0;
2600
- try log.end(ident, .Toxic);
2601
- }
2602
2533
  }
2603
2534
 
2604
- pub const Rolls = struct {
2605
- inline fn speedTie(battle: anytype) bool {
2606
- if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2607
- return battle.rng.next() < Gen12.percent(50) + 1;
2608
- }
2609
-
2610
- inline fn criticalHit(battle: anytype, chance: u16) bool {
2611
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2612
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2613
- }
2614
-
2615
- inline fn damage(battle: anytype) u8 {
2616
- if (showdown) return battle.rng.range(u8, 217, 256);
2617
- while (true) {
2618
- const r = std.math.rotr(u8, battle.rng.next(), 1);
2619
- if (r >= 217) return r;
2535
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2536
+ if (side.active.volatiles.disabled_move != 0) {
2537
+ // A Pokémon that transforms after being disabled may end up with less move slots
2538
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2539
+ if (m != .None and m == side.last_selected_move) {
2540
+ side.active.volatiles.Charging = false;
2541
+ try log.disabled(ident, side.last_selected_move);
2542
+ return true;
2620
2543
  }
2621
2544
  }
2545
+ return false;
2546
+ }
2622
2547
 
2623
- inline fn hit(battle: anytype, accuracy: u16) bool {
2624
- if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2625
- return battle.rng.next() < accuracy;
2626
- }
2627
-
2628
- inline fn confusionDuration(battle: anytype) u3 {
2629
- if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2630
- return @intCast(u3, (battle.rng.next() & 3) + 2);
2631
- }
2632
-
2633
- inline fn confused(battle: anytype) bool {
2634
- if (showdown) return !battle.rng.chance(u8, 128, 256);
2635
- return battle.rng.next() >= Gen12.percent(50) + 1;
2636
- }
2637
-
2638
- inline fn paralyzed(battle: anytype) bool {
2639
- if (showdown) return battle.rng.chance(u8, 63, 256);
2640
- return battle.rng.next() < Gen12.percent(25);
2641
- }
2642
-
2643
- inline fn confusionChance(battle: anytype) bool {
2644
- if (showdown) return battle.rng.chance(u8, 25, 256);
2645
- return battle.rng.next() < Gen12.percent(10);
2646
- }
2548
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
+ var volatiles = &active.volatiles;
2550
+ assert(volatiles.Thrashing);
2551
+ if (volatiles.attacks > 0) return false;
2647
2552
 
2648
- inline fn secondaryChance(battle: anytype, low: bool) bool {
2649
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2650
- return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2651
- }
2553
+ volatiles.Thrashing = false;
2554
+ volatiles.Confusion = true;
2555
+ volatiles.confusion = @intCast(u3, if (showdown)
2556
+ battle.rng.range(u8, 2, 6)
2557
+ else
2558
+ (battle.rng.next() & 3) + 2);
2652
2559
 
2653
- inline fn poisonChance(battle: anytype, low: bool) bool {
2654
- if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2655
- return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2656
- }
2560
+ return true;
2561
+ }
2657
2562
 
2658
- inline fn sleepDuration(battle: anytype) u3 {
2659
- if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2660
- while (true) {
2661
- const r = battle.rng.next() & 7;
2662
- if (r != 0) return @intCast(u3, r);
2663
- }
2664
- }
2563
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2665
2564
 
2666
- inline fn bideThrashDuration(battle: anytype) u3 {
2667
- if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2668
- return @intCast(u3, (battle.rng.next() & 1) + 2);
2669
- }
2565
+ fn distribution(battle: anytype) u3 {
2566
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2567
+ const r = (battle.rng.next() & 3);
2568
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2569
+ }
2670
2570
 
2671
- fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2672
- if (showdown) {
2673
- if (check_pp == 0) {
2674
- var i: usize = moves.len;
2675
- while (i > 0) {
2676
- i -= 1;
2677
- if (moves[i].id != .None) {
2678
- return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2679
- }
2680
- }
2681
- } else {
2682
- var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2683
- var i: usize = 0;
2684
- while (i < moves.len and r > 0) : (i += 1) {
2685
- if (moves[i].pp > 0) {
2686
- r -= 1;
2687
- if (r == 0) break;
2688
- }
2571
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2572
+ if (showdown) {
2573
+ if (check_pp == 0) {
2574
+ var i: usize = moves.len;
2575
+ while (i > 0) {
2576
+ i -= 1;
2577
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2578
+ }
2579
+ } else {
2580
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2581
+ var i: usize = 0;
2582
+ while (i < moves.len and r > 0) : (i += 1) {
2583
+ if (moves[i].pp > 0) {
2584
+ r -= 1;
2585
+ if (r == 0) break;
2689
2586
  }
2690
- return @intCast(u4, i + 1);
2691
2587
  }
2692
- }
2693
-
2694
- while (true) {
2695
- const r = @intCast(u4, battle.rng.next() & 3);
2696
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2697
- }
2698
- }
2699
-
2700
- inline fn disableDuration(battle: anytype) u4 {
2701
- if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2702
- return @intCast(u4, (battle.rng.next() & 7) + 1);
2703
- }
2704
-
2705
- inline fn metronome(battle: anytype) Move {
2706
- if (showdown) {
2707
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2708
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2709
- return @intToEnum(Move, r + mod);
2710
- }
2711
- while (true) {
2712
- const r = battle.rng.next();
2713
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2714
- if (r >= @enumToInt(Move.Struggle)) continue;
2715
- return @intToEnum(Move, r);
2588
+ return @intCast(u4, i + 1);
2716
2589
  }
2717
2590
  }
2718
2591
 
2719
- inline fn psywave(battle: anytype, max: u8) u8 {
2720
- if (showdown) return battle.rng.range(u8, 0, max);
2721
- while (true) {
2722
- const r = battle.rng.next();
2723
- if (r < max) return r;
2724
- }
2725
- }
2726
-
2727
- inline fn distribution(battle: anytype) u3 {
2728
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2729
- const r = (battle.rng.next() & 3);
2730
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2592
+ while (true) {
2593
+ const r = @intCast(u4, rand.next() & 3);
2594
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2731
2595
  }
2732
-
2733
- inline fn unboost(battle: anytype) bool {
2734
- if (showdown) return battle.rng.chance(u8, 85, 256);
2735
- return battle.rng.next() < Gen12.percent(33) + 1;
2736
- }
2737
- };
2596
+ }
2738
2597
 
2739
2598
  test "RNG agreement" {
2740
2599
  if (!showdown) return;
@@ -2834,11 +2693,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2834
2693
  while (slot <= 4) : (slot += 1) {
2835
2694
  const m = active.moves[slot - 1];
2836
2695
  if (m.id == .None) break;
2837
- if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2838
- // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2839
- const struggle =
2840
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2841
- const s = if (struggle) 0 else slot;
2696
+ if (m.id == side.last_selected_move) {
2697
+ // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2698
+ const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2842
2699
  out[n] = .{ .type = .Move, .data = s };
2843
2700
  n += 1;
2844
2701
  return n;