@pkmn/engine 0.1.0-dev.24867658 → 0.1.0-dev.2ec67dba

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,34 +56,30 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
-
61
59
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
60
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
61
  if (battle.turn == 0) return start(battle, log);
64
62
 
65
- var s1 = false;
66
- var s2 = false;
67
63
  var f1: ?Move = null;
68
64
  var f2: ?Move = null;
69
65
 
70
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
-
73
- var p1 = battle.side(.P1);
74
- var p2 = battle.side(.P2);
75
-
76
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
66
+ if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
+ if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
78
68
 
79
69
  if (turnOrder(battle, c1, c2) == .P1) {
80
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
70
+ if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
81
71
  } else {
82
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
72
+ if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
83
73
  }
84
74
 
85
- if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
- if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
75
+ var p1 = battle.side(.P1);
76
+ if (p1.active.volatiles.attacks == 0) {
77
+ p1.active.volatiles.Binding = false;
78
+ }
79
+ var p2 = battle.side(.P2);
80
+ if (p2.active.volatiles.attacks == 0) {
81
+ p2.active.volatiles.Binding = false;
82
+ }
87
83
 
88
84
  return endTurn(battle, log);
89
85
  }
@@ -116,7 +112,6 @@ fn selectMove(
116
112
  player: Player,
117
113
  choice: Choice,
118
114
  foe_choice: Choice,
119
- skip_turn: *bool,
120
115
  from: *?Move,
121
116
  ) ?Result {
122
117
  if (choice.type == .Pass) return null;
@@ -129,26 +124,17 @@ fn selectMove(
129
124
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
130
125
 
131
126
  // pre-battle menu
132
- if (volatiles.Recharging) {
133
- if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
- return null;
135
- }
127
+ if (volatiles.Recharging) return null;
136
128
  if (volatiles.Rage) {
137
129
  from.* = side.last_used_move;
138
- if (showdown) {
139
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
- saveMove(battle, player, null);
141
- }
130
+ if (showdown) saveMove(battle, player, null);
142
131
  return null;
143
132
  }
144
133
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
145
134
  if (!showdown) volatiles.Flinch = false;
146
135
  if (volatiles.Thrashing or volatiles.Charging) {
147
136
  from.* = side.last_selected_move;
148
- if (showdown) {
149
- if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
- saveMove(battle, player, null);
151
- }
137
+ if (showdown) saveMove(battle, player, null);
152
138
  return null;
153
139
  }
154
140
 
@@ -158,10 +144,7 @@ fn selectMove(
158
144
  // pre-move select
159
145
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
160
146
  assert(showdown or choice.data == 0);
161
- if (showdown) {
162
- if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
- saveMove(battle, player, choice);
164
- }
147
+ if (showdown) saveMove(battle, player, choice);
165
148
  return null;
166
149
  }
167
150
  if (volatiles.Binding) {
@@ -180,9 +163,9 @@ fn selectMove(
180
163
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
181
164
  if (foe_choice.type == .Switch) {
182
165
  const slot = if (player == .P1)
183
- battle.last_moves.p1_index
166
+ battle.last_selected_indexes.p1
184
167
  else
185
- battle.last_moves.p2_index;
168
+ battle.last_selected_indexes.p2;
186
169
  const last = side.active.move(@intCast(u4, slot));
187
170
  if (last.id == .Metronome) side.last_selected_move = last.id;
188
171
  if (last.id == .MirrorMove) return Result.Error;
@@ -192,7 +175,6 @@ fn selectMove(
192
175
  }
193
176
 
194
177
  if (battle.foe(player).active.volatiles.Binding) {
195
- skip_turn.* = true;
196
178
  if (showdown) {
197
179
  saveMove(battle, player, choice);
198
180
  } else {
@@ -203,7 +185,6 @@ fn selectMove(
203
185
  }
204
186
 
205
187
  // move select
206
- volatiles.state = 0;
207
188
  if (choice.data == 0) {
208
189
  const struggle = ok: {
209
190
  for (side.active.moves, 0..) |move, i| {
@@ -237,9 +218,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
237
218
 
238
219
  side.last_selected_move = move.id;
239
220
  if (player == .P1) {
240
- battle.last_moves.p1_index = @intCast(u3, c.data);
221
+ battle.last_selected_indexes.p1 = @intCast(u4, c.data);
241
222
  } else {
242
- battle.last_moves.p2_index = @intCast(u3, c.data);
223
+ battle.last_selected_indexes.p2 = @intCast(u4, c.data);
243
224
  }
244
225
  }
245
226
  }
@@ -259,9 +240,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
259
240
  side.order[slot - 1] = out;
260
241
 
261
242
  if (player == .P1) {
262
- battle.last_moves.p1_index = 1;
243
+ battle.last_selected_indexes.p1 = 1;
263
244
  } else {
264
- battle.last_moves.p2_index = 1;
245
+ battle.last_selected_indexes.p2 = 1;
265
246
  }
266
247
 
267
248
  side.last_used_move = .None;
@@ -318,11 +299,13 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
318
299
  const spe2 = battle.side(.P2).active.stats.spe;
319
300
  if (spe1 == spe2) {
320
301
  // Pokémon Showdown's beforeTurnCallback shenanigans
321
- if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
- battle.rng.advance(1);
323
- }
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;
324
308
 
325
- const p1 = Rolls.speedTie(battle);
326
309
  if (!showdown) return if (p1) .P1 else .P2;
327
310
 
328
311
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -332,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
332
315
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
333
316
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
334
317
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
335
- // that out and enforce host ordering of events, so P1 just goes first regardless of who
318
+ // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
336
319
  // won the original coin flip
337
320
  if (t1 and t2) return .P1;
338
321
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -345,13 +328,9 @@ fn doTurn(
345
328
  battle: anytype,
346
329
  player: Player,
347
330
  player_choice: Choice,
348
- player_rewrap: bool,
349
- player_skip: bool,
350
331
  player_from: ?Move,
351
332
  foe_player: Player,
352
333
  foe_choice: Choice,
353
- foe_rewrap: bool,
354
- foe_skip: bool,
355
334
  foe_from: ?Move,
356
335
  log: anytype,
357
336
  ) !?Result {
@@ -359,16 +338,7 @@ fn doTurn(
359
338
 
360
339
  var residual = true;
361
340
  var replace = battle.side(player).stored().hp == 0;
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;
341
+ if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
372
342
  if (!replace) {
373
343
  if (player_choice.type != .Switch) {
374
344
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -377,18 +347,8 @@ fn doTurn(
377
347
  if (try checkFaint(battle, player, log)) |r| return r;
378
348
  } else if (foe_choice.type == .Pass) return null;
379
349
 
380
- residual = true;
381
350
  replace = battle.side(foe_player).stored().hp == 0;
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;
351
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
392
352
  if (!replace) {
393
353
  if (foe_choice.type != .Switch) {
394
354
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -411,8 +371,6 @@ fn executeMove(
411
371
  battle: anytype,
412
372
  player: Player,
413
373
  choice: Choice,
414
- rewrap: bool,
415
- skip: bool,
416
374
  from: ?Move,
417
375
  residual: *bool,
418
376
  log: anytype,
@@ -424,10 +382,9 @@ fn executeMove(
424
382
  return null;
425
383
  }
426
384
 
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
430
385
  if (side.last_selected_move == .SKIP_TURN) {
386
+ // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
+ // Binding instead gets handled in beforeMove after sleep and freeze
431
388
  assert(!showdown);
432
389
  if (battle.foe(player).active.volatiles.Binding) {
433
390
  try log.cant(battle.active(player), .Bound);
@@ -446,9 +403,9 @@ fn executeMove(
446
403
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
447
404
  assert(!showdown);
448
405
  mslot = @intCast(u4, if (player == .P1)
449
- battle.last_moves.p1_index
406
+ battle.last_selected_indexes.p1
450
407
  else
451
- battle.last_moves.p2_index);
408
+ battle.last_selected_indexes.p2);
452
409
  const stored = side.stored();
453
410
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
454
411
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -470,14 +427,14 @@ fn executeMove(
470
427
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
471
428
  assert(mslot == 1);
472
429
  mslot = @intCast(u4, if (player == .P1)
473
- battle.last_moves.p1_index
430
+ battle.last_selected_indexes.p1
474
431
  else
475
- battle.last_moves.p2_index);
432
+ battle.last_selected_indexes.p2);
476
433
  }
477
434
 
478
435
  var skip_can = false;
479
436
  var skip_pp = false;
480
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
437
+ switch (try beforeMove(battle, player, from, log)) {
481
438
  .done => return null,
482
439
  .skip_can => skip_can = true,
483
440
  .skip_pp => skip_pp = true,
@@ -489,19 +446,12 @@ fn executeMove(
489
446
  return null;
490
447
  }
491
448
 
492
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
449
+ return doMove(battle, player, mslot, auto, residual, log);
493
450
  }
494
451
 
495
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
452
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
496
453
 
497
- fn beforeMove(
498
- battle: anytype,
499
- player: Player,
500
- skip: bool,
501
- from: ?Move,
502
- residual: *bool,
503
- log: anytype,
504
- ) !BeforeMove {
454
+ fn beforeMove(battle: anytype, player: Player, from: ?Move, log: anytype) !BeforeMove {
505
455
  var side = battle.side(player);
506
456
  const foe = battle.foe(player);
507
457
  var active = &side.active;
@@ -529,15 +479,13 @@ fn beforeMove(
529
479
  return .done;
530
480
  }
531
481
 
532
- if (skip or foe.active.volatiles.Binding) {
482
+ if (foe.active.volatiles.Binding) {
533
483
  try log.cant(ident, .Bound);
534
484
  return .done;
535
485
  }
536
486
 
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
+ if (!showdown and volatiles.Flinch) {
488
+ volatiles.Flinch = false;
541
489
  try log.cant(ident, .Flinch);
542
490
  return .done;
543
491
  }
@@ -558,6 +506,13 @@ fn beforeMove(
558
506
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
559
507
  if (showdown and try disabled(side, ident, log)) return .done;
560
508
 
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
+
561
516
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
562
517
  // before the side had a selected a move - we simply need to assume this leads to a desync
563
518
  if (side.last_selected_move == .None) {
@@ -575,9 +530,14 @@ fn beforeMove(
575
530
  } else {
576
531
  try log.activate(ident, .Confusion);
577
532
 
578
- if (Rolls.confused(battle)) {
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) {
579
539
  assert(!volatiles.MultiHit);
580
- if (!volatiles.Rage) volatiles.state = 0;
540
+ volatiles.state = 0;
581
541
  volatiles.Bide = false;
582
542
  volatiles.Thrashing = false;
583
543
  volatiles.MultiHit = false;
@@ -601,12 +561,8 @@ fn beforeMove(
601
561
  player
602
562
  else
603
563
  player.foe();
604
-
605
- const uncapped = battle.last_damage;
606
564
  // Skipping adjustDamage / randomizeDamage / checkHit
607
565
  _ = try applyDamage(battle, player, target, .Confusion, log);
608
- // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
609
- if (showdown) battle.last_damage = uncapped;
610
566
 
611
567
  return .done;
612
568
  }
@@ -615,28 +571,28 @@ fn beforeMove(
615
571
 
616
572
  if (!showdown and try disabled(side, ident, log)) return .done;
617
573
 
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;
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
+ }
627
590
  }
628
591
 
629
592
  if (volatiles.Bide) {
630
593
  assert(!volatiles.Thrashing and !volatiles.Rage);
631
594
 
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
- }
595
+ volatiles.state +%= battle.last_damage;
640
596
 
641
597
  assert(volatiles.attacks > 0);
642
598
 
@@ -657,18 +613,7 @@ fn beforeMove(
657
613
  return .done;
658
614
  }
659
615
 
660
- const sub = showdown and foe.active.volatiles.Substitute;
661
616
  _ = 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
-
672
617
  return .done;
673
618
  }
674
619
 
@@ -680,13 +625,8 @@ fn beforeMove(
680
625
  try log.start(battle.active(player), .ConfusionSilent);
681
626
  }
682
627
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
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
+ if (showdown and handleThrashing(battle, active)) {
629
+ try log.start(battle.active(player), .ConfusionSilent);
690
630
  }
691
631
  return .skip_can;
692
632
  }
@@ -701,11 +641,7 @@ fn beforeMove(
701
641
  side.last_selected_move,
702
642
  );
703
643
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
644
  _ = 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
- }
709
645
  }
710
646
  return .done;
711
647
  }
@@ -737,7 +673,6 @@ fn canMove(
737
673
  side.active.volatiles.Invulnerable = false;
738
674
  } else if (move.effect == .Charge) {
739
675
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
676
  try Effects.charge(battle, player, log);
742
677
  return false;
743
678
  }
@@ -754,7 +689,6 @@ fn canMove(
754
689
  // regrettable, but this information is more "nice to have" than required
755
690
  const f = if (from != null and from.? == .None) null else from;
756
691
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
692
 
759
693
  if (move.effect.onBegin()) {
760
694
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +699,12 @@ fn canMove(
765
699
  Effects.thrashing(battle, player);
766
700
  } else if (!showdown and move.effect == .Binding) {
767
701
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
702
+ Effects.binding(battle, player);
769
703
  }
770
704
 
771
705
  return true;
772
706
  }
773
707
 
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
-
789
708
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
709
  if (side.last_selected_move == .Struggle) return;
791
710
 
@@ -821,7 +740,6 @@ fn doMove(
821
740
  battle: anytype,
822
741
  player: Player,
823
742
  mslot: u4,
824
- rewrap: bool,
825
743
  auto: bool,
826
744
  residual: *bool,
827
745
  log: anytype,
@@ -852,21 +770,16 @@ fn doMove(
852
770
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
853
771
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
854
772
  if (immune and move.effect != .Binding) 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
- }
773
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
859
774
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
860
775
  };
861
776
  assert(!immune or miss or (showdown and move.effect == .Binding));
862
777
 
863
- var late = showdown and move.effect != .Explode;
864
778
  const skip = status or immune;
865
779
  if ((!showdown or (!skip or counter)) and !miss) blk: {
866
780
  if (showdown and move.effect.isMulti()) {
867
781
  Effects.multiHit(battle, player, move);
868
782
  hits = side.active.volatiles.attacks;
869
- late = false;
870
783
  }
871
784
 
872
785
  // Cartridge rolls for crit even for moves that can't crit (Counter/Metronome/status/OHKO)
@@ -900,7 +813,7 @@ fn doMove(
900
813
  }
901
814
 
902
815
  // Due to control flow shenanigans we need to clear last_damage for Pokémon Showdown
903
- if (showdown and skip) battle.last_damage = 0;
816
+ if (showdown and skip and move.effect != .Bide) battle.last_damage = 0;
904
817
 
905
818
  miss = if (showdown or skip)
906
819
  miss
@@ -913,9 +826,9 @@ fn doMove(
913
826
 
914
827
  if (!showdown or !miss) {
915
828
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
829
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
830
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
831
+ return metronome(battle, player, mslot, auto, residual, log);
919
832
  } else if (move.effect.onEnd()) {
920
833
  try onEnd(battle, player, move, log);
921
834
  return null;
@@ -930,7 +843,7 @@ fn doMove(
930
843
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
844
  if (ohko) {
932
845
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
846
+ } else if (immune and !invulnerable) {
934
847
  try log.immune(foe_ident, .None);
935
848
  } else if (mist) {
936
849
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -947,9 +860,15 @@ fn doMove(
947
860
  if (showdown and side.stored().hp == 0) residual.* = false;
948
861
  } else if (move.effect == .Explode) {
949
862
  try Effects.explode(battle, player);
950
- try buildRage(battle, player.foe(), log);
951
- } else if (showdown and move.effect == .Disable) {
952
- try buildRage(battle, player.foe(), log);
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
+ }
953
872
  }
954
873
  return null;
955
874
  }
@@ -962,9 +881,6 @@ fn doMove(
962
881
  hits = side.active.volatiles.attacks;
963
882
  }
964
883
 
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
-
968
884
  var nullified = false;
969
885
  var hit: u4 = 0;
970
886
  while (hit < hits) {
@@ -980,7 +896,9 @@ fn doMove(
980
896
  if (hit == 0 and ohko) try log.ohko();
981
897
  hit += 1;
982
898
  if (foe.stored().hp == 0) break;
983
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
899
+ if (foe.active.volatiles.Rage and foe.active.boosts.atk < 6) {
900
+ try Effects.boost(battle, player.foe(), Move.get(.Rage), log);
901
+ }
984
902
  // If the substitute breaks during a multi-hit attack, the attack ends
985
903
  if (nullified) break;
986
904
  }
@@ -996,17 +914,11 @@ fn doMove(
996
914
  } else if (showdown) {
997
915
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
916
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
917
+ Effects.binding(battle, player);
1000
918
  if (immune) {
1001
919
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
920
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
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
- }
921
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
922
  return null;
1011
923
  }
1012
924
  }
@@ -1023,9 +935,6 @@ fn doMove(
1023
935
 
1024
936
  if (foe.stored().hp == 0) return null;
1025
937
 
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
-
1029
938
  if (!move.effect.isSpecial()) {
1030
939
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
1031
940
  // second time here (after being executed in the "always happens" block above) but that
@@ -1037,9 +946,7 @@ fn doMove(
1037
946
  if (move.effect == .Twineedle) {
1038
947
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1039
948
  } else if (move.effect == .Disable) {
1040
- const result = try Effects.disable(battle, player, move, log);
1041
- if (showdown) try buildRage(battle, player.foe(), log);
1042
- return result;
949
+ return Effects.disable(battle, player, move, log);
1043
950
  } else {
1044
951
  try moveEffect(battle, player, move, log);
1045
952
  }
@@ -1057,7 +964,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1057
964
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1058
965
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1059
966
 
1060
- return Rolls.criticalHit(battle, chance);
967
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
968
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1061
969
  }
1062
970
 
1063
971
  fn calcDamage(
@@ -1161,37 +1069,49 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1161
1069
 
1162
1070
  fn randomizeDamage(battle: anytype) void {
1163
1071
  if (battle.last_damage <= 1) return;
1164
- const random = Rolls.damage(battle);
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
+
1165
1082
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1166
1083
  }
1167
1084
 
1168
1085
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1169
1086
  const side = battle.side(player);
1170
- const foe = battle.foe(player);
1171
1087
 
1172
1088
  if (!try checkHit(battle, player, move, log)) return null;
1173
1089
 
1174
1090
  battle.last_damage = switch (side.last_selected_move) {
1175
- .SuperFang => @max(foe.stored().hp / 2, 1),
1091
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1176
1092
  .SeismicToss, .NightShade => side.stored().level,
1177
1093
  .SonicBoom => 20,
1178
1094
  .DragonRage => 40,
1179
1095
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1180
1096
  .Psywave => power: {
1181
1097
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1182
- // GLITCH: Psywave infinite glitch loop
1183
- if (!showdown and max <= 1) return Result.Error;
1184
- break :power Rolls.psywave(battle, max);
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
+ }
1185
1108
  },
1186
1109
  else => unreachable,
1187
1110
  };
1188
1111
 
1189
1112
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1190
1113
 
1191
- const sub = showdown and foe.active.volatiles.Substitute;
1192
1114
  _ = 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
-
1195
1115
  return null;
1196
1116
  }
1197
1117
 
@@ -1203,17 +1123,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1123
  return null;
1204
1124
  }
1205
1125
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
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);
1207
1129
  const foe_last_selected_move =
1208
1130
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1131
+ .Counter
1210
1132
  else
1211
1133
  foe.last_selected_move);
1212
1134
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
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);
1217
1139
 
1218
1140
  const selected = foe_last_selected_move.bp > 0 and
1219
1141
  foe.last_selected_move != .Counter and
@@ -1232,14 +1154,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1154
  return null;
1233
1155
  }
1234
1156
 
1235
- battle.last_damage *|= 2;
1157
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1158
 
1237
1159
  // Pokémon Showdown calls checkHit before Counter
1238
1160
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1161
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1162
  _ = 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);
1243
1163
  return null;
1244
1164
  }
1245
1165
 
@@ -1283,7 +1203,6 @@ fn mirrorMove(
1283
1203
  battle: anytype,
1284
1204
  player: Player,
1285
1205
  mslot: u4,
1286
- rewrap: bool,
1287
1206
  auto: bool,
1288
1207
  residual: *bool,
1289
1208
  log: anytype,
@@ -1296,30 +1215,41 @@ fn mirrorMove(
1296
1215
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1216
  try log.fail(battle.active(player), .None);
1298
1217
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1218
  }
1302
1219
 
1220
+ incrementPP(side, mslot);
1221
+
1303
1222
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1223
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1224
  }
1306
1225
 
1307
1226
  fn metronome(
1308
1227
  battle: anytype,
1309
1228
  player: Player,
1310
1229
  mslot: u4,
1311
- rewrap: bool,
1312
1230
  auto: bool,
1313
1231
  residual: *bool,
1314
1232
  log: anytype,
1315
1233
  ) !?Result {
1316
1234
  var side = battle.side(player);
1317
1235
 
1318
- side.last_selected_move = Rolls.metronome(battle);
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
+
1319
1249
  incrementPP(side, mslot);
1320
1250
 
1321
1251
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1252
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1253
  }
1324
1254
 
1325
1255
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1373,24 +1303,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1373
1303
  }
1374
1304
 
1375
1305
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
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));
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));
1382
1310
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1383
1311
  accuracy = accuracy * boost[0] / boost[1];
1384
1312
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1385
1313
  accuracy = accuracy * boost[0] / boost[1];
1386
1314
  accuracy = @min(255, @max(1, accuracy));
1387
1315
 
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;
1316
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1391
1317
 
1392
1318
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1393
- break :miss !Rolls.hit(battle, accuracy);
1319
+ break :miss if (showdown)
1320
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1321
+ else
1322
+ battle.rng.next() >= accuracy;
1394
1323
  };
1395
1324
 
1396
1325
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1505,15 +1434,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1505
1434
  }
1506
1435
 
1507
1436
  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
-
1517
1437
  var damage = @max(stored.stats.hp / 16, 1);
1518
1438
 
1519
1439
  // GLITCH: Leech Seed + Toxic glitch
@@ -1525,6 +1445,10 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1525
1445
  const amount = @min(damage, stored.hp);
1526
1446
  stored.hp -= amount;
1527
1447
 
1448
+ var foe = battle.foe(player);
1449
+ var foe_stored = foe.stored();
1450
+ const foe_ident = battle.active(player.foe());
1451
+
1528
1452
  // As above, Pokémon Showdown uses damageOf but its not relevant
1529
1453
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1530
1454
 
@@ -1540,13 +1464,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1464
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1465
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1466
 
1543
- battle.turn += 1;
1544
-
1545
1467
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1468
  try log.tie();
1547
1469
  return Result.Tie;
1548
1470
  }
1549
1471
 
1472
+ battle.turn += 1;
1473
+
1550
1474
  if (showdown and battle.turn >= 1000) {
1551
1475
  try log.tie();
1552
1476
  return Result.Tie;
@@ -1613,42 +1537,6 @@ fn checkEBC(battle: anytype) bool {
1613
1537
  return false;
1614
1538
  }
1615
1539
 
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
1540
  inline fn onBegin(
1653
1541
  battle: anytype,
1654
1542
  player: Player,
@@ -1739,7 +1627,10 @@ pub const Effects = struct {
1739
1627
  side.active.volatiles.Bide = true;
1740
1628
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1741
1629
  side.active.volatiles.state = 0;
1742
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1630
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1631
+ battle.rng.range(u4, 2, 4)
1632
+ else
1633
+ (battle.rng.next() & 1) + 2);
1743
1634
 
1744
1635
  try log.start(battle.active(player), .Bide);
1745
1636
  }
@@ -1761,8 +1652,14 @@ pub const Effects = struct {
1761
1652
  return;
1762
1653
  }
1763
1654
 
1764
- if (foe.active.types.includes(move.type)) return;
1765
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
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;
1766
1663
 
1767
1664
  foe_stored.status = Status.init(.BRN);
1768
1665
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1786,7 +1683,11 @@ pub const Effects = struct {
1786
1683
  const sub = foe.active.volatiles.Substitute;
1787
1684
 
1788
1685
  if (move.effect == .ConfusionChance) {
1789
- if (!Rolls.confusionChance(battle)) return;
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;
1790
1691
  } else {
1791
1692
  if (showdown) {
1792
1693
  if (!try checkHit(battle, player, move, log)) {
@@ -1805,7 +1706,10 @@ pub const Effects = struct {
1805
1706
 
1806
1707
  if (foe.active.volatiles.Confusion) return;
1807
1708
  foe.active.volatiles.Confusion = true;
1808
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1709
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1710
+ battle.rng.range(u8, 2, 6)
1711
+ else
1712
+ (battle.rng.next() & 3) + 2);
1809
1713
 
1810
1714
  try log.start(battle.active(player.foe()), .Confusion);
1811
1715
  }
@@ -1855,8 +1759,11 @@ pub const Effects = struct {
1855
1759
  return Result.Error;
1856
1760
  }
1857
1761
 
1858
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
- volatiles.disabled_duration = Rolls.disableDuration(battle);
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);
1860
1767
 
1861
1768
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1862
1769
  return null;
@@ -1868,7 +1775,7 @@ pub const Effects = struct {
1868
1775
 
1869
1776
  if (battle.last_damage == 0) {
1870
1777
  assert(showdown);
1871
- if (!battle.foe(player).active.volatiles.Substitute) return;
1778
+ return;
1872
1779
  }
1873
1780
 
1874
1781
  const drain = @max(battle.last_damage / 2, 1);
@@ -1895,7 +1802,15 @@ pub const Effects = struct {
1895
1802
  var volatiles = &battle.foe(player).active.volatiles;
1896
1803
 
1897
1804
  if (volatiles.Substitute) return;
1898
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) 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;
1899
1814
 
1900
1815
  volatiles.Flinch = true;
1901
1816
  volatiles.Recharging = false;
@@ -1921,8 +1836,12 @@ pub const Effects = struct {
1921
1836
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1922
1837
  }
1923
1838
 
1924
- if (foe.active.types.includes(move.type)) return;
1925
- if (!Rolls.secondaryChance(battle, true)) return;
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
+
1926
1845
  // Freeze Clause Mod
1927
1846
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1928
1847
 
@@ -2059,15 +1978,15 @@ pub const Effects = struct {
2059
1978
  }
2060
1979
  break :has_mimic false;
2061
1980
  };
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;
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) {
2065
1984
  return try log.fail(battle.active(player.foe()), .None);
2066
1985
  }
2067
1986
  }
2068
1987
  if (!try checkHit(battle, player, move, log)) return;
2069
1988
 
2070
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1989
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2071
1990
  side.active.move(oslot).id = foe.active.move(rslot).id;
2072
1991
 
2073
1992
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2088,8 +2007,7 @@ pub const Effects = struct {
2088
2007
  assert(!side.active.volatiles.MultiHit);
2089
2008
  side.active.volatiles.MultiHit = true;
2090
2009
 
2091
- side.active.volatiles.attacks =
2092
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2010
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2093
2011
  }
2094
2012
 
2095
2013
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2133,8 +2051,14 @@ pub const Effects = struct {
2133
2051
  }
2134
2052
 
2135
2053
  // Body Slam can't paralyze a Normal type Pokémon
2136
- if (foe.active.types.includes(move.type)) return;
2137
- if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
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;
2138
2062
 
2139
2063
  foe_stored.status = Status.init(.PAR);
2140
2064
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2177,7 +2101,16 @@ pub const Effects = struct {
2177
2101
 
2178
2102
  if (move.effect == .Poison) {
2179
2103
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2180
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) 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
+ }
2181
2114
 
2182
2115
  foe_stored.status = Status.init(.PSN);
2183
2116
  if (toxic) {
@@ -2191,8 +2124,10 @@ pub const Effects = struct {
2191
2124
 
2192
2125
  fn rage(battle: anytype, player: Player) !void {
2193
2126
  var volatiles = &battle.side(player).active.volatiles;
2194
- assert(!volatiles.Bide);
2127
+ if (volatiles.Rage) return;
2195
2128
  volatiles.Rage = true;
2129
+ assert(!volatiles.Bide);
2130
+ volatiles.state = 0;
2196
2131
  }
2197
2132
 
2198
2133
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2244,7 +2179,16 @@ pub const Effects = struct {
2244
2179
  }
2245
2180
  foe.active.volatiles.Recharging = false;
2246
2181
 
2247
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
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);
2248
2192
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2249
2193
  }
2250
2194
 
@@ -2286,9 +2230,8 @@ pub const Effects = struct {
2286
2230
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2287
2231
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2288
2232
 
2289
- // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2233
+ // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2290
2234
  _ = try checkHit(battle, player, move, log);
2291
- battle.last_damage = 0;
2292
2235
  }
2293
2236
 
2294
2237
  fn thrashing(battle: anytype, player: Player) void {
@@ -2297,7 +2240,11 @@ pub const Effects = struct {
2297
2240
  assert(!volatiles.Bide);
2298
2241
 
2299
2242
  volatiles.Thrashing = true;
2300
- volatiles.attacks = Rolls.bideThrashDuration(battle);
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);
2301
2248
  }
2302
2249
 
2303
2250
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2329,7 +2276,7 @@ pub const Effects = struct {
2329
2276
  try log.transform(battle.active(player), foe_ident);
2330
2277
  }
2331
2278
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2279
+ fn binding(battle: anytype, player: Player) void {
2333
2280
  var side = battle.side(player);
2334
2281
  var foe = battle.foe(player);
2335
2282
 
@@ -2337,11 +2284,9 @@ pub const Effects = struct {
2337
2284
  side.active.volatiles.Binding = true;
2338
2285
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2286
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2340
- if (!showdown) {
2341
- foe.active.volatiles.Recharging = false;
2342
- } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2287
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2288
 
2344
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2289
+ side.active.volatiles.attacks = distribution(battle) - 1;
2345
2290
  }
2346
2291
 
2347
2292
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2368,11 +2313,9 @@ pub const Effects = struct {
2368
2313
  return try log.fail(ident, .None);
2369
2314
  }
2370
2315
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2371
- const stat = unmodifiedStats(battle, side).atk;
2316
+ const stat = unmodifiedStats(battle, player).atk;
2372
2317
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2318
  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;
2376
2319
  },
2377
2320
  .DefenseUp1, .DefenseUp2 => {
2378
2321
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2388,7 +2331,7 @@ pub const Effects = struct {
2388
2331
  return try log.fail(ident, .None);
2389
2332
  }
2390
2333
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2391
- const stat = unmodifiedStats(battle, side).def;
2334
+ const stat = unmodifiedStats(battle, player).def;
2392
2335
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2393
2336
  try log.boost(ident, .Defense, n);
2394
2337
  },
@@ -2405,7 +2348,7 @@ pub const Effects = struct {
2405
2348
  return try log.fail(ident, .None);
2406
2349
  }
2407
2350
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2408
- const stat = unmodifiedStats(battle, side).spe;
2351
+ const stat = unmodifiedStats(battle, player).spe;
2409
2352
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2410
2353
  try log.boost(ident, .Speed, 2);
2411
2354
  },
@@ -2425,7 +2368,7 @@ pub const Effects = struct {
2425
2368
  return try log.fail(ident, .None);
2426
2369
  }
2427
2370
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2428
- const stat = unmodifiedStats(battle, side).spc;
2371
+ const stat = unmodifiedStats(battle, player).spc;
2429
2372
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2430
2373
  try log.boost(ident, .SpecialAttack, n);
2431
2374
  try log.boost(ident, .SpecialDefense, n);
@@ -2452,7 +2395,11 @@ pub const Effects = struct {
2452
2395
  }
2453
2396
 
2454
2397
  if (move.effect.isStatDownChance()) {
2455
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
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;
2456
2403
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2457
2404
  return; // checkHit already checks for Invulnerable
2458
2405
  }
@@ -2474,7 +2421,7 @@ pub const Effects = struct {
2474
2421
  return try log.fail(foe_ident, .None);
2475
2422
  }
2476
2423
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2477
- const stat = unmodifiedStats(battle, foe).atk;
2424
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2478
2425
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2479
2426
  try log.boost(foe_ident, .Attack, -1);
2480
2427
  },
@@ -2492,7 +2439,7 @@ pub const Effects = struct {
2492
2439
  return try log.fail(foe_ident, .None);
2493
2440
  }
2494
2441
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2495
- const stat = unmodifiedStats(battle, foe).def;
2442
+ const stat = unmodifiedStats(battle, player.foe()).def;
2496
2443
  stats.def = @max(1, stat * mod[0] / mod[1]);
2497
2444
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2498
2445
  },
@@ -2509,7 +2456,7 @@ pub const Effects = struct {
2509
2456
  return try log.fail(foe_ident, .None);
2510
2457
  }
2511
2458
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2512
- const stat = unmodifiedStats(battle, foe).spe;
2459
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2513
2460
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2514
2461
  try log.boost(foe_ident, .Speed, -1);
2515
2462
  assert(boosts.spe >= -6);
@@ -2529,7 +2476,7 @@ pub const Effects = struct {
2529
2476
  return try log.fail(foe_ident, .None);
2530
2477
  }
2531
2478
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2532
- const stat = unmodifiedStats(battle, foe).spc;
2479
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2533
2480
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2534
2481
  try log.boost(foe_ident, .SpecialAttack, -1);
2535
2482
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2548,7 +2495,8 @@ pub const Effects = struct {
2548
2495
  }
2549
2496
  };
2550
2497
 
2551
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2498
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2499
+ const side = battle.side(who);
2552
2500
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2553
2501
  const id = ID.from(side.active.volatiles.transform);
2554
2502
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2562,7 +2510,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2562
2510
  }
2563
2511
  }
2564
2512
 
2565
- inline fn isForced(active: ActivePokemon) bool {
2513
+ fn isForced(active: ActivePokemon) bool {
2566
2514
  return active.volatiles.Recharging or active.volatiles.Rage or
2567
2515
  active.volatiles.Thrashing or active.volatiles.Charging;
2568
2516
  }
@@ -2614,140 +2562,68 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2614
2562
  }
2615
2563
  }
2616
2564
 
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;
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;
2633
2573
  }
