@pkmn/engine 0.1.0-dev.207fa86a → 0.1.0-dev.24867658

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;
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
299
318
  const spe2 = battle.side(.P2).active.stats.spe;
300
319
  if (spe1 == spe2) {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
-
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
321
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
+ battle.rng.advance(1);
323
+ }
308
324
 
325
+ const p1 = Rolls.speedTie(battle);
309
326
  if (!showdown) return if (p1) .P1 else .P2;
310
327
 
311
328
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
332
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
333
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
334
  // 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
335
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
336
  // won the original coin flip
320
337
  if (t1 and t2) return .P1;
321
338
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +345,13 @@ fn doTurn(
328
345
  battle: anytype,
329
346
  player: Player,
330
347
  player_choice: Choice,
348
+ player_rewrap: bool,
349
+ player_skip: bool,
331
350
  player_from: ?Move,
332
351
  foe_player: Player,
333
352
  foe_choice: Choice,
353
+ foe_rewrap: bool,
354
+ foe_skip: bool,
334
355
  foe_from: ?Move,
335
356
  log: anytype,
336
357
  ) !?Result {
@@ -338,7 +359,16 @@ fn doTurn(
338
359
 
339
360
  var residual = true;
340
361
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
362
+ if (try executeMove(
363
+ battle,
364
+ player,
365
+ player_choice,
366
+ player_rewrap,
367
+ player_skip,
368
+ player_from,
369
+ &residual,
370
+ log,
371
+ )) |r| return r;
342
372
  if (!replace) {
343
373
  if (player_choice.type != .Switch) {
344
374
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -347,8 +377,18 @@ fn doTurn(
347
377
  if (try checkFaint(battle, player, log)) |r| return r;
348
378
  } else if (foe_choice.type == .Pass) return null;
349
379
 
380
+ residual = true;
350
381
  replace = battle.side(foe_player).stored().hp == 0;
351
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
382
+ if (try executeMove(
383
+ battle,
384
+ foe_player,
385
+ foe_choice,
386
+ foe_rewrap,
387
+ foe_skip,
388
+ foe_from,
389
+ &residual,
390
+ log,
391
+ )) |r| return r;
352
392
  if (!replace) {
353
393
  if (foe_choice.type != .Switch) {
354
394
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +411,8 @@ fn executeMove(
371
411
  battle: anytype,
372
412
  player: Player,
373
413
  choice: Choice,
414
+ rewrap: bool,
415
+ skip: bool,
374
416
  from: ?Move,
375
417
  residual: *bool,
376
418
  log: anytype,
@@ -382,9 +424,10 @@ fn executeMove(
382
424
  return null;
383
425
  }
384
426
 
427
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
428
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
429
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
385
430
  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
431
  assert(!showdown);
389
432
  if (battle.foe(player).active.volatiles.Binding) {
390
433
  try log.cant(battle.active(player), .Bound);
@@ -403,9 +446,9 @@ fn executeMove(
403
446
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
404
447
  assert(!showdown);
405
448
  mslot = @intCast(u4, if (player == .P1)
406
- battle.last_selected_indexes.p1
449
+ battle.last_moves.p1_index
407
450
  else
408
- battle.last_selected_indexes.p2);
451
+ battle.last_moves.p2_index);
409
452
  const stored = side.stored();
410
453
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
411
454
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -427,14 +470,14 @@ fn executeMove(
427
470
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
428
471
  assert(mslot == 1);
429
472
  mslot = @intCast(u4, if (player == .P1)
430
- battle.last_selected_indexes.p1
473
+ battle.last_moves.p1_index
431
474
  else
432
- battle.last_selected_indexes.p2);
475
+ battle.last_moves.p2_index);
433
476
  }
434
477
 
435
478
  var skip_can = false;
436
479
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, log)) {
480
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
481
  .done => return null,
439
482
  .skip_can => skip_can = true,
440
483
  .skip_pp => skip_pp = true,
@@ -442,14 +485,23 @@ fn executeMove(
442
485
  .err => return @as(?Result, Result.Error),
443
486
  }
444
487
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
488
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
489
+ return null;
490
+ }
446
491
 
447
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
448
493
  }
449
494
 
450
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
451
496
 
452
- fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
497
+ fn beforeMove(
498
+ battle: anytype,
499
+ player: Player,
500
+ skip: bool,
501
+ from: ?Move,
502
+ residual: *bool,
503
+ log: anytype,
504
+ ) !BeforeMove {
453
505
  var side = battle.side(player);
454
506
  const foe = battle.foe(player);
455
507
  var active = &side.active;
@@ -477,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
477
529
  return .done;
478
530
  }
479
531
 
480
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
481
533
  try log.cant(ident, .Bound);
482
534
  return .done;
483
535
  }
484
536
 
485
- if (!showdown and volatiles.Flinch) {
486
- volatiles.Flinch = false;
537
+ if (volatiles.Flinch) {
538
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
539
+ // Pokémon can sometimes flinch multiple times from the same original hit
540
+ if (!showdown) volatiles.Flinch = false;
487
541
  try log.cant(ident, .Flinch);
488
542
  return .done;
489
543
  }
@@ -504,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
504
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
505
559
  if (showdown and try disabled(side, ident, log)) return .done;
506
560
 
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
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
515
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
516
563
  if (side.last_selected_move == .None) {
@@ -528,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
528
575
  } else {
529
576
  try log.activate(ident, .Confusion);
530
577
 
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) {
578
+ if (Rolls.confused(battle)) {
537
579
  assert(!volatiles.MultiHit);
538
- volatiles.state = 0;
580
+ if (!volatiles.Rage) volatiles.state = 0;
539
581
  volatiles.Bide = false;
540
582
  volatiles.Thrashing = false;
541
583
  volatiles.MultiHit = false;
@@ -559,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
559
601
  player
560
602
  else
561
603
  player.foe();
604
+
605
+ const uncapped = battle.last_damage;
562
606
  // Skipping adjustDamage / randomizeDamage / checkHit
563
607
  _ = try applyDamage(battle, player, target, .Confusion, log);
608
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
609
+ if (showdown) battle.last_damage = uncapped;
564
610
 
565
611
  return .done;
566
612
  }
@@ -569,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
569
615
 
570
616
  if (!showdown and try disabled(side, ident, log)) return .done;
571
617
 
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
- }
618
+ if (Status.is(stored.status, .PAR) and Rolls.paralyzed(battle)) {
619
+ if (!volatiles.Rage) volatiles.state = 0;
620
+ volatiles.Bide = false;
621
+ volatiles.Thrashing = false;
622
+ volatiles.Charging = false;
623
+ volatiles.Binding = false;
624
+ // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
625
+ try log.cant(ident, .Paralysis);
626
+ return .done;
588
627
  }
589
628
 
590
629
  if (volatiles.Bide) {
591
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
592
631
 
593
- volatiles.state +%= battle.last_damage;
632
+ if (showdown) {
633
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
+ // addition because 65535 is sufficient to faint any Pokémon anyway
636
+ volatiles.state +|= battle.last_damage;
637
+ } else {
638
+ volatiles.state +%= battle.last_damage;
639
+ }
594
640
 
595
641
  assert(volatiles.attacks > 0);
596
642
 
@@ -611,7 +657,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
611
657
  return .done;
612
658
  }
613
659
 
660
+ const sub = showdown and foe.active.volatiles.Substitute;
614
661
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
662
+ if (foe.stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
663
+
664
+ // For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
665
+ // Bide's user even if the above damage has caused the foe to faint. It's simpler to always
666
+ // run residual here regardless of whether the foe fainted and opt-out of the default flow
667
+ if (showdown) {
668
+ residual.* = false;
669
+ try handleResidual(battle, player, log);
670
+ }
671
+
615
672
  return .done;
616
673
  }
617
674
 
@@ -623,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
623
680
  try log.start(battle.active(player), .ConfusionSilent);
624
681
  }
625
682
  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);
683
+ if (showdown) {
684
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
685
+ // used in useMove and doesn't have the notion of skipping canMove semantics
686
+ side.last_used_move = side.last_selected_move;
687
+ if (handleThrashing(battle, active)) {
688
+ try log.start(battle.active(player), .ConfusionSilent);
689
+ }
628
690
  }
629
691
  return .skip_can;
630
692
  }
@@ -639,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
639
701
  side.last_selected_move,
640
702
  );
