@pkmn/engine 0.1.0-dev.2ec67dba → 0.1.0-dev.4005e495

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,
@@ -446,12 +489,19 @@ fn executeMove(
446
489
  return null;
447
490
  }
448
491
 
449
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
493
  }
451
494
 
452
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
496
 
454
- 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 {
455
505
  var side = battle.side(player);
456
506
  const foe = battle.foe(player);
457
507
  var active = &side.active;
@@ -479,13 +529,15 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
479
529
  return .done;
480
530
  }
481
531
 
482
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
483
533
  try log.cant(ident, .Bound);
484
534
  return .done;
485
535
  }
486
536
 
487
- if (!showdown and volatiles.Flinch) {
488
- 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;
489
541
  try log.cant(ident, .Flinch);
490
542
  return .done;
491
543
  }
@@ -506,13 +558,6 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
506
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
507
559
  if (showdown and try disabled(side, ident, log)) return .done;
508
560
 
509
- // Pokémon Showdown checks for Flinch *after* instead of before
510
- if (showdown and volatiles.Flinch) {
511
- volatiles.Flinch = false;
512
- try log.cant(ident, .Flinch);
513
- return .done;
514
- }
515
-
516
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
517
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
518
563
  if (side.last_selected_move == .None) {
@@ -530,14 +575,9 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
530
575
  } else {
531
576
  try log.activate(ident, .Confusion);
532
577
 
533
- const confused = if (showdown)
534
- !battle.rng.chance(u8, 128, 256)
535
- else
536
- battle.rng.next() >= Gen12.percent(50) + 1;
537
-
538
- if (confused) {
578
+ if (Rolls.confused(battle)) {
539
579
  assert(!volatiles.MultiHit);
540
- volatiles.state = 0;
580
+ if (!volatiles.Rage) volatiles.state = 0;
541
581
  volatiles.Bide = false;
542
582
  volatiles.Thrashing = false;
543
583
  volatiles.MultiHit = false;
@@ -561,8 +601,12 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
561
601
  player
562
602
  else
563
603
  player.foe();
604
+
605
+ const uncapped = battle.last_damage;
564
606
  // Skipping adjustDamage / randomizeDamage / checkHit
565
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;
566
610
 
567
611
  return .done;
568
612
  }
@@ -571,28 +615,28 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
571
615
 
572
616
  if (!showdown and try disabled(side, ident, log)) return .done;
573
617
 
574
- if (Status.is(stored.status, .PAR)) {
575
- const paralyzed = if (showdown)
576
- battle.rng.chance(u8, 63, 256)
577
- else
578
- battle.rng.next() < Gen12.percent(25);
579
-
580
- if (paralyzed) {
581
- volatiles.state = 0;
582
- volatiles.Bide = false;
583
- volatiles.Thrashing = false;
584
- volatiles.Charging = false;
585
- volatiles.Binding = false;
586
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
587
- try log.cant(ident, .Paralysis);
588
- return .done;
589
- }
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;
590
627
  }
591
628
 
592
629
  if (volatiles.Bide) {
593
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
594
631
 
595
- 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
+ }
596
640
 
597
641
  assert(volatiles.attacks > 0);
598
642
 
@@ -613,7 +657,18 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
613
657
  return .done;
614
658
  }
615
659
 
660
+ const sub = showdown and foe.active.volatiles.Substitute;
616
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
+
617
672
  return .done;
618
673
  }
619
674
 
@@ -625,8 +680,13 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
625
680
  try log.start(battle.active(player), .ConfusionSilent);
626
681
  }
627
682
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
628
- if (showdown and handleThrashing(battle, active)) {
629
- 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
+ }
630
690
  }
631
691
  return .skip_can;
632
692
  }
@@ -641,7 +701,11 @@ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !Befor
641
701
  side.last_selected_move,
642
702
  );
643
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
644
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
+ }
645
709
  }
646
710
  return .done;
647
711
  }
