@pkmn/engine 0.1.0-dev.7f37a47c → 0.1.0-dev.83de29ee

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 {
@@ -185,6 +203,7 @@ fn selectMove(
185
203
  }
186
204
 
187
205
  // move select
206
+ volatiles.state = 0;
188
207
  if (choice.data == 0) {
189
208
  const struggle = ok: {
190
209
  for (side.active.moves, 0..) |move, i| {
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
218
237
 
219
238
  side.last_selected_move = move.id;
220
239
  if (player == .P1) {
221
- battle.last_selected_indexes.p1 = @intCast(u4, c.data);
240
+ battle.last_moves.p1_index = @intCast(u3, c.data);
222
241
  } else {
223
- battle.last_selected_indexes.p2 = @intCast(u4, c.data);
242
+ battle.last_moves.p2_index = @intCast(u3, c.data);
224
243
  }
225
244
  }
226
245
  }
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
240
259
  side.order[slot - 1] = out;
241
260
 
242
261
  if (player == .P1) {
243
- battle.last_selected_indexes.p1 = 1;
262
+ battle.last_moves.p1_index = 1;
244
263
  } else {
245
- battle.last_selected_indexes.p2 = 1;
264
+ battle.last_moves.p2_index = 1;
246
265
  }
247
266
 
248
267
  side.last_used_move = .None;
@@ -301,11 +320,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
321
  if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
322
 
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
308
-
323
+ const p1 = Rolls.speedTie(battle);
309
324
  if (!showdown) return if (p1) .P1 else .P2;
310
325
 
311
326
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +330,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
330
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
331
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
332
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
318
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
333
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
334
  // won the original coin flip
320
335
  if (t1 and t2) return .P1;
321
336
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +343,13 @@ fn doTurn(
328
343
  battle: anytype,
329
344
  player: Player,
330
345
  player_choice: Choice,
346
+ player_rewrap: bool,
347
+ player_skip: bool,
331
348
  player_from: ?Move,
332
349
  foe_player: Player,
333
350
  foe_choice: Choice,
351
+ foe_rewrap: bool,
352
+ foe_skip: bool,
334
353
  foe_from: ?Move,
335
354
  log: anytype,
336
355
  ) !?Result {
@@ -338,7 +357,16 @@ fn doTurn(
338
357
 
339
358
  var residual = true;
340
359
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
360
+ if (try executeMove(
361
+ battle,
362
+ player,
363
+ player_choice,
364
+ player_rewrap,
365
+ player_skip,
366
+ player_from,
367
+ &residual,
368
+ log,
369
+ )) |r| return r;
342
370
  if (!replace) {
343
371
  if (player_choice.type != .Switch) {
344
372
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -347,8 +375,18 @@ fn doTurn(
347
375
  if (try checkFaint(battle, player, log)) |r| return r;
348
376
  } else if (foe_choice.type == .Pass) return null;
349
377
 
378
+ residual = true;
350
379
  replace = battle.side(foe_player).stored().hp == 0;
351
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
380
+ if (try executeMove(
381
+ battle,
382
+ foe_player,
383
+ foe_choice,
384
+ foe_rewrap,
385
+ foe_skip,
386
+ foe_from,
387
+ &residual,
388
+ log,
389
+ )) |r| return r;
352
390
  if (!replace) {
353
391
  if (foe_choice.type != .Switch) {
354
392
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +409,8 @@ fn executeMove(
371
409
  battle: anytype,
372
410
  player: Player,
373
411
  choice: Choice,
412
+ rewrap: bool,
413
+ skip: bool,
374
414
  from: ?Move,
375
415
  residual: *bool,
376
416
  log: anytype,
@@ -382,9 +422,10 @@ fn executeMove(
382
422
  return null;
383
423
  }
384
424
 
425
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
426
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
427
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
385
428
  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
388
429
  assert(!showdown);
389
430
  if (battle.foe(player).active.volatiles.Binding) {
390
431
  try log.cant(battle.active(player), .Bound);
@@ -403,9 +444,9 @@ fn executeMove(
403
444
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
404
445
  assert(!showdown);
405
446
  mslot = @intCast(u4, if (player == .P1)
406
- battle.last_selected_indexes.p1
447
+ battle.last_moves.p1_index
407
448
  else
408
- battle.last_selected_indexes.p2);
449
+ battle.last_moves.p2_index);
409
450
  const stored = side.stored();
410
451
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
411
452
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -427,14 +468,14 @@ fn executeMove(
427
468
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
428
469
  assert(mslot == 1);
429
470
  mslot = @intCast(u4, if (player == .P1)
430
- battle.last_selected_indexes.p1
471
+ battle.last_moves.p1_index
431
472
  else
432
- battle.last_selected_indexes.p2);
473
+ battle.last_moves.p2_index);
433
474
  }
434
475
 
435
476
  var skip_can = false;
436
477
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
478
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
479
  .done => return null,
439
480
  .skip_can => skip_can = true,
440
481
  .skip_pp => skip_pp = true,
@@ -442,14 +483,23 @@ fn executeMove(
442
483
  .err => return @as(?Result, Result.Error),
443
484
  }
444
485
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
486
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
487
+ return null;
488
+ }
446
489
 
447
- return doMove(battle, player, mslot, auto, residual, log);
490
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
448
491
  }
449
492
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
493
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
494
 
452
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
495
+ fn beforeMove(
496
+ battle: anytype,
497
+ player: Player,
498
+ skip: bool,
499
+ from: ?Move,
500
+ residual: *bool,
501
+ log: anytype,
502
+ ) !BeforeMove {
453
503
  var side = battle.side(player);
454
504
  const foe = battle.foe(player);
455
505
  var active = &side.active;
@@ -477,13 +527,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
477
527
  return .done;
478
528
  }
479
529
 
480
- if (foe.active.volatiles.Binding) {
530
+ if (skip or foe.active.volatiles.Binding) {
481
531
  try log.cant(ident, .Bound);
482
532
  return .done;
483
533
  }
484
534
 
485
- if (!showdown and volatiles.Flinch) {
486
- volatiles.Flinch = false;
535
+ if (volatiles.Flinch) {
536
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
537
+ // Pokémon can sometimes flinch multiple times from the same original hit
538
+ if (!showdown) volatiles.Flinch = false;
487
539
  try log.cant(ident, .Flinch);
488
540
  return .done;
489
541
  }
@@ -504,13 +556,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
556
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
557
  if (showdown and try disabled(side, ident, log)) return .done;
506
558
 
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
-
514
559
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
515
560
  // before the side had a selected a move - we simply need to assume this leads to a desync
516
561
  if (side.last_selected_move == .None) {
@@ -528,14 +573,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
573
  } else {
529
574
  try log.activate(ident, .Confusion);
530
575
 
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) {
576
+ if (Rolls.confused(battle)) {
537
577
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
578
+ if (!volatiles.Rage) volatiles.state = 0;
539
579
  volatiles.Bide = false;
540
580
  volatiles.Thrashing = false;
541
581
  volatiles.MultiHit = false;
@@ -559,8 +599,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
599
  player
560
600
  else
561
601
  player.foe();
602
+
603
+ const uncapped = battle.last_damage;
562
604
  // Skipping adjustDamage / randomizeDamage / checkHit
563
605
  _ = try applyDamage(battle, player, target, .Confusion, log);
606
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
607
+ if (showdown) battle.last_damage = uncapped;
564
608
 
565
609
  return .done;
566
610
  }
@@ -569,22 +613,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
613
 
570
614
  if (!showdown and try disabled(side, ident, log)) return .done;
571
615
 
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
- }
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;
588
625
  }
589
626
 
590
627
  if (volatiles.Bide) {
@@ -611,7 +648,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
648
  return .done;
612
649
  }
613
650
 
651
+ const sub = showdown and foe.active.volatiles.Substitute;
614
652
  _ = 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
+
615
663
  return .done;
616
664
  }
617
665
 
@@ -623,8 +671,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
623
671
  try log.start(battle.active(player), .ConfusionSilent);
624
672
  }
625
673
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
626
- if (showdown and handleThrashing(battle, active)) {
627
- try log.start(battle.active(player), .ConfusionSilent);
674
+ if (showdown) {
675
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
676
+ // used in useMove and doesn't have the notion of skipping canMove semantics
677
+ side.last_used_move = side.last_selected_move;
678
+ if (handleThrashing(battle, active)) {
679
+ try log.start(battle.active(player), .ConfusionSilent);
680
+ }
628
681
  }
629
682
  return .skip_can;
630
683
  }
@@ -639,7 +692,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
639
692
  side.last_selected_move,
640
693
  );
641
694
  if (showdown or battle.last_damage != 0) {
695
+ const sub = showdown and foe.active.volatiles.Substitute;
642
696
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
697
+ if (battle.foe(player).stored().hp > 0 and !sub) {
698
+ try buildRage(battle, player.foe(), log);
699
+ }
643
700
  }
644
701
  return .done;
645
702
  }
@@ -654,6 +711,7 @@ fn canMove(
654
711
  auto: bool,
655
712
  skip_pp: bool,
656
713
  from: ?Move,
714
+ residual: *bool,
657
715
  log: anytype,
658
716
  ) !bool {
659
717
  var side = battle.side(player);
@@ -687,8 +745,21 @@ fn canMove(
687
745
  const f = if (from != null and from.? == .None) null else from;
688
746
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
689
747
 
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
+
690
761
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
762
+ try onBegin(battle, player, move, mslot, residual, log);
692
763
  return false;
693
764
  }
694
765
 
@@ -696,7 +767,7 @@ fn canMove(
696
767
  Effects.thrashing(battle, player);
697
768
  } else if (!showdown and move.effect == .Binding) {
698
769
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
699
- Effects.binding(battle, player);
770
+ Effects.binding(battle, player, false);
700
771
  }
701
772
 
702
773
  return true;
@@ -737,6 +808,7 @@ fn doMove(
737
808
  battle: anytype,
738
809
  player: Player,
739
810
  mslot: u4,
811
+ rewrap: bool,
740
812
  auto: bool,
741
813
  residual: *bool,
742
814
  log: anytype,
@@ -767,16 +839,21 @@ fn doMove(
767
839
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
840
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
841
  if (immune and move.effect != .Binding) break :miss true;
770
- if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) 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
+ }
771
846
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
847
  };
773
848
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
849
 
850
+ var late = showdown and move.effect != .Explode;
775
851
  const skip = status or immune;
776
852
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
853
  if (showdown and move.effect.isMulti()) {
778
854
  Effects.multiHit(battle, player, move);
779
855
  hits = side.active.volatiles.attacks;
856
+ late = false;
780
857
  }
781
858
 
782
859
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +887,7 @@ fn doMove(
810
887
  }
811
888
 
812
889
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
813
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
890
+ if (showdown and skip) battle.last_damage = 0;
814
891
 
815
892
  miss = if (showdown or skip)
816
893
  miss
@@ -823,9 +900,9 @@ fn doMove(
823
900
 
824
901
  if (!showdown or !miss) {
825
902
  if (move.effect == .MirrorMove) {
826
- return mirrorMove(battle, player, mslot, auto, residual, log);
903
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
827
904
  } else if (move.effect == .Metronome) {
828
- return metronome(battle, player, mslot, auto, residual, log);
905
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
829
906
  } else if (move.effect.onEnd()) {
830
907
  try onEnd(battle, player, move, log);
831
908
  return null;
@@ -840,7 +917,7 @@ fn doMove(
840
917
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
841
918
  if (ohko) {
842
919
  try log.immune(foe_ident, .OHKO);
843
- } else if (immune and !invulnerable) {
920
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
844
921
  try log.immune(foe_ident, .None);
845
922
  } else if (mist) {
846
923
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -857,15 +934,9 @@ fn doMove(
857
934
  if (showdown and side.stored().hp == 0) residual.* = false;
858
935
  } else if (move.effect == .Explode) {
859
936
  try Effects.explode(battle, player);
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
- }
937
+ try buildRage(battle, player.foe(), log);
938
+ } else if (showdown and move.effect == .Disable) {
939
+ try buildRage(battle, player.foe(), log);
869
940
  }
870
941
  return null;
871
942
  }
@@ -878,6 +949,9 @@ fn doMove(
878
949
  hits = side.active.volatiles.attacks;
879
950
  }
880
951
 
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
+
881
955
  var nullified = false;
882
956
  var hit: u4 = 0;
883
957
  while (hit < hits) {
@@ -893,9 +967,7 @@ fn doMove(
893
967
  if (hit == 0 and ohko) try log.ohko();
894
968
  hit += 1;
895
969
  if (foe.stored().hp == 0) break;
896
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
897
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
898
- }
970
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
971
  // If the substitute breaks during a multi-hit attack, the attack ends
900
972
  if (nullified) break;
901
973
  }
@@ -911,11 +983,17 @@ fn doMove(
911
983
  } else if (showdown) {
912
984
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
913
985
  if (move.effect == .Binding) {
914
- Effects.binding(battle, player);
986
+ Effects.binding(battle, player, rewrap);
915
987
  if (immune) {
916
988
  battle.last_damage = 0;
989
+ assert(foe.stored().hp > 0);
917
990
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
918
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
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
+ }
919
997
  return null;
920
998
  }
921
999
  }
@@ -932,6 +1010,9 @@ fn doMove(
932
1010
 
933
1011
  if (foe.stored().hp == 0) return null;
934
1012
 
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
+
935
1016
  if (!move.effect.isSpecial()) {
936
1017
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
1018
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +1024,9 @@ fn doMove(
943
1024
  if (move.effect == .Twineedle) {
944
1025
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
1026
  } else if (move.effect == .Disable) {
946
- return Effects.disable(battle, player, move, log);
1027
+ const result = try Effects.disable(battle, player, move, log);
1028
+ if (showdown) try buildRage(battle, player.foe(), log);
1029
+ return result;
947
1030
  } else {
948
1031
  try moveEffect(battle, player, move, log);
949
1032
  }
@@ -961,8 +1044,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
1044
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
1045
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
1046
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1047
+ return Rolls.criticalHit(battle, chance);
966
1048
  }
967
1049
 
968
1050
  fn calcDamage(
@@ -1066,49 +1148,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1148
 
1067
1149
  fn randomizeDamage(battle: anytype) void {
1068
1150
  if (battle.last_damage <= 1) return;
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
-
1151
+ const random = Rolls.damage(battle);
1079
1152
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1153
  }
1081
1154
 
1082
1155
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1156
  const side = battle.side(player);
1157
+ const foe = battle.foe(player);
1084
1158
 
1085
1159
  if (!try checkHit(battle, player, move, log)) return null;
1086
1160
 
1087
1161
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1162
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1163
  .SeismicToss, .NightShade => side.stored().level,
1090
1164
  .SonicBoom => 20,
1091
1165
  .DragonRage => 40,
1092
1166
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1167
  .Psywave => power: {
1094
1168
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
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
- }
1169
+ // GLITCH: Psywave infinite glitch loop
1170
+ if (!showdown and max <= 1) return Result.Error;
1171
+ break :power Rolls.psywave(battle, max);
1105
1172
  },
1106
1173
  else => unreachable,
1107
1174
  };
1108
1175
 
1109
1176
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1177
 
1178
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1179
  _ = 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
+
1112
1182
  return null;
1113
1183
  }
1114
1184
 
@@ -1120,19 +1190,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1120
1190
  return null;
1121
1191
  }
1122
1192
 
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);
1193
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1126
1194
  const foe_last_selected_move =
1127
1195
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1128
- .Counter
1196
+ .Splash
1129
1197
  else
1130
1198
  foe.last_selected_move);
1131
1199
 
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);
1200
+ const used = (if (player.foe() == .P1)
1201
+ battle.last_moves.p1_counterable
1202
+ else
1203
+ battle.last_moves.p2_counterable) != 0;
1136
1204
 
1137
1205
  const selected = foe_last_selected_move.bp > 0 and
1138
1206
  foe.last_selected_move != .Counter and
@@ -1156,7 +1224,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1156
1224
  // Pokémon Showdown calls checkHit before Counter
1157
1225
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1158
1226
 
1227
+ const sub = showdown and foe.active.volatiles.Substitute;
1159
1228
  _ = 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);
1160
1230
  return null;
1161
1231
  }
1162
1232
 
@@ -1200,6 +1270,7 @@ fn mirrorMove(
1200
1270
  battle: anytype,
1201
1271
  player: Player,
1202
1272
  mslot: u4,
1273
+ rewrap: bool,
1203
1274
  auto: bool,
1204
1275
  residual: *bool,
1205
1276
  log: anytype,
@@ -1212,41 +1283,30 @@ fn mirrorMove(
1212
1283
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1213
1284
  try log.fail(battle.active(player), .None);
1214
1285
  return null;
1286
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1287
+ incrementPP(side, mslot);
1215
1288
  }
1216
1289
 
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);
1290
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1291
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1221
1292
  }
