@pkmn/engine 0.1.0-dev.af1565df → 0.1.0-dev.c4dec0f7

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,17 +613,8 @@ 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
- }
617
+ try buildRage(battle, player.foe(), log);
671
618
 
672
619
  return .done;
673
620
  }
@@ -680,13 +627,8 @@ fn beforeMove(
680
627
  try log.start(battle.active(player), .ConfusionSilent);
681
628
  }
682
629
  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
- }
630
+ if (showdown and handleThrashing(battle, active)) {
631
+ try log.start(battle.active(player), .ConfusionSilent);
690
632
  }
691
633
  return .skip_can;
692
634
  }
@@ -701,11 +643,7 @@ fn beforeMove(
701
643
  side.last_selected_move,
702
644
  );
703
645
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
646
  _ = 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
647
  }
710
648
  return .done;
711
649
  }
@@ -737,7 +675,6 @@ fn canMove(
737
675
  side.active.volatiles.Invulnerable = false;
738
676
  } else if (move.effect == .Charge) {
739
677
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
678
  try Effects.charge(battle, player, log);
742
679
  return false;
743
680
  }
@@ -754,7 +691,6 @@ fn canMove(
754
691
  // regrettable, but this information is more "nice to have" than required
755
692
  const f = if (from != null and from.? == .None) null else from;
756
693
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
694
 
759
695
  if (move.effect.onBegin()) {
760
696
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +701,12 @@ fn canMove(
765
701
  Effects.thrashing(battle, player);
766
702
  } else if (!showdown and move.effect == .Binding) {
767
703
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
704
+ Effects.binding(battle, player);
769
705
  }
770
706
 
771
707
  return true;
772
708
  }
773
709
 
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
710
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
711
  if (side.last_selected_move == .Struggle) return;
791
712
 
@@ -821,7 +742,6 @@ fn doMove(
821
742
  battle: anytype,
822
743
  player: Player,
823
744
  mslot: u4,
824
- rewrap: bool,
825
745
  auto: bool,
826
746
  residual: *bool,
827
747
  log: anytype,
@@ -852,15 +772,12 @@ fn doMove(
852
772
  (@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
853
773
  @enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
854
774
  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
- }
775
+ if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
859
776
  break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
860
777
  };
861
778
  assert(!immune or miss or (showdown and move.effect == .Binding));
862
779
 
863
- var late = showdown and move.effect != .Explode;
780
+ var late = showdown;
864
781
  const skip = status or immune;
865
782
  if ((!showdown or (!skip or counter)) and !miss) blk: {
866
783
  if (showdown and move.effect.isMulti()) {
@@ -913,9 +830,9 @@ fn doMove(
913
830
 
914
831
  if (!showdown or !miss) {
915
832
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
833
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
834
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
835
+ return metronome(battle, player, mslot, auto, residual, log);
919
836
  } else if (move.effect.onEnd()) {
920
837
  try onEnd(battle, player, move, log);
921
838
  return null;
@@ -930,7 +847,7 @@ fn doMove(
930
847
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
848
  if (ohko) {
932
849
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
850
+ } else if (immune and !invulnerable) {
934
851
  try log.immune(foe_ident, .None);
935
852
  } else if (mist) {
936
853
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -962,9 +879,6 @@ fn doMove(
962
879
  hits = side.active.volatiles.attacks;
963
880
  }
964
881
 
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
882
  var nullified = false;
969
883
  var hit: u4 = 0;
970
884
  while (hit < hits) {
@@ -980,7 +894,7 @@ fn doMove(
980
894
  if (hit == 0 and ohko) try log.ohko();
981
895
  hit += 1;
982
896
  if (foe.stored().hp == 0) break;
983
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
897
+ if (!late) try buildRage(battle, player.foe(), log);
984
898
  // If the substitute breaks during a multi-hit attack, the attack ends
985
899
  if (nullified) break;
986
900
  }
@@ -996,17 +910,11 @@ fn doMove(
996
910
  } else if (showdown) {
997
911
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
912
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
913
+ Effects.binding(battle, player);
1000
914
  if (immune) {
1001
915
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
916
  // 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
- }
917
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
918
  return null;
1011
919
  }
1012
920
  }
@@ -1023,8 +931,8 @@ fn doMove(
1023
931
 
1024
932
  if (foe.stored().hp == 0) return null;
1025
933
 
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);
934
+ // Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
935
+ if (late) try buildRage(battle, player.foe(), log);
1028
936
 
1029
937
  if (!move.effect.isSpecial()) {
1030
938
  // On the cartridge Rage is not considered to be "special" and thus gets executed for a
@@ -1037,9 +945,7 @@ fn doMove(
1037
945
  if (move.effect == .Twineedle) {
1038
946
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), log);
1039
947
  } 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;
948
+ return Effects.disable(battle, player, move, log);
1043
949
  } else {
1044
950
  try moveEffect(battle, player, move, log);
1045
951
  }
@@ -1057,7 +963,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1057
963
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1058
964
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1059
965
 
1060
- return Rolls.criticalHit(battle, chance);
966
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
967
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1061
968
  }
1062
969
 
1063
970
  fn calcDamage(
@@ -1161,36 +1068,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1161
1068
 
1162
1069
  fn randomizeDamage(battle: anytype) void {
1163
1070
  if (battle.last_damage <= 1) return;
1164
- const random = Rolls.damage(battle);
1071
+
1072
+ const random = if (showdown)
1073
+ battle.rng.range(u8, 217, 256)
1074
+ else loop: {
1075
+ while (true) {
1076
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
1077
+ if (r >= 217) break :loop r;
1078
+ }
1079
+ };
1080
+
1165
1081
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1166
1082
  }
1167
1083
 
1168
1084
  fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
1169
1085
  const side = battle.side(player);
1170
- const foe = battle.foe(player);
1171
1086
 
1172
1087
  if (!try checkHit(battle, player, move, log)) return null;
1173
1088
 
1174
1089
  battle.last_damage = switch (side.last_selected_move) {
1175
- .SuperFang => @max(foe.stored().hp / 2, 1),
1090
+ .SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
1176
1091
  .SeismicToss, .NightShade => side.stored().level,
1177
1092
  .SonicBoom => 20,
1178
1093
  .DragonRage => 40,
1179
1094
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1180
1095
  .Psywave => power: {
1181
1096
  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);
1097
+ if (showdown) {
1098
+ break :power battle.rng.range(u8, 0, max);
1099
+ } else {
1100
+ // GLITCH: Psywave infinite glitch loop
1101
+ if (max <= 1) return Result.Error;
1102
+ while (true) {
1103
+ const r = battle.rng.next();
1104
+ if (r < max) break :power r;
1105
+ }
1106
+ }
1185
1107
  },
1186
1108
  else => unreachable,
1187
1109
  };
1188
1110
 
1189
1111
  if (battle.last_damage == 0) return if (showdown) null else Result.Error;
1190
1112
 
1191
- const sub = showdown and foe.active.volatiles.Substitute;
1192
1113
  _ = 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);
1114
+ try buildRage(battle, player.foe(), log);
1194
1115
 
1195
1116
  return null;
1196
1117
  }
@@ -1203,17 +1124,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1124
  return null;
1204
1125
  }
1205
1126
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1127
+ // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1128
+ const foe_last_used_move =
1129
+ Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1207
1130
  const foe_last_selected_move =
1208
1131
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1132
+ .Counter
1210
1133
  else
1211
1134
  foe.last_selected_move);
1212
1135
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
1136
+ const used = foe_last_used_move.bp > 0 and
1137
+ foe.last_used_move != .Counter and
1138
+ (foe_last_used_move.type == .Normal or
1139
+ foe_last_used_move.type == .Fighting);
1217
1140
 
1218
1141
  const selected = foe_last_selected_move.bp > 0 and
1219
1142
  foe.last_selected_move != .Counter and
@@ -1232,14 +1155,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1155
  return null;
1233
1156
  }
1234
1157
 
1235
- battle.last_damage *|= 2;
1158
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1159
 
1237
1160
  // Pokémon Showdown calls checkHit before Counter
1238
1161
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1162
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1163
  _ = 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
1164
  return null;
1244
1165
  }