@@ -673,6 +737,7 @@ fn canMove(
673
737
  side.active.volatiles.Invulnerable = false;
674
738
  } else if (move.effect == .Charge) {
675
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
676
741
  try Effects.charge(battle, player, log);
677
742
  return false;
678
743
  }
@@ -689,6 +754,7 @@ fn canMove(
689
754
  // regrettable, but this information is more "nice to have" than required
690
755
  const f = if (from != null and from.? == .None) null else from;
691
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
692
758
 
693
759
  if (move.effect.onBegin()) {
694
760
  try onBegin(battle, player, move, mslot, residual, log);
@@ -699,12 +765,27 @@ fn canMove(
699
765
  Effects.thrashing(battle, player);
700
766
  } else if (!showdown and move.effect == .Binding) {
701
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
702
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
703
769
  }
704
770
 
705
771
  return true;
706
772
  }
707
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
+
708
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
709
790
  if (side.last_selected_move == .Struggle) return;
710
791
 
@@ -740,6 +821,7 @@ fn doMove(
740
821
  battle: anytype,
741
822
  player: Player,
742
823
  mslot: u4,
824
+ rewrap: bool,
743
825
  auto: bool,
744
826
  residual: *bool,
745
827
  log: anytype,
@@ -770,16 +852,21 @@ fn doMove(
770
852
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
771
853
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
772
854
  if (immune and move.effect != .Binding) break :miss true;
773
- 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
+ }
774
859
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
775
860
  };
776
861
  assert(!immune or miss or (showdown and move.effect == .Binding));
777
862
 
863
+ var late = showdown and move.effect != .Explode;
778
864
  const skip = status or immune;
779
865
  if ((!showdown or (!skip or counter)) and !miss) blk: {
780
866
  if (showdown and move.effect.isMulti()) {
781
867
  Effects.multiHit(battle, player, move);
782
868
  hits = side.active.volatiles.attacks;
869
+ late = false;
783
870
  }
784
871
 
785
872
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -813,7 +900,7 @@ fn doMove(
813
900
  }
814
901
 
815
902
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
816
- if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
903
+ if (showdown and skip) battle.last_damage = 0;
817
904
 
818
905
  miss = if (showdown or skip)
819
906
  miss
@@ -826,9 +913,9 @@ fn doMove(
826
913
 
827
914
  if (!showdown or !miss) {
828
915
  if (move.effect == .MirrorMove) {
829
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
830
917
  } else if (move.effect == .Metronome) {
831
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
832
919
  } else if (move.effect.onEnd()) {
833
920
  try onEnd(battle, player, move, log);
834
921
  return null;
@@ -843,7 +930,7 @@ fn doMove(
843
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
844
931
  if (ohko) {
845
932
  try log.immune(foe_ident, .OHKO);
846
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
847
934
  try log.immune(foe_ident, .None);
848
935
  } else if (mist) {
849
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -860,15 +947,9 @@ fn doMove(
860
947
  if (showdown and side.stored().hp == 0) residual.* = false;
861
948
  } else if (move.effect == .Explode) {
862
949
  try Effects.explode(battle, player);
863
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
864
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
865
- }
866
- } else if (showdown) {
867
- if (move.effect == .Disable) {
868
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
869
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
870
- }
871
- }
950
+ try buildRage(battle, player.foe(), log);
951
+ } else if (showdown and move.effect == .Disable) {
952
+ try buildRage(battle, player.foe(), log);
872
953
  }
873
954
  return null;
874
955
  }
@@ -881,6 +962,9 @@ fn doMove(
881
962
  hits = side.active.volatiles.attacks;
882
963
  }
883
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
+
884
968
  var nullified = false;
885
969
  var hit: u4 = 0;
886
970
  while (hit < hits) {
@@ -896,9 +980,7 @@ fn doMove(
896
980
  if (hit == 0 and ohko) try log.ohko();
897
981
  hit += 1;
898
982
  if (foe.stored().hp == 0) break;
899
- if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
900
- try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
901
- }
983
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
902
984
  // If the substitute breaks during a multi-hit attack, the attack ends
903
985
  if (nullified) break;
904
986
  }
@@ -914,11 +996,17 @@ fn doMove(
914
996
  } else if (showdown) {
915
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
916
998
  if (move.effect == .Binding) {
917
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
918
1000
  if (immune) {
919
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
920
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
921
- 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
+ }
922
1010
  return null;
923
1011
  }
924
1012
  }
@@ -935,6 +1023,9 @@ fn doMove(
935
1023
 
936
1024
  if (foe.stored().hp == 0) return null;
937
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
+
938
1029
  if (!move.effect.isSpecial()) {
939
1030
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
940
1031
  // second time here (after being executed in the "always happens" block above) but that
@@ -946,7 +1037,9 @@ fn doMove(
946
1037
  if (move.effect == .Twineedle) {
947
1038
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
948
1039
  } else if (move.effect == .Disable) {
949
- 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;
950
1043
  } else {
951
1044
  try moveEffect(battle, player, move, log);
952
1045
  }
@@ -964,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
964
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
965
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
966
1059
 
967
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
968
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
969
1061
  }
970
1062
 
971
1063
  fn calcDamage(
@@ -1069,49 +1161,37 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1069
1161
 
1070
1162
  fn randomizeDamage(battle: anytype) void {
1071
1163
  if (battle.last_damage <= 1) return;
1072
-
1073
- const random = if (showdown)
1074
- battle.rng.range(u8, 217, 256)
1075
- else loop: {
1076
- while (true) {
1077
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1078
- if (r >= 217) break :loop r;
1079
- }
1080
- };
1081
-
1164
+ const random = Rolls.damage(battle);
1082
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1083
1166
  }
1084
1167
 
1085
1168
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1086
1169
  const side = battle.side(player);
1170
+ const foe = battle.foe(player);
1087
1171
 
1088
1172
  if (!try checkHit(battle, player, move, log)) return null;
1089
1173
 
1090
1174
  battle.last_damage = switch (side.last_selected_move) {
1091
- .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1175
+ .SuperFang => @max(foe.stored().hp / 2, 1),
1092
1176
  .SeismicToss, .NightShade => side.stored().level,
1093
1177
  .SonicBoom => 20,
1094
1178
  .DragonRage => 40,
1095
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1096
1180
  .Psywave => power: {
1097
1181
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1098
- if (showdown) {
1099
- break :power battle.rng.range(u8, 0, max);
1100
- } else {
1101
- // GLITCH: Psywave infinite glitch loop
1102
- if (max <= 1) return Result.Error;
1103
- while (true) {
1104
- const r = battle.rng.next();
1105
- if (r < max) break :power r;
1106
- }
1107
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1108
1185
  },
1109
1186
  else => unreachable,
1110
1187
  };
1111
1188
 
1112
1189
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1113
1190
 
1191
+ const sub = showdown and foe.active.volatiles.Substitute;
1114
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
+
1115
1195
  return null;
1116
1196
  }
1117
1197
 
@@ -1123,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1123
1203
  return null;
1124
1204
  }
1125
1205
 
1126
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1127
- const foe_last_used_move =
1128
- 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
1129
1207
  const foe_last_selected_move =
1130
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1131
- .Counter
1209
+ .Splash
1132
1210
  else
1133
1211
  foe.last_selected_move);
1134
1212
 
1135
- const used = foe_last_used_move.bp > 0 and
1136
- foe.last_used_move != .Counter and
1137
- (foe_last_used_move.type == .Normal or
1138
- 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;
1139
1217
 
1140
1218
  const selected = foe_last_selected_move.bp > 0 and
1141
1219
  foe.last_selected_move != .Counter and
@@ -1154,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1154
1232
  return null;
1155
1233
  }
1156
1234
 
1157
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1158
1236
 
1159
1237
  // Pokémon Showdown calls checkHit before Counter
1160
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1161
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1162
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);
1163
1243
  return null;
1164
1244
  }
1165
1245
 
@@ -1203,6 +1283,7 @@ fn mirrorMove(
1203
1283
  battle: anytype,
1204
1284
  player: Player,
1205
1285
  mslot: u4,
1286
+ rewrap: bool,
1206
1287
  auto: bool,
1207
1288
  residual: *bool,
1208
1289
  log: anytype,
@@ -1215,41 +1296,30 @@ fn mirrorMove(
1215
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1216
1297
  try log.fail(battle.active(player), .None);
1217
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1218
1301
  }
1219
1302
 
1220
- incrementPP(side, mslot);
1221
-
1222
1303
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1223
- return doMove(battle, player, mslot, auto, residual, log);
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1224
1305
  }
1225
1306
 
1226
1307
  fn metronome(
1227
1308
  battle: anytype,
1228
1309
  player: Player,
1229
1310
  mslot: u4,
1311
+ rewrap: bool,
1230
1312
  auto: bool,
1231
1313
  residual: *bool,
1232
1314
  log: anytype,
1233
1315
  ) !?Result {
1234
1316
  var side = battle.side(player);
1235
1317
 
1236
- side.last_selected_move = if (showdown) blk: {
1237
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1238
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1239
- break :blk @intToEnum(Move, r + mod);
1240
- } else loop: {
1241
- while (true) {
1242
- const r = battle.rng.next();
1243
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1244
- if (r >= @enumToInt(Move.Struggle)) continue;
1245
- break :loop @intToEnum(Move, r);
1246
- }
1247
- };
1248
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1249
1319
  incrementPP(side, mslot);
1250
1320
 
1251
1321
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1252
- return doMove(battle, player, mslot, auto, residual, log);
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1253
1323
  }
1254
1324
 
1255
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1303,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1303
1373
  }
1304
1374
 
1305
1375
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1306
- var state = &side.active.volatiles.state;
1307
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1308
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1309
- 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));
1310
1382
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1311
1383
  accuracy = accuracy * boost[0] / boost[1];