641
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
642
705
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
706
+ if (battle.foe(player).stored().hp > 0 and !sub) {
707
+ try buildRage(battle, player.foe(), log);
708
+ }
643
709
  }
644
710
  return .done;
645
711
  }
@@ -654,6 +720,7 @@ fn canMove(
654
720
  auto: bool,
655
721
  skip_pp: bool,
656
722
  from: ?Move,
723
+ residual: *bool,
657
724
  log: anytype,
658
725
  ) !bool {
659
726
  var side = battle.side(player);
@@ -670,6 +737,7 @@ fn canMove(
670
737
  side.active.volatiles.Invulnerable = false;
671
738
  } else if (move.effect == .Charge) {
672
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
673
741
  try Effects.charge(battle, player, log);
674
742
  return false;
675
743
  }
@@ -686,9 +754,10 @@ fn canMove(
686
754
  // regrettable, but this information is more "nice to have" than required
687
755
  const f = if (from != null and from.? == .None) null else from;
688
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
689
758
 
690
759
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
760
+ try onBegin(battle, player, move, mslot, residual, log);
692
761
  return false;
693
762
  }
694
763
 
@@ -696,12 +765,27 @@ fn canMove(
696
765
  Effects.thrashing(battle, player);
697
766
  } else if (!showdown and move.effect == .Binding) {
698
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
699
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
700
769
  }
701
770
 
702
771
  return true;
703
772
  }
704
773
 
774
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
+ // in cases where an early return happens the data for a players last selected move does not get
777
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
+ // as the desync results from actually having two separate battle states that subtly disagree
780
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
+ (move.type == .Normal or move.type == .Fighting);
782
+ if (player == .P1) {
783
+ battle.last_moves.p1_counterable = @boolToInt(counterable);
784
+ } else {
785
+ battle.last_moves.p2_counterable = @boolToInt(counterable);
786
+ }
787
+ }
788
+
705
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
706
790
  if (side.last_selected_move == .Struggle) return;
707
791
 
@@ -737,6 +821,7 @@ fn doMove(
737
821
  battle: anytype,
738
822
  player: Player,
739
823
  mslot: u4,
824
+ rewrap: bool,
740
825
  auto: bool,
741
826
  residual: *bool,
742
827
  log: anytype,
@@ -767,16 +852,21 @@ fn doMove(
767
852
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
768
853
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
769
854
  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;
855
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
856
+ battle.last_damage = 0;
857
+ break :miss true;
858
+ }
771
859
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
772
860
  };