1222
1293
 
1223
1294
  fn metronome(
1224
1295
  battle: anytype,
1225
1296
  player: Player,
1226
1297
  mslot: u4,
1298
+ rewrap: bool,
1227
1299
  auto: bool,
1228
1300
  residual: *bool,
1229
1301
  log: anytype,
1230
1302
  ) !?Result {
1231
1303
  var side = battle.side(player);
1232
1304
 
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
-
1305
+ side.last_selected_move = Rolls.metronome(battle);
1246
1306
  incrementPP(side, mslot);
1247
1307
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
- return doMove(battle, player, mslot, auto, residual, log);
1308
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1309
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
1310
  }
1251
1311
 
1252
1312
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1278,7 +1338,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1278
1338
  if (move.effect == .Swift) return true;
1279
1339
  if (foe.active.volatiles.Invulnerable) break :miss true;
1280
1340
  }
1281
- if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1341
+ if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1342
+ (showdown and foe.active.volatiles.Substitute)))
1343
+ {
1282
1344
  immune.* = true;
1283
1345
  if (showdown) return false;
1284
1346
  break :miss true;
@@ -1298,23 +1360,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1298
1360
  }
1299
1361
 
1300
1362
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
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));
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));
1305
1369
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1306
1370
  accuracy = accuracy * boost[0] / boost[1];