1312
1384
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1313
1385
  accuracy = accuracy * boost[0] / boost[1];
1314
1386
  accuracy = @min(255, @max(1, accuracy));
1315
1387
 
1316
- 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;
1317
1391
 
1318
1392
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1319
- break :miss if (showdown)
1320
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1321
- else
1322
- battle.rng.next() >= accuracy;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1323
1394
  };
1324
1395
 
1325
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1434,6 +1505,15 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1434
1505
  }
1435
1506
 
1436
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
+
1437
1517
  var damage = @max(stored.stats.hp / 16, 1);
1438
1518
 
1439
1519
  // GLITCH: Leech Seed + Toxic glitch
@@ -1445,10 +1525,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1445
1525
  const amount = @min(damage, stored.hp);
1446
1526
  stored.hp -= amount;
1447
1527
 
1448
- var foe = battle.foe(player);
1449
- var foe_stored = foe.stored();
1450
- const foe_ident = battle.active(player.foe());
1451
-
1452
1528
  // As above, Pokémon Showdown uses damageOf but its not relevant
1453
1529
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1454
1530
 
@@ -1464,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1464
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1465
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1466
1542
 
1543
+ battle.turn += 1;
1544
+
1467
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1468
1546
  try log.tie();
1469
1547
  return Result.Tie;