773
861
  assert(!immune or miss or (showdown and move.effect == .Binding));
774
862
 
863
+ var late = showdown and move.effect != .Explode;
775
864
  const skip = status or immune;
776
865
  if ((!showdown or (!skip or counter)) and !miss) blk: {
777
866
  if (showdown and move.effect.isMulti()) {
778
867
  Effects.multiHit(battle, player, move);
779
868
  hits = side.active.volatiles.attacks;
869
+ late = false;
780
870
  }
781
871
 
782
872
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -810,7 +900,7 @@ fn doMove(
810
900
  }
811
901
 
812
902
  // 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;
903
+ if (showdown and skip) battle.last_damage = 0;
814
904
 
815
905
  miss = if (showdown or skip)
816
906
  miss
@@ -823,9 +913,9 @@ fn doMove(
823
913
 
824
914
  if (!showdown or !miss) {
825
915
  if (move.effect == .MirrorMove) {
826
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
827
917
  } else if (move.effect == .Metronome) {
828
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
829
919
  } else if (move.effect.onEnd()) {
830
920
  try onEnd(battle, player, move, log);
831
921
  return null;
@@ -840,7 +930,7 @@ fn doMove(
840
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
841
931
  if (ohko) {
842
932
  try log.immune(foe_ident, .OHKO);
843
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
844
934
  try log.immune(foe_ident, .None);
845
935
  } else if (mist) {
846
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -857,15 +947,9 @@ fn doMove(
857
947
  if (showdown and side.stored().hp == 0) residual.* = false;
858
948
  } else if (move.effect == .Explode) {
859
949
  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
- }
950
+ try buildRage(battle, player.foe(), log);
951
+ } else if (showdown and move.effect == .Disable) {
952
+ try buildRage(battle, player.foe(), log);
869
953
  }
870
954
  return null;
871
955
  }
@@ -878,6 +962,9 @@ fn doMove(
878
962
  hits = side.active.volatiles.attacks;
879
963
  }
880
964
 
965
+ // Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
966
+ const sub = showdown and foe.active.volatiles.Substitute;
967
+
881
968
  var nullified = false;
882
969
  var hit: u4 = 0;
883
970
  while (hit < hits) {
@@ -893,9 +980,7 @@ fn doMove(
893
980
  if (hit == 0 and ohko) try log.ohko();
894
981
  hit += 1;
895
982
  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
- }
983
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
984
  // If the substitute breaks during a multi-hit attack, the attack ends
900
985
  if (nullified) break;
901
986
  }
@@ -911,11 +996,17 @@ fn doMove(
911
996
  } else if (showdown) {
912
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
913
998
  if (move.effect == .Binding) {
914
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
915
1000
  if (immune) {
916
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
917
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
918
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1004
+ if (sub) {
1005
+ try log.activate(battle.active(player.foe()), .Substitute);
1006
+ } else {
1007
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try buildRage(battle, player.foe(), log);
1009
+ }
919
1010
  return null;
920
1011
  }
921
1012
  }
@@ -932,6 +1023,9 @@ fn doMove(
932
1023
 
933
1024
  if (foe.stored().hp == 0) return null;
934
1025
 
1026
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit move
1027
+ if (late and !sub and move.effect != .Disable) try buildRage(battle, player.foe(), log);
1028
+
935
1029
  if (!move.effect.isSpecial()) {
936
1030
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
937
1031
  // second time here (after being executed in the "always happens" block above) but that
@@ -943,7 +1037,9 @@ fn doMove(
943
1037
  if (move.effect == .Twineedle) {
944
1038
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
945
1039
  } else if (move.effect == .Disable) {
946
- return Effects.disable(battle, player, move, log);
1040
+ const result = try Effects.disable(battle, player, move, log);
1041
+ if (showdown) try buildRage(battle, player.foe(), log);
1042
+ return result;
947
1043
  } else {
948
1044
  try moveEffect(battle, player, move, log);
949
1045
  }
@@ -961,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
961
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
962
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
963
1059
 
964
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
965
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
966
1061
  }
967
1062
 
968
1063
  fn calcDamage(
@@ -1066,49 +1161,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1066
1161
 
1067
1162
  fn randomizeDamage(battle: anytype) void {
1068
1163
  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
-
1164
+ const random = Rolls.damage(battle);
1079
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1080
1166
  }
1081
1167
 
1082
1168
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1083
1169
  const side = battle.side(player);
1170
+ const foe = battle.foe(player);
1084
1171
 
1085
1172
  if (!try checkHit(battle, player, move, log)) return null;
1086
1173
 
1087
1174
  battle.last_damage = switch (side.last_selected_move) {
1088
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1175
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1089
1176
  .SeismicToss, .NightShade => side.stored().level,
1090
1177
  .SonicBoom => 20,
1091
1178
  .DragonRage => 40,
1092
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1093
1180
  .Psywave => power: {
1094
1181
  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
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1105
1185
  },
1106
1186
  else => unreachable,
1107
1187
  };
1108
1188
 
1109
1189
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1110
1190
 
1191
+ const sub = showdown and foe.active.volatiles.Substitute;
1111
1192
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1193
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1194
+
1112
1195
  return null;
1113
1196
  }
1114
1197
 
@@ -1120,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1120
1203
  return null;
1121
1204
  }
1122
1205
 
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);
1206
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1126
1207
  const foe_last_selected_move =
1127
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1128
- .Counter
1209
+ .Splash
1129
1210
  else
1130
1211
  foe.last_selected_move);
1131
1212
 
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);
1213
+ const used = (if (player.foe() == .P1)
1214
+ battle.last_moves.p1_counterable
1215
+ else
1216
+ battle.last_moves.p2_counterable) != 0;
1136
1217
 
1137
1218
  const selected = foe_last_selected_move.bp > 0 and
1138
1219
  foe.last_selected_move != .Counter and
@@ -1151,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1151
1232
  return null;
1152
1233
  }
1153
1234
 
1154
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1155
1236
 
1156
1237
  // Pokémon Showdown calls checkHit before Counter
1157
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1158
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1159
1241
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1242
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1160
1243
  return null;
1161
1244
  }
1162
1245
 
@@ -1200,6 +1283,7 @@ fn mirrorMove(
1200
1283
  battle: anytype,
1201
1284
  player: Player,
1202
1285
  mslot: u4,
1286
+ rewrap: bool,
1203
1287
  auto: bool,
1204
1288
  residual: *bool,
1205
1289
  log: anytype,
@@ -1212,41 +1296,30 @@ fn mirrorMove(
1212
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1213
1297
  try log.fail(battle.active(player), .None);
1214
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1215
1301
  }
1216
1302
 
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);
1303
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1221
1305
  }