1245
1166
 
@@ -1283,7 +1204,6 @@ fn mirrorMove(
1283
1204
  battle: anytype,
1284
1205
  player: Player,
1285
1206
  mslot: u4,
1286
- rewrap: bool,
1287
1207
  auto: bool,
1288
1208
  residual: *bool,
1289
1209
  log: anytype,
@@ -1296,30 +1216,41 @@ fn mirrorMove(
1296
1216
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1217
  try log.fail(battle.active(player), .None);
1298
1218
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1219
  }
1302
1220
 
1221
+ incrementPP(side, mslot);
1222
+
1303
1223
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1224
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1225
  }
1306
1226
 
1307
1227
  fn metronome(
1308
1228
  battle: anytype,
1309
1229
  player: Player,
1310
1230
  mslot: u4,
1311
- rewrap: bool,
1312
1231
  auto: bool,
1313
1232
  residual: *bool,
1314
1233
  log: anytype,
1315
1234
  ) !?Result {
1316
1235
  var side = battle.side(player);
1317
1236
 
1318
- side.last_selected_move = Rolls.metronome(battle);
1237
+ side.last_selected_move = if (showdown) blk: {
1238
+ const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1239
+ const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1240
+ break :blk @intToEnum(Move, r + mod);
1241
+ } else loop: {
1242
+ while (true) {
1243
+ const r = battle.rng.next();
1244
+ if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1245
+ if (r >= @enumToInt(Move.Struggle)) continue;
1246
+ break :loop @intToEnum(Move, r);
1247
+ }
1248
+ };
1249
+
1319
1250
  incrementPP(side, mslot);
1320
1251
 
1321
1252
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1253
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1254
  }