1470
1548
  }
1471
1549
 
1472
- battle.turn += 1;
1473
-
1474
1550
  if (showdown and battle.turn >= 1000) {
1475
1551
  try log.tie();
1476
1552
  return Result.Tie;
@@ -1537,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
1537
1613
  return false;
1538
1614
  }
1539
1615
 
1616
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1617
+ if (side.active.volatiles.disabled_move != 0) {
1618
+ // A Pokémon that transforms after being disabled may end up with less move slots
1619
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1620
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1621
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1622
+ m
1623
+ else
1624
+ side.last_selected_move;
1625
+ if (m != .None and m == last) {
1626
+ side.active.volatiles.Charging = false;
1627
+ try log.disabled(ident, last);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1635
+ const side = battle.side(who);
1636
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1637
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1638
+ }
1639
+ }
1640
+
1641
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1642
+ var volatiles = &active.volatiles;
1643
+ assert(volatiles.Thrashing);
1644
+ if (volatiles.attacks > 0) return false;
1645
+
1646
+ volatiles.Thrashing = false;
1647
+ volatiles.Confusion = true;
1648
+ volatiles.confusion = Rolls.confusionDuration(battle);
1649
+ return true;
1650
+ }
1651
+
1540
1652
  inline fn onBegin(
1541
1653
  battle: anytype,
1542
1654
  player: Player,
@@ -1627,10 +1739,7 @@ pub const Effects = struct {
1627
1739
  side.active.volatiles.Bide = true;
1628
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1629
1741
  side.active.volatiles.state = 0;
1630
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1631
- battle.rng.range(u4, 2, 4)
1632
- else
1633
- (battle.rng.next() & 1) + 2);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1634
1743
 
1635
1744
  try log.start(battle.active(player), .Bide);
1636
1745
  }