1222
1306
 
1223
1307
  fn metronome(
1224
1308
  battle: anytype,
1225
1309
  player: Player,
1226
1310
  mslot: u4,
1311
+ rewrap: bool,
1227
1312
  auto: bool,
1228
1313
  residual: *bool,
1229
1314
  log: anytype,
1230
1315
  ) !?Result {
1231
1316
  var side = battle.side(player);
1232
1317
 
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
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1246
1319
  incrementPP(side, mslot);
1247
1320
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1249
- return doMove(battle, player, mslot, auto, residual, log);
1321
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
1323
  }
1251
1324
 
1252
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1278,7 +1351,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1278
1351
  if (move.effect == .Swift) return true;
1279
1352
  if (foe.active.volatiles.Invulnerable) break :miss true;
1280
1353
  }
1281
- if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1354
+ if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1355
+ (showdown and foe.active.volatiles.Substitute)))
1356
+ {
1282
1357
  immune.* = true;
1283
1358
  if (showdown) return false;
1284
1359
  break :miss true;
@@ -1298,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1298
1373
  }
1299
1374
 
1300
1375
  // 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));
1376
+ const state = side.active.volatiles.state;
1377
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1378
+ const overwritten = overwrite and state > 0;
1379
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1380
+
1381
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1305
1382
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1306
1383
  accuracy = accuracy * boost[0] / boost[1];
1307
1384
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1308
1385
  accuracy = accuracy * boost[0] / boost[1];
1309
1386
  accuracy = @min(255, @max(1, accuracy));
1310
1387
 
1311
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1388
+ // Pokémon Showdown only overwrites if the volatile is present
1389
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1390
+ if (overwrite) side.active.volatiles.state = accuracy;
1312
1391
 
1313
1392
  // 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;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1318
1394
  };
1319
1395
 
1320
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1429,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1429
1505
  }
1430
1506
 
1431
1507
  if (volatiles.LeechSeed) {
1508
+ var foe = battle.foe(player);
1509
+ var foe_stored = foe.stored();
1510
+ const foe_ident = battle.active(player.foe());
1511
+
1512
+ if (foe_stored.hp == 0) {
1513
+ assert(showdown);
1514
+ return;
1515
+ }
1516
+
1432
1517
  var damage = @max(stored.stats.hp / 16, 1);
1433
1518
 
1434
1519
  // GLITCH: Leech Seed + Toxic glitch
@@ -1440,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1440
1525
  const amount = @min(damage, stored.hp);
1441
1526
  stored.hp -= amount;
1442
1527
 
1443
- var foe = battle.foe(player);
1444
- var foe_stored = foe.stored();
1445
- const foe_ident = battle.active(player.foe());
1446
-
1447
1528
  // As above, Pokémon Showdown uses damageOf but its not relevant
1448
1529
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1449
1530
 
@@ -1459,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1459
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1460
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1461
1542
 
1543
+ battle.turn += 1;
1544
+
1462
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1463
1546
  try log.tie();
1464
1547
  return Result.Tie;
1465
1548
  }
1466
1549
 