1324
1255
 
1325
1256
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1373,24 +1304,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1373
1304
  }
1374
1305
 
1375
1306
  // 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));
1307
+ var state = &side.active.volatiles.state;
1308
+ const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1309
+ assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1310
+ var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1382
1311
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1383
1312
  accuracy = accuracy * boost[0] / boost[1];
1384
1313
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1385
1314
  accuracy = accuracy * boost[0] / boost[1];
1386
1315
  accuracy = @min(255, @max(1, accuracy));
1387
1316
 
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
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1391
1318
 
1392
1319
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1393
- break :miss !Rolls.hit(battle, accuracy);
1320
+ break :miss if (showdown)
1321
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1322
+ else
1323
+ battle.rng.next() >= accuracy;
1394
1324
  };
1395
1325
 
1396
1326
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1505,15 +1435,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1505
1435
  }
1506
1436
 
1507
1437
  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
1438
  var damage = @max(stored.stats.hp / 16, 1);
1518
1439
 
1519
1440
  // GLITCH: Leech Seed + Toxic glitch
@@ -1525,6 +1446,10 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1525
1446
  const amount = @min(damage, stored.hp);
1526
1447
  stored.hp -= amount;
1527
1448
 
1449
+ var foe = battle.foe(player);
1450
+ var foe_stored = foe.stored();
1451
+ const foe_ident = battle.active(player.foe());
1452
+
1528
1453
  // As above, Pokémon Showdown uses damageOf but its not relevant
1529
1454
  if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1530
1455
 
@@ -1540,13 +1465,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1465
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1466
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1467
 
1543
- battle.turn += 1;
1544
-
1545
1468
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1469
  try log.tie();
1547
1470
  return Result.Tie;
1548
1471
  }
1549
1472
 