@@ -1652,14 +1761,8 @@ pub const Effects = struct {
1652
1761
  return;
1653
1762
  }
1654
1763
 
1655
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1656
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1657
- else
1658
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1659
- Gen12.percent(10)
1660
- else
1661
- Gen12.percent(30));
1662
- if (!chance) return;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1663
1766
 
1664
1767
  foe_stored.status = Status.init(.BRN);
1665
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1683,11 +1786,7 @@ pub const Effects = struct {
1683
1786
  const sub = foe.active.volatiles.Substitute;
1684
1787
 
1685
1788
  if (move.effect == .ConfusionChance) {
1686
- const chance = if (showdown)
1687
- battle.rng.chance(u8, 25, 256)
1688
- else
1689
- battle.rng.next() < Gen12.percent(10);
1690
- if (!chance) return;
1789
+ if (!Rolls.confusionChance(battle)) return;
1691
1790
  } else {
1692
1791
  if (showdown) {
1693
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1706,10 +1805,7 @@ pub const Effects = struct {
1706
1805
 
1707
1806
  if (foe.active.volatiles.Confusion) return;
1708
1807
  foe.active.volatiles.Confusion = true;
1709
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1710
- battle.rng.range(u8, 2, 6)
1711
- else
1712
- (battle.rng.next() & 3) + 2);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1713
1809
 
1714
1810
  try log.start(battle.active(player.foe()), .Confusion);
1715
1811
  }
@@ -1759,11 +1855,8 @@ pub const Effects = struct {
1759
1855
  return Result.Error;
1760
1856
  }
1761
1857
 
1762
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1763
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1764
- battle.rng.range(u8, 1, 9)
1765
- else
1766
- (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);
1767
1860
 
1768
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1769
1862
  return null;
@@ -1775,7 +1868,7 @@ pub const Effects = struct {
1775
1868
 
1776
1869
  if (battle.last_damage == 0) {
1777
1870
  assert(showdown);
1778
- return;
1871
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1779
1872
  }
1780
1873
 
1781
1874
  const drain = @max(battle.last_damage / 2, 1);
@@ -1802,15 +1895,7 @@ pub const Effects = struct {
1802
1895
  var volatiles = &battle.foe(player).active.volatiles;
1803
1896
 
1804
1897
  if (volatiles.Substitute) return;
1805
-
1806
- const chance = if (showdown)
1807
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1808
- else
1809
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1810
- Gen12.percent(10)
1811
- else
1812
- Gen12.percent(30));
1813
- if (!chance) return;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1814
1899
 
1815
1900
  volatiles.Flinch = true;
1816
1901
  volatiles.Recharging = false;
@@ -1836,12 +1921,8 @@ pub const Effects = struct {
1836
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1837
1922
  }
1838
1923
 
1839
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1840
- battle.rng.chance(u8, 26, 256)
1841
- else
1842
- battle.rng.next() < 1 + Gen12.percent(10);
1843
- if (!chance) return;
1844
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1845
1926
  // Freeze Clause Mod
1846
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1847
1928
 
@@ -1978,15 +2059,15 @@ pub const Effects = struct {
1978
2059
  }
1979
2060
  break :has_mimic false;
1980
2061
  };
1981
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1982
- // trigger |-miss| instead of |-fail|
1983
- 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;
1984
2065
  return try log.fail(battle.active(player.foe()), .None);
1985
2066
  }
1986
2067
  }