1467
- battle.turn += 1;
1468
-
1469
1550
  if (showdown and battle.turn >= 1000) {
1470
1551
  try log.tie();
1471
1552
  return Result.Tie;
@@ -1480,16 +1561,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1480
1561
 
1481
1562
  fn checkEBC(battle: anytype) bool {
1482
1563
  for (battle.sides, 0..) |side, i| {
1564
+ const foe = battle.sides[~@intCast(u1, i)];
1565
+
1483
1566
  var foe_all_ghosts = true;
1484
1567
  var foe_all_transform = true;
1568
+ for (foe.order, 0..) |id, j| {
1569
+ if (id == 0) break;
1570
+ const active = j == 0;
1571
+ const pokemon = foe.pokemon[id - 1];
1485
1572
 
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);
1573
+ const ghost = pokemon.hp == 0 or
1574
+ (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1490
1575
  foe_all_ghosts = foe_all_ghosts and ghost;
1491
1576
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1492
- for (pokemon.moves) |m| {
1577
+ for (if (active) foe.active.moves else pokemon.moves) |m| {
1493
1578
  if (m.id == .None) break :transform true;
1494
1579
  if (m.id != .Transform) break :transform false;
1495
1580
  }
@@ -1497,10 +1582,14 @@ fn checkEBC(battle: anytype) bool {
1497
1582
  };
1498
1583
  }
1499
1584
 
1500
- for (side.pokemon) |pokemon| {
1585
+ for (side.order, 0..) |id, j| {
1586
+ if (id == 0) break;
1587
+ const active = j == 0;
1588
+ const pokemon = side.pokemon[id - 1];
1589
+
1501
1590
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1502
1591
  const transform = foe_all_transform and transform: {
1503
- for (pokemon.moves) |m| {
1592
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1504
1593
  if (m.id == .None) break :transform true;
1505
1594
  if (m.id != .Transform) break :transform false;
1506
1595
  }
@@ -1508,7 +1597,7 @@ fn checkEBC(battle: anytype) bool {
1508
1597
  };
1509
1598
  if (transform) continue;
1510
1599
  const no_pp = foe_all_ghosts and no_pp: {
1511
- for (pokemon.moves) |m| {
1600
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1512
1601
  if (m.pp != 0) break :no_pp false;
1513
1602
  }
1514
1603
  break :no_pp true;
@@ -1524,7 +1613,50 @@ fn checkEBC(battle: anytype) bool {
1524
1613
  return false;
1525
1614
  }
1526
1615
 
1527
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1616
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1617
+ if (side.active.volatiles.disabled_move != 0) {
1618
+ // A Pokémon that transforms after being disabled may end up with less move slots
1619
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1620
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1621
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1622
+ m
1623
+ else
1624
+ side.last_selected_move;
1625
+ if (m != .None and m == last) {
1626
+ side.active.volatiles.Charging = false;
1627
+ try log.disabled(ident, last);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1635
+ const side = battle.side(who);
1636
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1637
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1638
+ }
1639
+ }
1640
+
1641
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1642
+ var volatiles = &active.volatiles;
1643
+ assert(volatiles.Thrashing);
1644
+ if (volatiles.attacks > 0) return false;
1645
+
1646
+ volatiles.Thrashing = false;
1647
+ volatiles.Confusion = true;
1648
+ volatiles.confusion = Rolls.confusionDuration(battle);
1649
+ return true;
1650
+ }
1651
+
1652
+ inline fn onBegin(
1653
+ battle: anytype,
1654
+ player: Player,
1655
+ move: Move.Data,
1656
+ mslot: u8,
1657
+ residual: *bool,
1658
+ log: anytype,
1659
+ ) !void {
1528
1660
  assert(move.effect.onBegin());
1529
1661
  return switch (move.effect) {
1530
1662
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1540,7 +1672,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1540
1672
  .Poison => Effects.poison(battle, player, move, log),
1541
1673
  .Reflect => Effects.reflect(battle, player, log),
1542
1674
  .Splash => Effects.splash(battle, player, log),
1543
- .Substitute => Effects.substitute(battle, player, log),
1675
+ .Substitute => Effects.substitute(battle, player, residual, log),
1544
1676
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1545
1677
  .Transform => Effects.transform(battle, player, log),
1546
1678
  else => unreachable,
@@ -1607,10 +1739,7 @@ pub const Effects = struct {
1607
1739
  side.active.volatiles.Bide = true;
1608
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1609
1741
  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);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1614
1743
 
1615
1744
  try log.start(battle.active(player), .Bide);
1616
1745
  }
@@ -1632,14 +1761,8 @@ pub const Effects = struct {
1632
1761
  return;
1633
1762
  }
1634
1763
 
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;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1643
1766
 
1644
1767
  foe_stored.status = Status.init(.BRN);
1645
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1663,11 +1786,7 @@ pub const Effects = struct {
1663
1786
  const sub = foe.active.volatiles.Substitute;
1664
1787
 
1665
1788
  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;
1789
+ if (!Rolls.confusionChance(battle)) return;
1671
1790
  } else {
1672
1791
  if (showdown) {
1673
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1686,10 +1805,7 @@ pub const Effects = struct {
1686
1805
 
1687
1806
  if (foe.active.volatiles.Confusion) return;
1688
1807
  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);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1693
1809
 
1694
1810
  try log.start(battle.active(player.foe()), .Confusion);
1695
1811
  }
@@ -1739,11 +1855,8 @@ pub const Effects = struct {
1739
1855
  return Result.Error;
1740
1856
  }
1741
1857
 
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);
1858
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1747
1860
 
1748
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1749
1862
  return null;
@@ -1753,6 +1866,11 @@ pub const Effects = struct {
1753
1866
  var side = battle.side(player);
1754
1867
  var stored = side.stored();
1755
1868
 
1869
+ if (battle.last_damage == 0) {
1870
+ assert(showdown);
1871
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1872
+ }
1873
+
1756
1874
  const drain = @max(battle.last_damage / 2, 1);
1757
1875
  battle.last_damage = drain;
1758
1876
 
@@ -1777,15 +1895,7 @@ pub const Effects = struct {
1777
1895
  var volatiles = &battle.foe(player).active.volatiles;
1778
1896
 
1779
1897
  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;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1789
1899
 
1790
1900
  volatiles.Flinch = true;
1791
1901
  volatiles.Recharging = false;
@@ -1811,12 +1921,8 @@ pub const Effects = struct {
1811
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1812
1922
  }
1813
1923
 
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
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1820
1926
  // Freeze Clause Mod
1821
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1822
1928
 
@@ -1953,15 +2059,15 @@ pub const Effects = struct {
1953
2059
  }
1954
2060
  break :has_mimic false;
1955
2061
  };
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) {
2062
+ if (!has_mimic) {
2063
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2064
+ if (!try checkHit(battle, player, move, log)) return;
1959
2065
  return try log.fail(battle.active(player.foe()), .None);
1960
2066
  }
1961
2067
  }
1962
2068
  if (!try checkHit(battle, player, move, log)) return;
1963
2069
 
1964
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1965
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
1966
2072
 
1967
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -1982,7 +2088,8 @@ pub const Effects = struct {
1982
2088
  assert(!side.active.volatiles.MultiHit);
1983
2089
  side.active.volatiles.MultiHit = true;
1984
2090
 
1985
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2091
+ side.active.volatiles.attacks =
2092
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
1986
2093
  }
1987
2094
 
1988
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2026,14 +2133,8 @@ pub const Effects = struct {
2026
2133
  }
2027
2134
 
2028
2135
  // 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;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2037
2138
 
2038
2139
  foe_stored.status = Status.init(.PAR);
2039
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2063,7 +2164,7 @@ pub const Effects = struct {
2063
2164
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2064
2165
  if (!showdown)
2065
2166
  .Poison
2066
- else if (toxic == foe.active.volatiles.Toxic)
2167
+ else if (toxic == (foe_stored.status == Status.TOX))
2067
2168
  if (toxic) .Toxic else .Poison
2068
2169
  else
2069
2170
  .None
@@ -2076,16 +2177,7 @@ pub const Effects = struct {
2076
2177
 
2077
2178
  if (move.effect == .Poison) {
2078
2179
  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
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2089
2181
 
2090
2182
  foe_stored.status = Status.init(.PSN);
2091
2183
  if (toxic) {
@@ -2099,10 +2191,8 @@ pub const Effects = struct {
2099
2191
 
2100
2192
  fn rage(battle: anytype, player: Player) !void {
2101
2193
  var volatiles = &battle.side(player).active.volatiles;
2102
- if (volatiles.Rage) return;
2103
- volatiles.Rage = true;
2104
2194
  assert(!volatiles.Bide);
2105
- volatiles.state = 0;
2195
+ volatiles.Rage = true;
2106
2196
  }
2107
2197
 
2108
2198
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2154,16 +2244,7 @@ pub const Effects = struct {
2154
2244
  }
2155
2245
  foe.active.volatiles.Recharging = false;
2156
2246
 
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);
2247
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2167
2248
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2168
2249
  }
2169
2250
 
@@ -2171,7 +2252,7 @@ pub const Effects = struct {
2171
2252
  try log.activate(battle.active(player), .Splash);
2172
2253
  }
2173
2254
 
2174
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2255
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2175
2256
  var side = battle.side(player);
2176
2257
  if (side.active.volatiles.Substitute) {
2177
2258
  try log.fail(battle.active(player), .Substitute);
@@ -2196,14 +2277,18 @@ pub const Effects = struct {
2196
2277
  side.active.volatiles.substitute = hp + 1;
2197
2278
  side.active.volatiles.Substitute = true;
2198
2279
  try log.start(battle.active(player), .Substitute);
2199
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2280
+ if (hp > 0) {
2281
+ try log.damage(battle.active(player), side.stored(), .None);
2282
+ if (showdown and side.stored().hp == 0) residual.* = false;
2283
+ }
2200
2284
  }
2201
2285
 
2202
2286
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2203
2287
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2204
2288
 
2205
- // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2289
+ // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2206
2290
  _ = try checkHit(battle, player, move, log);
2291
+ battle.last_damage = 0;
2207
2292
  }
2208
2293
 
2209
2294
  fn thrashing(battle: anytype, player: Player) void {
@@ -2212,11 +2297,7 @@ pub const Effects = struct {
2212
2297
  assert(!volatiles.Bide);
2213
2298
 
2214
2299
  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);
2300
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2220
2301
  }
2221
2302
 
2222
2303
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2248,7 +2329,7 @@ pub const Effects = struct {
2248
2329
  try log.transform(battle.active(player), foe_ident);
2249
2330
  }
2250
2331
 
2251
- fn binding(battle: anytype, player: Player) void {
2332
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2252
2333
  var side = battle.side(player);
2253
2334
  var foe = battle.foe(player);
2254
2335
 
@@ -2256,9 +2337,11 @@ pub const Effects = struct {
2256
2337
  side.active.volatiles.Binding = true;
2257
2338
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2258
2339
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2259
- if (!showdown) foe.active.volatiles.Recharging = false;
2340
+ if (!showdown) {
2341
+ foe.active.volatiles.Recharging = false;
2342
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2260
2343
 
2261
- side.active.volatiles.attacks = distribution(battle) - 1;
2344
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2262
2345
  }
2263
2346
 
2264
2347
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2285,9 +2368,11 @@ pub const Effects = struct {
2285
2368
  return try log.fail(ident, .None);
2286
2369
  }
2287
2370
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2288
- const stat = unmodifiedStats(battle, player).atk;
2371
+ const stat = unmodifiedStats(battle, side).atk;
2289
2372
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2290
2373
  try log.boost(ident, reason, n);
2374
+ // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
2375
+ if (showdown and move.effect == .Rage) return;
2291
2376
  },
2292
2377
  .DefenseUp1, .DefenseUp2 => {
2293
2378
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2303,7 +2388,7 @@ pub const Effects = struct {
2303
2388
  return try log.fail(ident, .None);
2304
2389
  }
2305
2390
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2306
- const stat = unmodifiedStats(battle, player).def;
2391
+ const stat = unmodifiedStats(battle, side).def;
2307
2392
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2308
2393
  try log.boost(ident, .Defense, n);
2309
2394
  },
@@ -2320,7 +2405,7 @@ pub const Effects = struct {
2320
2405
  return try log.fail(ident, .None);
2321
2406
  }
2322
2407
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2323
- const stat = unmodifiedStats(battle, player).spe;
2408
+ const stat = unmodifiedStats(battle, side).spe;
2324
2409
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2325
2410
  try log.boost(ident, .Speed, 2);
2326
2411
  },
@@ -2340,7 +2425,7 @@ pub const Effects = struct {
2340
2425
  return try log.fail(ident, .None);
2341
2426
  }
2342
2427
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2343
- const stat = unmodifiedStats(battle, player).spc;
2428
+ const stat = unmodifiedStats(battle, side).spc;
2344
2429
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2345
2430
  try log.boost(ident, .SpecialAttack, n);
2346
2431
  try log.boost(ident, .SpecialDefense, n);
@@ -2367,11 +2452,7 @@ pub const Effects = struct {
2367
2452
  }
2368
2453
 
2369
2454
  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;
2455
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2375
2456
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2376
2457
  return; // checkHit already checks for Invulnerable
2377
2458
  }
@@ -2393,7 +2474,7 @@ pub const Effects = struct {
2393
2474
  return try log.fail(foe_ident, .None);
2394
2475
  }
2395
2476
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2396
- const stat = unmodifiedStats(battle, player.foe()).atk;
2477
+ const stat = unmodifiedStats(battle, foe).atk;
2397
2478
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2398
2479
  try log.boost(foe_ident, .Attack, -1);
2399
2480
  },
@@ -2411,7 +2492,7 @@ pub const Effects = struct {
2411
2492
  return try log.fail(foe_ident, .None);
2412
2493
  }
2413
2494
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2414
- const stat = unmodifiedStats(battle, player.foe()).def;
2495
+ const stat = unmodifiedStats(battle, foe).def;
2415
2496
  stats.def = @max(1, stat * mod[0] / mod[1]);
2416
2497
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2417
2498
  },
@@ -2428,7 +2509,7 @@ pub const Effects = struct {
2428
2509
  return try log.fail(foe_ident, .None);
2429
2510
  }
2430
2511
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2431
- const stat = unmodifiedStats(battle, player.foe()).spe;
2512
+ const stat = unmodifiedStats(battle, foe).spe;
2432
2513
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2433
2514
  try log.boost(foe_ident, .Speed, -1);
2434
2515
  assert(boosts.spe >= -6);
@@ -2448,7 +2529,7 @@ pub const Effects = struct {
2448
2529
  return try log.fail(foe_ident, .None);
2449
2530
  }
2450
2531
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2451
- const stat = unmodifiedStats(battle, player.foe()).spc;
2532
+ const stat = unmodifiedStats(battle, foe).spc;
2452
2533
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2453
2534
  try log.boost(foe_ident, .SpecialAttack, -1);
2454
2535
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2467,8 +2548,7 @@ pub const Effects = struct {
2467
2548
  }
2468
2549
  };
2469
2550
 
2470
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2471
- const side = battle.side(who);
2551
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2472
2552
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2473
2553
  const id = ID.from(side.active.volatiles.transform);
2474
2554
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2482,7 +2562,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2482
2562
  }
2483
2563
  }
2484
2564
 
2485
- fn isForced(active: ActivePokemon) bool {
2565
+ inline fn isForced(active: ActivePokemon) bool {
2486
2566
  return active.volatiles.Recharging or active.volatiles.Rage or
2487
2567
  active.volatiles.Thrashing or active.volatiles.Charging;
2488
2568
  }
@@ -2514,13 +2594,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2514
2594
  volatiles.LeechSeed = false;
2515
2595
  try log.end(ident, .LeechSeed);
2516
2596
  }
2517
- if (volatiles.Toxic) {
2597
+ if (!showdown and volatiles.Toxic) {
2518
2598
  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
- }
2599
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2524
2600
  }
2525
2601
  if (volatiles.LightScreen) {
2526
2602
  volatiles.LightScreen = false;
@@ -2530,70 +2606,148 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2530
2606
  volatiles.Reflect = false;
2531
2607
  try log.end(ident, .Reflect);
2532
2608
  }
2609
+ if (showdown and volatiles.Toxic) {
2610
+ volatiles.Toxic = false;
2611
+ // Pokémon Showdown erroneously clears the toxic counter
2612
+ volatiles.toxic = 0;
2613
+ try log.end(ident, .Toxic);
2614
+ }
2533
2615
  }
2534
2616
 
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;
2617
+ pub const Rolls = struct {
2618
+ inline fn speedTie(battle: anytype) bool {
2619
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2620
+ return battle.rng.next() < Gen12.percent(50) + 1;
2621
+ }
2622
+
2623
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2624
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2625
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2626
+ }
2627
+
2628
+ inline fn damage(battle: anytype) u8 {
2629
+ if (showdown) return battle.rng.range(u8, 217, 256);
2630
+ while (true) {
2631
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2632
+ if (r >= 217) return r;
2543
2633
  }
2544
2634
  }
2545
- return false;
2546
- }
2547
2635
 
2548
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2549
- var volatiles = &active.volatiles;
2550
- assert(volatiles.Thrashing);
2551
- if (volatiles.attacks > 0) return false;
2636
+ inline fn hit(battle: anytype, accuracy: u16) bool {
2637
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, accuracy), 256);
2638
+ return battle.rng.next() < accuracy;
2639
+ }
2552
2640
 
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);
2641
+ inline fn confusionDuration(battle: anytype) u3 {
2642
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
2643
+ return @intCast(u3, (battle.rng.next() & 3) + 2);
2644
+ }
2559
2645
 