1473
+ battle.turn += 1;
1474
+
1550
1475
  if (showdown and battle.turn >= 1000) {
1551
1476
  try log.tie();
1552
1477
  return Result.Tie;
@@ -1613,42 +1538,6 @@ fn checkEBC(battle: anytype) bool {
1613
1538
  return false;
1614
1539
  }
1615
1540
 
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
1541
  inline fn onBegin(
1653
1542
  battle: anytype,
1654
1543
  player: Player,
@@ -1739,7 +1628,10 @@ pub const Effects = struct {
1739
1628
  side.active.volatiles.Bide = true;
1740
1629
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1741
1630
  side.active.volatiles.state = 0;
1742
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1631
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1632
+ battle.rng.range(u4, 2, 4)
1633
+ else
1634
+ (battle.rng.next() & 1) + 2);
1743
1635
 
1744
1636
  try log.start(battle.active(player), .Bide);
1745
1637
  }
@@ -1761,8 +1653,14 @@ pub const Effects = struct {
1761
1653
  return;
1762
1654
  }
1763
1655
 
1764
- if (foe.active.types.includes(move.type)) return;
1765
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1656
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1657
+ battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1658
+ else
1659
+ battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1660
+ Gen12.percent(10)
1661
+ else
1662
+ Gen12.percent(30));
1663
+ if (!chance) return;
1766
1664
 
1767
1665
  foe_stored.status = Status.init(.BRN);
1768
1666
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1786,7 +1684,11 @@ pub const Effects = struct {
1786
1684
  const sub = foe.active.volatiles.Substitute;
1787
1685
 
1788
1686
  if (move.effect == .ConfusionChance) {
1789
- if (!Rolls.confusionChance(battle)) return;
1687
+ const chance = if (showdown)
1688
+ battle.rng.chance(u8, 25, 256)
1689
+ else
1690
+ battle.rng.next() < Gen12.percent(10);
1691
+ if (!chance) return;
1790
1692
  } else {
1791
1693
  if (showdown) {
1792
1694
  if (!try checkHit(battle, player, move, log)) {
@@ -1805,7 +1707,10 @@ pub const Effects = struct {
1805
1707
 
1806
1708
  if (foe.active.volatiles.Confusion) return;
1807
1709
  foe.active.volatiles.Confusion = true;
1808
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1710
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1711
+ battle.rng.range(u8, 2, 6)
1712
+ else
1713
+ (battle.rng.next() & 3) + 2);
1809
1714
 
1810
1715
  try log.start(battle.active(player.foe()), .Confusion);
1811
1716
  }
@@ -1855,8 +1760,11 @@ pub const Effects = struct {
1855
1760
  return Result.Error;
1856
1761
  }
1857
1762
 
1858
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
- volatiles.disabled_duration = Rolls.disableDuration(battle);
1763
+ volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1764
+ volatiles.disabled_duration = @intCast(u4, if (showdown)
1765
+ battle.rng.range(u8, 1, 9)
1766
+ else
1767
+ (battle.rng.next() & 7) + 1);
1860
1768
 
1861
1769
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1862
1770
  return null;
@@ -1868,7 +1776,7 @@ pub const Effects = struct {
1868
1776
 
1869
1777
  if (battle.last_damage == 0) {
1870
1778
  assert(showdown);
1871
- if (!battle.foe(player).active.volatiles.Substitute) return;
1779
+ return;
1872
1780
  }
1873
1781
 
1874
1782
  const drain = @max(battle.last_damage / 2, 1);
@@ -1895,7 +1803,15 @@ pub const Effects = struct {
1895
1803
  var volatiles = &battle.foe(player).active.volatiles;
1896
1804
 
1897
1805
  if (volatiles.Substitute) return;
1898
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1806
+
1807
+ const chance = if (showdown)
1808
+ battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1809
+ else
1810
+ battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1811
+ Gen12.percent(10)
1812
+ else
1813
+ Gen12.percent(30));
1814
+ if (!chance) return;
1899
1815
 
1900
1816
  volatiles.Flinch = true;
1901
1817
  volatiles.Recharging = false;
@@ -1921,8 +1837,12 @@ pub const Effects = struct {
1921
1837
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1922
1838
  }
1923
1839
 
1924
- if (foe.active.types.includes(move.type)) return;
1925
- if (!Rolls.secondaryChance(battle, true)) return;
1840
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
1841
+ battle.rng.chance(u8, 26, 256)
1842
+ else
1843
+ battle.rng.next() < 1 + Gen12.percent(10);
1844
+ if (!chance) return;
1845
+
1926
1846
  // Freeze Clause Mod
1927
1847
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1928
1848
 
@@ -2059,15 +1979,15 @@ pub const Effects = struct {
2059
1979
  }
2060
1980
  break :has_mimic false;
2061
1981
  };
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;
1982
+ // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1983
+ // trigger |-miss| instead of |-fail|
1984
+ if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2065
1985
  return try log.fail(battle.active(player.foe()), .None);
2066
1986
  }
2067
1987
  }
2068
1988
  if (!try checkHit(battle, player, move, log)) return;
2069
1989
 
2070
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
1990
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2071
1991
  side.active.move(oslot).id = foe.active.move(rslot).id;
2072
1992
 
2073
1993
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2088,8 +2008,7 @@ pub const Effects = struct {
2088
2008
  assert(!side.active.volatiles.MultiHit);
2089
2009
  side.active.volatiles.MultiHit = true;
2090
2010
 
2091
- side.active.volatiles.attacks =
2092
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2011
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2093
2012
  }
2094
2013
 
2095
2014
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2133,8 +2052,14 @@ pub const Effects = struct {
2133
2052
  }
2134
2053
 
2135
2054
  // 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;
2055
+ const chance = !foe.active.types.includes(move.type) and if (showdown)
2056
+ battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2057
+ else
2058
+ battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2059
+ Gen12.percent(10)
2060
+ else
2061
+ Gen12.percent(30));
2062
+ if (!chance) return;
2138
2063
 