1987
2068
  if (!try checkHit(battle, player, move, log)) return;
1988
2069
 
1989
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1990
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
1991
2072
 
1992
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2007,7 +2088,8 @@ pub const Effects = struct {
2007
2088
  assert(!side.active.volatiles.MultiHit);
2008
2089
  side.active.volatiles.MultiHit = true;
2009
2090
 
2010
- 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;
2011
2093
  }
2012
2094
 
2013
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2051,14 +2133,8 @@ pub const Effects = struct {
2051
2133
  }
2052
2134
 
2053
2135
  // Body Slam can't paralyze a Normal type Pokémon
2054
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2055
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2056
- else
2057
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2058
- Gen12.percent(10)
2059
- else
2060
- Gen12.percent(30));
2061
- if (!chance) return;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2062
2138
 
2063
2139
  foe_stored.status = Status.init(.PAR);
2064
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2101,16 +2177,7 @@ pub const Effects = struct {
2101
2177
 
2102
2178
  if (move.effect == .Poison) {
2103
2179
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2104
- } else {
2105
- const chance = if (showdown)
2106
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2107
- else
2108
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2109
- Gen12.percent(20)
2110
- else
2111
- Gen12.percent(40));
2112
- if (!chance) return;
2113
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2114
2181
 
2115
2182
  foe_stored.status = Status.init(.PSN);
2116
2183
  if (toxic) {
@@ -2124,10 +2191,8 @@ pub const Effects = struct {
2124
2191
 
2125
2192
  fn rage(battle: anytype, player: Player) !void {
2126
2193
  var volatiles = &battle.side(player).active.volatiles;
2127
- if (volatiles.Rage) return;
2128
- volatiles.Rage = true;
2129
2194
  assert(!volatiles.Bide);
2130
- volatiles.state = 0;
2195
+ volatiles.Rage = true;
2131
2196
  }
2132
2197
 
2133
2198
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2179,16 +2244,7 @@ pub const Effects = struct {
2179
2244
  }
2180
2245
  foe.active.volatiles.Recharging = false;
2181
2246
 
2182
- const duration = @intCast(u3, if (showdown)
2183
- battle.rng.range(u8, 1, 8)
2184
- else loop: {
2185
- while (true) {
2186
- const r = battle.rng.next() & 7;
2187
- if (r != 0) break :loop r;
2188
- }
2189
- });
2190
-
2191
- foe_stored.status = Status.slp(duration);
2247
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2192
2248
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2193
2249
  }
2194
2250
 
@@ -2230,8 +2286,9 @@ pub const Effects = struct {
2230
2286
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2231
2287
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2232
2288
 
2233
- // 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
2234
2290
  _ = try checkHit(battle, player, move, log);
2291
+ battle.last_damage = 0;
2235
2292
  }
2236
2293
 
2237
2294
  fn thrashing(battle: anytype, player: Player) void {
@@ -2240,11 +2297,7 @@ pub const Effects = struct {
2240
2297
  assert(!volatiles.Bide);
2241
2298
 
2242
2299
  volatiles.Thrashing = true;
2243
- volatiles.state = 0;
2244
- volatiles.attacks = @intCast(u3, if (showdown)
2245
- battle.rng.range(u8, 2, 4)
2246
- else
2247
- (battle.rng.next() & 1) + 2);
2300
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2248
2301
  }
2249
2302
 
2250
2303
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2276,7 +2329,7 @@ pub const Effects = struct {
2276
2329
  try log.transform(battle.active(player), foe_ident);
2277
2330
  }
2278
2331
 
2279
- fn binding(battle: anytype, player: Player) void {
2332
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2280
2333
  var side = battle.side(player);
2281
2334
  var foe = battle.foe(player);
2282
2335
 
@@ -2284,9 +2337,11 @@ pub const Effects = struct {
2284
2337
  side.active.volatiles.Binding = true;
2285
2338
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2286
2339
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2287
- 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);
2288
2343
 
2289
- side.active.volatiles.attacks = distribution(battle) - 1;
2344
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2290
2345
  }
2291
2346
 
2292
2347
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2313,9 +2368,11 @@ pub const Effects = struct {
2313
2368
  return try log.fail(ident, .None);
2314
2369
  }
2315
2370
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2316
- const stat = unmodifiedStats(battle, player).atk;
2371
+ const stat = unmodifiedStats(battle, side).atk;
2317
2372
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2318
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;
2319
2376
  },
2320
2377
  .DefenseUp1, .DefenseUp2 => {
2321
2378
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2331,7 +2388,7 @@ pub const Effects = struct {
2331
2388
  return try log.fail(ident, .None);
2332
2389
  }
2333
2390
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2334
- const stat = unmodifiedStats(battle, player).def;
2391
+ const stat = unmodifiedStats(battle, side).def;
2335
2392
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2336
2393
  try log.boost(ident, .Defense, n);
2337
2394
  },
@@ -2348,7 +2405,7 @@ pub const Effects = struct {
2348
2405
  return try log.fail(ident, .None);
2349
2406
  }
2350
2407
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2351
- const stat = unmodifiedStats(battle, player).spe;
2408
+ const stat = unmodifiedStats(battle, side).spe;
2352
2409
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2353
2410
  try log.boost(ident, .Speed, 2);
2354
2411
  },
@@ -2368,7 +2425,7 @@ pub const Effects = struct {
2368
2425
  return try log.fail(ident, .None);
2369
2426
  }
2370
2427
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2371
- const stat = unmodifiedStats(battle, player).spc;
2428
+ const stat = unmodifiedStats(battle, side).spc;
2372
2429
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2430
  try log.boost(ident, .SpecialAttack, n);
2374
2431
  try log.boost(ident, .SpecialDefense, n);
@@ -2395,11 +2452,7 @@ pub const Effects = struct {
2395
2452
  }
2396
2453
 
2397
2454
  if (move.effect.isStatDownChance()) {
2398
- const chance = if (showdown)
2399
- battle.rng.chance(u8, 85, 256)
2400
- else
2401
- battle.rng.next() < Gen12.percent(33) + 1;
2402
- if (!chance or foe.active.volatiles.Invulnerable) return;
2455
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2403
2456
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2404
2457
  return; // checkHit already checks for Invulnerable
2405
2458
  }
@@ -2421,7 +2474,7 @@ pub const Effects = struct {
2421
2474
  return try log.fail(foe_ident, .None);
2422
2475
  }
2423
2476
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2424
- const stat = unmodifiedStats(battle, player.foe()).atk;
2477
+ const stat = unmodifiedStats(battle, foe).atk;
2425
2478
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2426
2479
  try log.boost(foe_ident, .Attack, -1);
2427
2480
  },