2634
2574
  }
2575
+ return false;
2576
+ }
2635
2577
 
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
- }
2640
-
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
- }
2645
-
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
- }
2650
-
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
- }
2655
-
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
- }
2578
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2579
+ var volatiles = &active.volatiles;
2580
+ assert(volatiles.Thrashing);
2581
+ if (volatiles.attacks > 0) return false;
2660
2582
 
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
- }
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);
2665
2589
 
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
- }
2590
+ return true;
2591
+ }
2670
2592
 
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
- }
2593
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2678
2594
 
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
- }
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
+ }
2683
2600
 
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
- }
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;
2702
2616
  }
2703
- return @intCast(u4, i + 1);
2704
2617
  }
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;
2618
+ return @intCast(u4, i + 1);
2710
2619
  }
2711
2620
  }
2712
2621
 
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);
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;
2716
2625
  }
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
- };
2626
+ }
2751
2627
 
2752
2628
  test "RNG agreement" {
2753
2629
  if (!showdown) return;
@@ -2847,11 +2723,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2847
2723
  while (slot <= 4) : (slot += 1) {
2848
2724
  const m = active.moves[slot - 1];
2849
2725
  if (m.id == .None) break;
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;
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;
2855
2729
  out[n] = .{ .type = .Move, .data = s };
2856
2730
  n += 1;
2857
2731
  return n;