1307
1371
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1308
1372
  accuracy = accuracy * boost[0] / boost[1];
1309
1373
  accuracy = @min(255, @max(1, accuracy));
1310
1374
 
1311
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
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;
1312
1378
 
1313
1379
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1314
- break :miss if (showdown)
1315
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1316
- else
1317
- battle.rng.next() >= accuracy;
1380
+ break :miss !Rolls.hit(battle, accuracy);
1318
1381
  };
1319
1382
 
1320
1383
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1429,6 +1492,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1429
1492
  }
1430
1493
 
1431
1494
  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
+
1432
1504
  var damage = @max(stored.stats.hp / 16, 1);
1433
1505
 
1434
1506
  // GLITCH: Leech Seed + Toxic glitch
@@ -1440,10 +1512,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1440
1512
  const amount = @min(damage, stored.hp);
1441
1513
  stored.hp -= amount;
1442
1514
 
1443
- var foe = battle.foe(player);
1444
- var foe_stored = foe.stored();
1445
- const foe_ident = battle.active(player.foe());
1446
-
1447
1515
  // As above, Pokémon Showdown uses damageOf but its not relevant
1448
1516
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1449
1517
 
@@ -1480,16 +1548,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1480
1548
 
1481
1549
  fn checkEBC(battle: anytype) bool {
1482
1550
  for (battle.sides, 0..) |side, i| {
1551
+ const foe = battle.sides[~@intCast(u1, i)];
1552
+
1483
1553
  var foe_all_ghosts = true;
1484
1554
  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];
1485
1559
 
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);
1560
+ const ghost = pokemon.hp == 0 or
1561
+ (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1490
1562
  foe_all_ghosts = foe_all_ghosts and ghost;
1491
1563
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1492
- for (pokemon.moves) |m| {
1564
+ for (if (active) foe.active.moves else pokemon.moves) |m| {
1493
1565
  if (m.id == .None) break :transform true;
1494
1566
  if (m.id != .Transform) break :transform false;
1495
1567
  }
@@ -1497,10 +1569,14 @@ fn checkEBC(battle: anytype) bool {
1497
1569
  };
1498
1570
  }
1499
1571
 
1500
- for (side.pokemon) |pokemon| {
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
+
1501
1577
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1502
1578
  const transform = foe_all_transform and transform: {
1503
- for (pokemon.moves) |m| {
1579
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1504
1580
  if (m.id == .None) break :transform true;
1505
1581
  if (m.id != .Transform) break :transform false;
1506
1582
  }
@@ -1508,7 +1584,7 @@ fn checkEBC(battle: anytype) bool {
1508
1584
  };
1509
1585
  if (transform) continue;
1510
1586
  const no_pp = foe_all_ghosts and no_pp: {
1511
- for (pokemon.moves) |m| {
1587
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1512
1588
  if (m.pp != 0) break :no_pp false;
1513
1589
  }
1514
1590
  break :no_pp true;
@@ -1524,7 +1600,50 @@ fn checkEBC(battle: anytype) bool {
1524
1600
  return false;
1525
1601
  }
1526
1602
 
1527
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
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 {
1528
1647
  assert(move.effect.onBegin());
1529
1648
  return switch (move.effect) {
1530
1649
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1540,7 +1659,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1540
1659
  .Poison => Effects.poison(battle, player, move, log),
1541
1660
  .Reflect => Effects.reflect(battle, player, log),
1542
1661
  .Splash => Effects.splash(battle, player, log),
1543
- .Substitute => Effects.substitute(battle, player, log),
1662
+ .Substitute => Effects.substitute(battle, player, residual, log),
1544
1663
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1545
1664
  .Transform => Effects.transform(battle, player, log),
1546
1665
  else => unreachable,
@@ -1607,10 +1726,7 @@ pub const Effects = struct {
1607
1726
  side.active.volatiles.Bide = true;
1608
1727
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1609
1728
  side.active.volatiles.state = 0;
1610
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1611
- battle.rng.range(u4, 2, 4)
1612
- else
1613
- (battle.rng.next() & 1) + 2);
1729
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1614
1730
 
1615
1731
  try log.start(battle.active(player), .Bide);
1616
1732
  }
@@ -1632,14 +1748,8 @@ pub const Effects = struct {
1632
1748
  return;
1633
1749
  }
1634
1750
 
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;
1751
+ if (foe.active.types.includes(move.type)) return;
1752
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1643
1753
 
1644
1754
  foe_stored.status = Status.init(.BRN);
1645
1755
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1663,11 +1773,7 @@ pub const Effects = struct {
1663
1773
  const sub = foe.active.volatiles.Substitute;
1664
1774
 
1665
1775
  if (move.effect == .ConfusionChance) {
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;
1776
+ if (!Rolls.confusionChance(battle)) return;
1671
1777
  } else {
1672
1778
  if (showdown) {
1673
1779
  if (!try checkHit(battle, player, move, log)) {
@@ -1686,10 +1792,7 @@ pub const Effects = struct {
1686
1792
 
1687
1793
  if (foe.active.volatiles.Confusion) return;
1688
1794
  foe.active.volatiles.Confusion = true;
1689
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1690
- battle.rng.range(u8, 2, 6)
1691
- else
1692
- (battle.rng.next() & 3) + 2);
1795
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1693
1796
 
1694
1797
  try log.start(battle.active(player.foe()), .Confusion);
1695
1798
  }
@@ -1739,11 +1842,8 @@ pub const Effects = struct {
1739
1842
  return Result.Error;
1740
1843
  }
1741
1844
 
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);
1845
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1846
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1747
1847
 
1748
1848
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1749
1849
  return null;
@@ -1753,6 +1853,11 @@ pub const Effects = struct {
1753
1853
  var side = battle.side(player);
1754
1854
  var stored = side.stored();
1755
1855
 
1856
+ if (battle.last_damage == 0) {
1857
+ assert(showdown);
1858
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1859
+ }
1860
+
1756
1861
  const drain = @max(battle.last_damage / 2, 1);
1757
1862
  battle.last_damage = drain;
1758
1863
 
@@ -1777,15 +1882,7 @@ pub const Effects = struct {
1777
1882
  var volatiles = &battle.foe(player).active.volatiles;
1778
1883
 
1779
1884
  if (volatiles.Substitute) 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;
1885
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1789
1886
 
1790
1887
  volatiles.Flinch = true;
1791
1888
  volatiles.Recharging = false;
@@ -1811,12 +1908,8 @@ pub const Effects = struct {
1811
1908
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1812
1909
  }
1813
1910
 
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
-
1911
+ if (foe.active.types.includes(move.type)) return;
1912
+ if (!Rolls.secondaryChance(battle, true)) return;
1820
1913
  // Freeze Clause Mod
1821
1914
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1822
1915
 
@@ -1953,15 +2046,15 @@ pub const Effects = struct {
1953
2046
  }
1954
2047
  break :has_mimic false;
1955
2048
  };
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) {
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;
1959
2052
  return try log.fail(battle.active(player.foe()), .None);
1960
2053
  }
1961
2054
  }
1962
2055
  if (!try checkHit(battle, player, move, log)) return;
1963
2056
 
1964
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2057
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1965
2058
  side.active.move(oslot).id = foe.active.move(rslot).id;
1966
2059
 
1967
2060
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1982,7 +2075,8 @@ pub const Effects = struct {
1982
2075
  assert(!side.active.volatiles.MultiHit);
1983
2076
  side.active.volatiles.MultiHit = true;
1984
2077
 
1985
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2078
+ side.active.volatiles.attacks =
2079
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1986
2080
  }
1987
2081
 
1988
2082
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2026,14 +2120,8 @@ pub const Effects = struct {
2026
2120
  }
2027
2121
 
2028
2122
  // Body Slam can't paralyze a Normal type Pokémon
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;
2123
+ if (foe.active.types.includes(move.type)) return;
2124
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2037
2125
 
2038
2126
  foe_stored.status = Status.init(.PAR);
2039
2127
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2063,7 +2151,7 @@ pub const Effects = struct {
2063
2151
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2064
2152
  if (!showdown)
2065
2153
  .Poison
2066
- else if (toxic == foe.active.volatiles.Toxic)
2154
+ else if (toxic == (foe_stored.status == Status.TOX))
2067
2155
  if (toxic) .Toxic else .Poison
2068
2156
  else
2069
2157
  .None
@@ -2076,16 +2164,7 @@ pub const Effects = struct {
2076
2164
 
2077
2165
  if (move.effect == .Poison) {
2078
2166
  if (!showdown and !try checkHit(battle, player, move, log)) 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
- }
2167
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2089
2168
 
2090
2169
  foe_stored.status = Status.init(.PSN);
2091
2170
  if (toxic) {
@@ -2099,10 +2178,8 @@ pub const Effects = struct {
2099
2178
 
2100
2179
  fn rage(battle: anytype, player: Player) !void {
2101
2180
  var volatiles = &battle.side(player).active.volatiles;
2102
- if (volatiles.Rage) return;
2103
- volatiles.Rage = true;
2104
2181
  assert(!volatiles.Bide);
2105
- volatiles.state = 0;
2182
+ volatiles.Rage = true;
2106
2183
  }
2107
2184
 
2108
2185
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2154,16 +2231,7 @@ pub const Effects = struct {
2154
2231
  }
2155
2232
  foe.active.volatiles.Recharging = false;
2156
2233
 
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);
2234
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2167
2235
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2168
2236
  }
2169
2237
 
@@ -2171,7 +2239,7 @@ pub const Effects = struct {
2171
2239
  try log.activate(battle.active(player), .Splash);
2172
2240
  }
2173
2241
 
2174
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2242
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2175
2243
  var side = battle.side(player);
2176
2244
  if (side.active.volatiles.Substitute) {
2177
2245
  try log.fail(battle.active(player), .Substitute);
@@ -2196,14 +2264,18 @@ pub const Effects = struct {
2196
2264
  side.active.volatiles.substitute = hp + 1;
2197
2265
  side.active.volatiles.Substitute = true;
2198
2266
  try log.start(battle.active(player), .Substitute);
2199
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
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
+ }
2200
2271
  }
2201
2272
 
2202
2273
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2203
2274
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2204
2275
 
2205
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2276
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2206
2277
  _ = try checkHit(battle, player, move, log);
2278
+ battle.last_damage = 0;
2207
2279
  }
2208
2280
 
2209
2281
  fn thrashing(battle: anytype, player: Player) void {
@@ -2212,11 +2284,7 @@ pub const Effects = struct {
2212
2284
  assert(!volatiles.Bide);
2213
2285
 
2214
2286
  volatiles.Thrashing = true;
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);
2287
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2220
2288
  }
2221
2289
 
2222
2290
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2248,7 +2316,7 @@ pub const Effects = struct {
2248
2316
  try log.transform(battle.active(player), foe_ident);
2249
2317
  }
2250
2318
 
2251
- fn binding(battle: anytype, player: Player) void {
2319
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2252
2320
  var side = battle.side(player);
2253
2321
  var foe = battle.foe(player);
2254
2322
 
@@ -2256,9 +2324,11 @@ pub const Effects = struct {
2256
2324
  side.active.volatiles.Binding = true;
2257
2325
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2258
2326
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2259
- if (!showdown) foe.active.volatiles.Recharging = false;
2327
+ if (!showdown) {
2328
+ foe.active.volatiles.Recharging = false;
2329
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2260
2330
 
2261
- side.active.volatiles.attacks = distribution(battle) - 1;
2331
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2262
2332
  }
2263
2333
 
2264
2334
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2285,9 +2355,11 @@ pub const Effects = struct {
2285
2355
  return try log.fail(ident, .None);
2286
2356
  }
2287
2357
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2288
- const stat = unmodifiedStats(battle, player).atk;
2358
+ const stat = unmodifiedStats(battle, side).atk;
2289
2359
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2290
2360
  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;
2291
2363
  },
2292
2364
  .DefenseUp1, .DefenseUp2 => {
2293
2365
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2303,7 +2375,7 @@ pub const Effects = struct {
2303
2375
  return try log.fail(ident, .None);
2304
2376
  }
2305
2377
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2306
- const stat = unmodifiedStats(battle, player).def;
2378
+ const stat = unmodifiedStats(battle, side).def;
2307
2379
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2308
2380
  try log.boost(ident, .Defense, n);
2309
2381
  },
@@ -2320,7 +2392,7 @@ pub const Effects = struct {
2320
2392
  return try log.fail(ident, .None);
2321
2393
  }
2322
2394
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2323
- const stat = unmodifiedStats(battle, player).spe;
2395
+ const stat = unmodifiedStats(battle, side).spe;
2324
2396
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2325
2397
  try log.boost(ident, .Speed, 2);
2326
2398
  },
@@ -2340,7 +2412,7 @@ pub const Effects = struct {
2340
2412
  return try log.fail(ident, .None);
2341
2413
  }
2342
2414
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2343
- const stat = unmodifiedStats(battle, player).spc;
2415
+ const stat = unmodifiedStats(battle, side).spc;
2344
2416
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2417
  try log.boost(ident, .SpecialAttack, n);
2346
2418
  try log.boost(ident, .SpecialDefense, n);
@@ -2367,11 +2439,7 @@ pub const Effects = struct {
2367
2439
  }
2368
2440
 
2369
2441
  if (move.effect.isStatDownChance()) {
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;
2442
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2375
2443
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2376
2444
  return; // checkHit already checks for Invulnerable
2377
2445
  }
@@ -2393,7 +2461,7 @@ pub const Effects = struct {
2393
2461
  return try log.fail(foe_ident, .None);
2394
2462
  }
2395
2463
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2396
- const stat = unmodifiedStats(battle, player.foe()).atk;
2464
+ const stat = unmodifiedStats(battle, foe).atk;
2397
2465
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2398
2466
  try log.boost(foe_ident, .Attack, -1);
2399
2467
  },
@@ -2411,7 +2479,7 @@ pub const Effects = struct {
2411
2479
  return try log.fail(foe_ident, .None);
2412
2480
  }
2413
2481
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2414
- const stat = unmodifiedStats(battle, player.foe()).def;
2482
+ const stat = unmodifiedStats(battle, foe).def;
2415
2483
  stats.def = @max(1, stat * mod[0] / mod[1]);
2416
2484
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2417
2485
  },
@@ -2428,7 +2496,7 @@ pub const Effects = struct {
2428
2496
  return try log.fail(foe_ident, .None);
2429
2497
  }
2430
2498
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2431
- const stat = unmodifiedStats(battle, player.foe()).spe;
2499
+ const stat = unmodifiedStats(battle, foe).spe;
2432
2500
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2433
2501
  try log.boost(foe_ident, .Speed, -1);
2434
2502
  assert(boosts.spe >= -6);
@@ -2448,7 +2516,7 @@ pub const Effects = struct {
2448
2516
  return try log.fail(foe_ident, .None);
2449
2517
  }
2450
2518
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2451
- const stat = unmodifiedStats(battle, player.foe()).spc;
2519
+ const stat = unmodifiedStats(battle, foe).spc;
2452
2520
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2453
2521
  try log.boost(foe_ident, .SpecialAttack, -1);
2454
2522
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2467,8 +2535,7 @@ pub const Effects = struct {
2467
2535
  }
2468
2536
  };
2469
2537
 
2470
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
- const side = battle.side(who);
2538
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2472
2539
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2473
2540
  const id = ID.from(side.active.volatiles.transform);
2474
2541
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2482,7 +2549,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2482
2549
  }
2483
2550
  }
2484
2551
 
2485
- fn isForced(active: ActivePokemon) bool {
2552
+ inline fn isForced(active: ActivePokemon) bool {
2486
2553
  return active.volatiles.Recharging or active.volatiles.Rage or
2487
2554
  active.volatiles.Thrashing or active.volatiles.Charging;
2488
2555
  }
@@ -2514,13 +2581,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2514
2581
  volatiles.LeechSeed = false;
2515
2582
  try log.end(ident, .LeechSeed);
2516
2583
  }
2517
- if (volatiles.Toxic) {
2584
+ if (!showdown and volatiles.Toxic) {
2518
2585
  volatiles.Toxic = false;
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
- }
2586
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2524
2587
  }
2525
2588
  if (volatiles.LightScreen) {
2526
2589
  volatiles.LightScreen = false;
@@ -2530,70 +2593,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2530
2593
  volatiles.Reflect = false;
2531
2594
  try log.end(ident, .Reflect);
2532
2595
  }
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
+ }
2533
2602
  }