@@ -2439,7 +2492,7 @@ pub const Effects = struct {
2439
2492
  return try log.fail(foe_ident, .None);
2440
2493
  }
2441
2494
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2442
- const stat = unmodifiedStats(battle, player.foe()).def;
2495
+ const stat = unmodifiedStats(battle, foe).def;
2443
2496
  stats.def = @max(1, stat * mod[0] / mod[1]);
2444
2497
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2445
2498
  },
@@ -2456,7 +2509,7 @@ pub const Effects = struct {
2456
2509
  return try log.fail(foe_ident, .None);
2457
2510
  }
2458
2511
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2459
- const stat = unmodifiedStats(battle, player.foe()).spe;
2512
+ const stat = unmodifiedStats(battle, foe).spe;
2460
2513
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2461
2514
  try log.boost(foe_ident, .Speed, -1);
2462
2515
  assert(boosts.spe >= -6);
@@ -2476,7 +2529,7 @@ pub const Effects = struct {
2476
2529
  return try log.fail(foe_ident, .None);
2477
2530
  }
2478
2531
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2479
- const stat = unmodifiedStats(battle, player.foe()).spc;
2532
+ const stat = unmodifiedStats(battle, foe).spc;
2480
2533
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2481
2534
  try log.boost(foe_ident, .SpecialAttack, -1);