2139
2064
  foe_stored.status = Status.init(.PAR);
2140
2065
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2177,7 +2102,16 @@ pub const Effects = struct {
2177
2102
 
2178
2103
  if (move.effect == .Poison) {
2179
2104
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2180
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2105
+ } else {
2106
+ const chance = if (showdown)
2107
+ battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2108
+ else
2109
+ battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2110
+ Gen12.percent(20)
2111
+ else
2112
+ Gen12.percent(40));
2113
+ if (!chance) return;
2114
+ }
2181
2115
 
2182
2116
  foe_stored.status = Status.init(.PSN);
2183
2117
  if (toxic) {
@@ -2191,8 +2125,10 @@ pub const Effects = struct {
2191
2125
 
2192
2126
  fn rage(battle: anytype, player: Player) !void {
2193
2127
  var volatiles = &battle.side(player).active.volatiles;
2194
- assert(!volatiles.Bide);
2128
+ if (volatiles.Rage) return;
2195
2129
  volatiles.Rage = true;
2130
+ assert(!volatiles.Bide);
2131
+ volatiles.state = 0;
2196
2132
  }
2197
2133
 
2198
2134
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2244,7 +2180,16 @@ pub const Effects = struct {
2244
2180
  }
2245
2181
  foe.active.volatiles.Recharging = false;
2246
2182
 
2247
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2183
+ const duration = @intCast(u3, if (showdown)
2184
+ battle.rng.range(u8, 1, 8)
2185
+ else loop: {
2186
+ while (true) {
2187
+ const r = battle.rng.next() & 7;
2188
+ if (r != 0) break :loop r;
2189
+ }
2190
+ });
2191
+
2192
+ foe_stored.status = Status.slp(duration);
2248
2193
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2249
2194
  }
2250
2195
 
@@ -2286,9 +2231,8 @@ pub const Effects = struct {
2286
2231
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
2287
2232
  if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2288
2233
 
2289
- // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2234
+ // Whirlwind/Roar should not roll to hit but Pokémon Showdown does anyway
2290
2235
  _ = try checkHit(battle, player, move, log);
2291
- battle.last_damage = 0;
2292
2236
  }
2293
2237
 
2294
2238
  fn thrashing(battle: anytype, player: Player) void {
@@ -2297,7 +2241,11 @@ pub const Effects = struct {
2297
2241
  assert(!volatiles.Bide);
2298
2242
 
2299
2243
  volatiles.Thrashing = true;
2300
- volatiles.attacks = Rolls.bideThrashDuration(battle);
2244
+ volatiles.state = 0;
2245
+ volatiles.attacks = @intCast(u3, if (showdown)
2246
+ battle.rng.range(u8, 2, 4)
2247
+ else
2248
+ (battle.rng.next() & 1) + 2);
2301
2249
  }
2302
2250
 
2303
2251
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2329,7 +2277,7 @@ pub const Effects = struct {
2329
2277
  try log.transform(battle.active(player), foe_ident);
2330
2278
  }
2331
2279
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2280
+ fn binding(battle: anytype, player: Player) void {
2333
2281
  var side = battle.side(player);
2334
2282
  var foe = battle.foe(player);
2335
2283
 
@@ -2337,11 +2285,9 @@ pub const Effects = struct {
2337
2285
  side.active.volatiles.Binding = true;
2338
2286
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2287
  // (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);
2288
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2289
 
2344
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2290
+ side.active.volatiles.attacks = distribution(battle) - 1;
2345
2291
  }
2346
2292
 
2347
2293
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2368,11 +2314,9 @@ pub const Effects = struct {
2368
2314
  return try log.fail(ident, .None);
2369
2315
  }
2370
2316
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2371
- const stat = unmodifiedStats(battle, side).atk;
2317
+ const stat = unmodifiedStats(battle, player).atk;
2372
2318
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2319
  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
2320
  },
2377
2321
  .DefenseUp1, .DefenseUp2 => {
2378
2322
  assert(boosts.def >= -6 and boosts.def <= 6);
@@ -2388,7 +2332,7 @@ pub const Effects = struct {
2388
2332
  return try log.fail(ident, .None);
2389
2333
  }
2390
2334
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2391
- const stat = unmodifiedStats(battle, side).def;
2335
+ const stat = unmodifiedStats(battle, player).def;
2392
2336
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2393
2337
  try log.boost(ident, .Defense, n);
2394
2338
  },
@@ -2405,7 +2349,7 @@ pub const Effects = struct {
2405
2349
  return try log.fail(ident, .None);
2406
2350
  }
2407
2351
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2408
- const stat = unmodifiedStats(battle, side).spe;
2352
+ const stat = unmodifiedStats(battle, player).spe;
2409
2353
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2410
2354
  try log.boost(ident, .Speed, 2);
2411
2355
  },
@@ -2425,7 +2369,7 @@ pub const Effects = struct {
2425
2369
  return try log.fail(ident, .None);
2426
2370
  }
2427
2371
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2428
- const stat = unmodifiedStats(battle, side).spc;
2372
+ const stat = unmodifiedStats(battle, player).spc;
2429
2373
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2430
2374
  try log.boost(ident, .SpecialAttack, n);
2431
2375
  try log.boost(ident, .SpecialDefense, n);
@@ -2452,7 +2396,11 @@ pub const Effects = struct {
2452
2396
  }
2453
2397
 
2454
2398
  if (move.effect.isStatDownChance()) {
2455
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2399
+ const chance = if (showdown)
2400
+ battle.rng.chance(u8, 85, 256)
2401
+ else
2402
+ battle.rng.next() < Gen12.percent(33) + 1;
2403
+ if (!chance or foe.active.volatiles.Invulnerable) return;
2456
2404
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2457
2405
  return; // checkHit already checks for Invulnerable
2458
2406
  }
@@ -2474,7 +2422,7 @@ pub const Effects = struct {
2474
2422
  return try log.fail(foe_ident, .None);
2475
2423
  }
2476
2424
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2477
- const stat = unmodifiedStats(battle, foe).atk;
2425
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2478
2426
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2479
2427
  try log.boost(foe_ident, .Attack, -1);
2480
2428
  },