2534
2603
 
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;
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;
2543
2620
  }
2544
2621
  }
2545
- return false;
2546
- }
2547
2622
 
2548
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
- var volatiles = &active.volatiles;
2550
- assert(volatiles.Thrashing);
2551
- if (volatiles.attacks > 0) return false;
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
+ }
2552
2627
 
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);
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
+ }
2559
2632
 
2560
- return true;
2561
- }
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
+ }
2562
2637
 
2563
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
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
+ }
2564
2642
 
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
- }
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
+ }
2570
2647
 
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;
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
+ }
2652
+
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
+ }
2657
+
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
+ }
2665
+
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
+ }
2670
+
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
+ }
2586
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
+ }
2689
+ }
2690
+ return @intCast(u4, i + 1);
2587
2691
  }
2588
- return @intCast(u4, i + 1);
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;
2589
2697
  }
2590
2698
  }
2591
2699
 
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;
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);
2595
2703
  }
2596
- }
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);
2716
+ }
2717
+ }
2718
+
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);
2731
+ }
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
+ };
2597
2738
 
2598
2739
  test "RNG agreement" {
2599
2740
  if (!showdown) return;
@@ -2693,9 +2834,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2693
2834
  while (slot <= 4) : (slot += 1) {
2694
2835
  const m = active.moves[slot - 1];
2695
2836
  if (m.id == .None) break;
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;
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;
2699
2842
  out[n] = .{ .type = .Move, .data = s };
2700
2843
  n += 1;
2701
2844
  return n;