2482
2535
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2495,8 +2548,7 @@ pub const Effects = struct {
2495
2548
  }
2496
2549
  };
2497
2550
 
2498
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2499
- const side = battle.side(who);
2551
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2500
2552
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2501
2553
  const id = ID.from(side.active.volatiles.transform);
2502
2554
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2510,7 +2562,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2510
2562
  }
2511
2563
  }
2512
2564
 
2513
- fn isForced(active: ActivePokemon) bool {
2565
+ inline fn isForced(active: ActivePokemon) bool {
2514
2566
  return active.volatiles.Recharging or active.volatiles.Rage or
2515
2567
  active.volatiles.Thrashing or active.volatiles.Charging;
2516
2568
  }
@@ -2562,68 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2562
2614
  }
2563
2615
  }
2564
2616
 
2565
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2566
- if (side.active.volatiles.disabled_move != 0) {
2567
- // A Pokémon that transforms after being disabled may end up with less move slots
2568
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2569
- if (m != .None and m == side.last_selected_move) {
2570
- side.active.volatiles.Charging = false;
2571
- try log.disabled(ident, side.last_selected_move);
2572
- 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;
2573
2633
  }
2574
2634
  }
2575
- return false;
2576
- }
2577
2635
 
2578
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2579
- var volatiles = &active.volatiles;
2580
- assert(volatiles.Thrashing);
2581
- 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
+ }
2582
2640
 
2583
- volatiles.Thrashing = false;
2584
- volatiles.Confusion = true;
2585
- volatiles.confusion = @intCast(u3, if (showdown)
2586
- battle.rng.range(u8, 2, 6)
2587
- else
2588
- (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
+ }
2589
2645
 
2590
- return true;
2591
- }
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
+ }
2592
2650
 
2593
- 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
+ }
2594
2655
 
2595
- fn distribution(battle: anytype) u3 {
2596
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2597
- const r = (battle.rng.next() & 3);
2598
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2599
- }
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
+ }
2600
2660
 
2601
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2602
- if (showdown) {
2603
- if (check_pp == 0) {
2604
- var i: usize = moves.len;
2605
- while (i > 0) {
2606
- i -= 1;
2607
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2608
- }
2609
- } else {
2610
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2611
- var i: usize = 0;
2612
- while (i < moves.len and r > 0) : (i += 1) {
2613
- if (moves[i].pp > 0) {
2614
- r -= 1;
2615
- 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
+ }
2616
2702
  }
2703
+ return @intCast(u4, i + 1);
2617
2704
  }
2618
- 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;
2619
2710
  }
2620
2711
  }
2621
2712
 
2622
- while (true) {
2623
- const r = @intCast(u4, rand.next() & 3);
2624
- 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);
2625
2716
  }
2626
- }
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
+ };
2627
2751
 
2628
2752
  test "RNG agreement" {
2629
2753
  if (!showdown) return;
@@ -2723,9 +2847,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2723
2847
  while (slot <= 4) : (slot += 1) {
2724
2848
  const m = active.moves[slot - 1];
2725
2849
  if (m.id == .None) break;
2726
- if (m.id == side.last_selected_move) {
2727
- // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2728
- 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;
2729
2855
  out[n] = .{ .type = .Move, .data = s };
2730
2856
  n += 1;
2731
2857
  return n;