@@ -2492,7 +2440,7 @@ pub const Effects = struct {
2492
2440
  return try log.fail(foe_ident, .None);
2493
2441
  }
2494
2442
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2495
- const stat = unmodifiedStats(battle, foe).def;
2443
+ const stat = unmodifiedStats(battle, player.foe()).def;
2496
2444
  stats.def = @max(1, stat * mod[0] / mod[1]);
2497
2445
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2498
2446
  },
@@ -2509,7 +2457,7 @@ pub const Effects = struct {
2509
2457
  return try log.fail(foe_ident, .None);
2510
2458
  }
2511
2459
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2512
- const stat = unmodifiedStats(battle, foe).spe;
2460
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2513
2461
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2514
2462
  try log.boost(foe_ident, .Speed, -1);
2515
2463
  assert(boosts.spe >= -6);
@@ -2529,7 +2477,7 @@ pub const Effects = struct {
2529
2477
  return try log.fail(foe_ident, .None);
2530
2478
  }
2531
2479
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2532
- const stat = unmodifiedStats(battle, foe).spc;
2480
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2533
2481
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2534
2482
  try log.boost(foe_ident, .SpecialAttack, -1);
2535
2483
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2548,7 +2496,8 @@ pub const Effects = struct {
2548
2496
  }
2549
2497
  };