2560
- return true;
2561
- }
2646
+ inline fn confused(battle: anytype) bool {
2647
+ if (showdown) return !battle.rng.chance(u8, 128, 256);
2648
+ return battle.rng.next() >= Gen12.percent(50) + 1;
2649
+ }
2562
2650
 
2563
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2651
+ inline fn paralyzed(battle: anytype) bool {
2652
+ if (showdown) return battle.rng.chance(u8, 63, 256);
2653
+ return battle.rng.next() < Gen12.percent(25);
2654
+ }
2564
2655
 
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
- }
2656
+ inline fn confusionChance(battle: anytype) bool {
2657
+ if (showdown) return battle.rng.chance(u8, 25, 256);
2658
+ return battle.rng.next() < Gen12.percent(10);
2659
+ }
2570
2660
 
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;
2661
+ inline fn secondaryChance(battle: anytype, low: bool) bool {
2662
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 26 else 77), 256);
2663
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(10) else Gen12.percent(30));
2664
+ }
2665
+
2666
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2667
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2668
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2669
+ }
2670
+
2671
+ inline fn sleepDuration(battle: anytype) u3 {
2672
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2673
+ while (true) {
2674
+ const r = battle.rng.next() & 7;
2675
+ if (r != 0) return @intCast(u3, r);
2676
+ }
2677
+ }
2678
+
2679
+ inline fn bideThrashDuration(battle: anytype) u3 {
2680
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2681
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2682
+ }
2683
+
2684
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2685
+ if (showdown) {
2686
+ if (check_pp == 0) {
2687
+ var i: usize = moves.len;
2688
+ while (i > 0) {
2689
+ i -= 1;
2690
+ if (moves[i].id != .None) {
2691
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2692
+ }
2693
+ }
2694
+ } else {
2695
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2696
+ var i: usize = 0;
2697
+ while (i < moves.len and r > 0) : (i += 1) {
2698
+ if (moves[i].pp > 0) {
2699
+ r -= 1;
2700
+ if (r == 0) break;
2701
+ }
2586
2702
  }
2703
+ return @intCast(u4, i + 1);
2587
2704
  }
2588
- return @intCast(u4, i + 1);
2705
+ }
2706
+
2707
+ while (true) {
2708
+ const r = @intCast(u4, battle.rng.next() & 3);
2709
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2589
2710
  }