2550
2498
 
2551
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2499
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2500
+ const side = battle.side(who);
2552
2501
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2553
2502
  const id = ID.from(side.active.volatiles.transform);
2554
2503
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2614,140 +2563,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2614
2563
  }
2615
2564
  }
2616
2565
 
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;
2566
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2567
+ if (side.active.volatiles.disabled_move != 0) {
2568
+ // A Pokémon that transforms after being disabled may end up with less move slots
2569
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2570
+ if (m != .None and m == side.last_selected_move) {
2571
+ side.active.volatiles.Charging = false;
2572
+ try log.disabled(ident, side.last_selected_move);
2573
+ return true;
2633
2574
  }
2634
2575
  }
2576
+ return false;
2577
+ }
2635
2578
 
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);
2579
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2580
+ const side = battle.side(who);
2581
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2582
+ try Effects.boost(battle, who, Move.get(.Rage), log);
2654
2583
  }
2584
+ }
2655
2585
 
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
- }
2586
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2587
+ var volatiles = &active.volatiles;
2588
+ assert(volatiles.Thrashing);
2589
+ if (volatiles.attacks > 0) return false;
2660
2590
 
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
- }
2591
+ volatiles.Thrashing = false;
2592
+ volatiles.Confusion = true;
2593
+ volatiles.confusion = @intCast(u3, if (showdown)
2594
+ battle.rng.range(u8, 2, 6)
2595
+ else
2596
+ (battle.rng.next() & 3) + 2);
2665
2597
 
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
- }
2598
+ return true;
2599
+ }
2670
2600
 
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
- }
2601
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2678
2602
 
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
- }
2603
+ fn distribution(battle: anytype) u3 {
2604
+ if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2605
+ const r = (battle.rng.next() & 3);
2606
+ return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2607
+ }
2683
2608
 
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
- }
2609
+ fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2610
+ if (showdown) {
2611
+ if (check_pp == 0) {
2612
+ var i: usize = moves.len;
2613
+ while (i > 0) {
2614
+ i -= 1;
2615
+ if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2616
+ }
2617
+ } else {
2618
+ var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2619
+ var i: usize = 0;
2620
+ while (i < moves.len and r > 0) : (i += 1) {
2621
+ if (moves[i].pp > 0) {
2622
+ r -= 1;
2623
+ if (r == 0) break;
2702
2624
  }
2703
- return @intCast(u4, i + 1);
2704
2625
  }
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;
2710
- }
2711
- }
2712
-
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);
2716
- }
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;
2626
+ return @intCast(u4, i + 1);
2737
2627
  }
2738
2628
  }
2739
2629
 
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);
2630
+ while (true) {
2631
+ const r = @intCast(u4, rand.next() & 3);
2632
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2744
2633
  }
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
- };
2634
+ }
2751
2635
 
2752
2636
  test "RNG agreement" {
2753
2637
  if (!showdown) return;
@@ -2847,11 +2731,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
2847
2731
  while (slot <= 4) : (slot += 1) {
2848
2732
  const m = active.moves[slot - 1];
2849
2733
  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;
2734
+ if (m.id == side.last_selected_move) {
2735
+ // Pokémon Showdown displays Struggle if limited to Bide when out of PP
2736
+ const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
2855
2737
  out[n] = .{ .type = .Move, .data = s };
2856
2738
  n += 1;
2857
2739
  return n;