2590
2711
  }
2591
2712
 
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;
2713
+ inline fn disableDuration(battle: anytype) u4 {
2714
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2715
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2595
2716
  }
2596
- }
2717
+
2718
+ inline fn metronome(battle: anytype) Move {
2719
+ if (showdown) {
2720
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
2721
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
2722
+ return @intToEnum(Move, r + mod);
2723
+ }
2724
+ while (true) {
2725
+ const r = battle.rng.next();
2726
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
2727
+ if (r >= @enumToInt(Move.Struggle)) continue;
2728
+ return @intToEnum(Move, r);
2729
+ }
2730
+ }
2731
+
2732
+ inline fn psywave(battle: anytype, max: u8) u8 {
2733
+ if (showdown) return battle.rng.range(u8, 0, max);
2734
+ while (true) {
2735
+ const r = battle.rng.next();
2736
+ if (r < max) return r;
2737
+ }
2738
+ }
2739
+
2740
+ inline fn distribution(battle: anytype) u3 {
2741
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2742
+ const r = (battle.rng.next() & 3);
2743
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2744
+ }
2745
+
2746
+ inline fn unboost(battle: anytype) bool {
2747
+ if (showdown) return battle.rng.chance(u8, 85, 256);
2748
+ return battle.rng.next() < Gen12.percent(33) + 1;
2749
+ }
2750
+ };
2597
2751
 
2598
2752
  test "RNG agreement" {
2599
2753
  if (!showdown) return;
@@ -2693,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2693
2847
  while (slot <= 4) : (slot += 1) {
2694
2848
  const m = active.moves[slot - 1];
2695
2849
  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;
2850
+ if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
2851
+ // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
2852
+ const struggle =
2853
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
2854
+ const s = if (struggle) 0 else slot;
2699
2855
  out[n] = .{ .type = .Move, .data = s };
2700
2856
  n += 1;
2701
2857
  return n;