@pkmn/engine 0.1.0-dev.9a0c07c3 → 0.1.0-dev.af1565df

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,30 +56,34 @@ const BOOSTS = &[_][2]u8{
56
56
 
57
57
  const MAX_STAT_VALUE = 999;
58
58
 
59
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
60
+
59
61
  pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
60
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
61
63
  if (battle.turn == 0) return start(battle, log);
62
64
 
65
+ var s1 = false;
66
+ var s2 = false;
63
67
  var f1: ?Move = null;
64
68
  var f2: ?Move = null;
65
69
 
66
- if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
67
- if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
70
+ if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
+ if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
+
73
+ var p1 = battle.side(.P1);
74
+ var p2 = battle.side(.P2);
75
+
76
+ var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
68
78
 
69
79
  if (turnOrder(battle, c1, c2) == .P1) {
70
- if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
80
+ if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
71
81
  } else {
72
- if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
82
+ if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
73
83
  }
74
84
 
75
- var p1 = battle.side(.P1);
76
- if (p1.active.volatiles.attacks == 0) {
77
- p1.active.volatiles.Binding = false;
78
- }
79
- var p2 = battle.side(.P2);
80
- if (p2.active.volatiles.attacks == 0) {
81
- p2.active.volatiles.Binding = false;
82
- }
85
+ if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
+ if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
83
87
 
84
88
  return endTurn(battle, log);
85
89
  }
@@ -112,6 +116,7 @@ fn selectMove(
112
116
  player: Player,
113
117
  choice: Choice,
114
118
  foe_choice: Choice,
119
+ skip_turn: *bool,
115
120
  from: *?Move,
116
121
  ) ?Result {
117
122
  if (choice.type == .Pass) return null;
@@ -124,17 +129,26 @@ fn selectMove(
124
129
  (choice.type == .Move and choice.data == @boolToInt(showdown)));
125
130
 
126
131
  // pre-battle menu
127
- if (volatiles.Recharging) return null;
132
+ if (volatiles.Recharging) {
133
+ if (showdown and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
134
+ return null;
135
+ }
128
136
  if (volatiles.Rage) {
129
137
  from.* = side.last_used_move;
130
- if (showdown) saveMove(battle, player, null);
138
+ if (showdown) {
139
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
140
+ saveMove(battle, player, null);
141
+ }
131
142
  return null;
132
143
  }
133
144
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
134
145
  if (!showdown) volatiles.Flinch = false;
135
146
  if (volatiles.Thrashing or volatiles.Charging) {
136
147
  from.* = side.last_selected_move;
137
- if (showdown) saveMove(battle, player, null);
148
+ if (showdown) {
149
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
150
+ saveMove(battle, player, null);
151
+ }
138
152
  return null;
139
153
  }
140
154
 
@@ -144,7 +158,10 @@ fn selectMove(
144
158
  // pre-move select
145
159
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
146
160
  assert(showdown or choice.data == 0);
147
- if (showdown) saveMove(battle, player, choice);
161
+ if (showdown) {
162
+ if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
163
+ saveMove(battle, player, choice);
164
+ }
148
165
  return null;
149
166
  }
150
167
  if (volatiles.Binding) {
@@ -163,9 +180,9 @@ fn selectMove(
163
180
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
164
181
  if (foe_choice.type == .Switch) {
165
182
  const slot = if (player == .P1)
166
- battle.last_selected_indexes.p1
183
+ battle.last_moves.p1_index
167
184
  else
168
- battle.last_selected_indexes.p2;
185
+ battle.last_moves.p2_index;
169
186
  const last = side.active.move(@intCast(u4, slot));
170
187
  if (last.id == .Metronome) side.last_selected_move = last.id;
171
188
  if (last.id == .MirrorMove) return Result.Error;
@@ -175,6 +192,7 @@ fn selectMove(
175
192
  }
176
193
 
177
194
  if (battle.foe(player).active.volatiles.Binding) {
195
+ skip_turn.* = true;
178
196
  if (showdown) {
179
197
  saveMove(battle, player, choice);
180
198
  } else {
@@ -185,6 +203,7 @@ fn selectMove(
185
203
  }
186
204
 
187
205
  // move select
206
+ volatiles.state = 0;
188
207
  if (choice.data == 0) {
189
208
  const struggle = ok: {
190
209
  for (side.active.moves, 0..) |move, i| {
@@ -218,9 +237,9 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
218
237
 
219
238
  side.last_selected_move = move.id;
220
239
  if (player == .P1) {
221
- battle.last_selected_indexes.p1 = @intCast(u4, c.data);
240
+ battle.last_moves.p1_index = @intCast(u3, c.data);
222
241
  } else {
223
- battle.last_selected_indexes.p2 = @intCast(u4, c.data);
242
+ battle.last_moves.p2_index = @intCast(u3, c.data);
224
243
  }
225
244
  }
226
245
  }
@@ -240,9 +259,9 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, log: anyty
240
259
  side.order[slot - 1] = out;
241
260
 
242
261
  if (player == .P1) {
243
- battle.last_selected_indexes.p1 = 1;
262
+ battle.last_moves.p1_index = 1;
244
263
  } else {
245
- battle.last_selected_indexes.p2 = 1;
264
+ battle.last_moves.p2_index = 1;
246
265
  }
247
266
 
248
267
  side.last_used_move = .None;
@@ -299,13 +318,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
299
318
  const spe2 = battle.side(.P2).active.stats.spe;
300
319
  if (spe1 == spe2) {
301
320
  // Pokémon Showdown's beforeTurnCallback shenanigans
302
- if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
303
-
304
- const p1 = if (showdown)
305
- battle.rng.range(u8, 0, 2) == 0
306
- else
307
- battle.rng.next() < Gen12.percent(50) + 1;
321
+ if (showdown and !double_switch and m1 == .Counter and m2 == .Counter) {
322
+ battle.rng.advance(1);
323
+ }
308
324
 
325
+ const p1 = Rolls.speedTie(battle);
309
326
  if (!showdown) return if (p1) .P1 else .P2;
310
327
 
311
328
  // Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
@@ -315,7 +332,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
315
332
  const t1 = battle.side(.P1).active.volatiles.Thrashing;
316
333
  const t2 = battle.side(.P2).active.volatiles.Thrashing;
317
334
  // If *both* sides are thrashing it really should be another speed tie, but we've patched
318
- // that out and enforce host ordering of events, so P1 just goes first reqgardless of who
335
+ // that out and enforce host ordering of events, so P1 just goes first regardless of who
319
336
  // won the original coin flip
320
337
  if (t1 and t2) return .P1;
321
338
  return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
@@ -328,9 +345,13 @@ fn doTurn(
328
345
  battle: anytype,
329
346
  player: Player,
330
347
  player_choice: Choice,
348
+ player_rewrap: bool,
349
+ player_skip: bool,
331
350
  player_from: ?Move,
332
351
  foe_player: Player,
333
352
  foe_choice: Choice,
353
+ foe_rewrap: bool,
354
+ foe_skip: bool,
334
355
  foe_from: ?Move,
335
356
  log: anytype,
336
357
  ) !?Result {
@@ -338,7 +359,16 @@ fn doTurn(
338
359
 
339
360
  var residual = true;
340
361
  var replace = battle.side(player).stored().hp == 0;
341
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
362
+ if (try executeMove(
363
+ battle,
364
+ player,
365
+ player_choice,
366
+ player_rewrap,
367
+ player_skip,
368
+ player_from,
369
+ &residual,
370
+ log,
371
+ )) |r| return r;
342
372
  if (!replace) {
343
373
  if (player_choice.type != .Switch) {
344
374
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -349,7 +379,16 @@ fn doTurn(
349
379
 
350
380
  residual = true;
351
381
  replace = battle.side(foe_player).stored().hp == 0;
352
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
382
+ if (try executeMove(
383
+ battle,
384
+ foe_player,
385
+ foe_choice,
386
+ foe_rewrap,
387
+ foe_skip,
388
+ foe_from,
389
+ &residual,
390
+ log,
391
+ )) |r| return r;
353
392
  if (!replace) {
354
393
  if (foe_choice.type != .Switch) {
355
394
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -372,6 +411,8 @@ fn executeMove(
372
411
  battle: anytype,
373
412
  player: Player,
374
413
  choice: Choice,
414
+ rewrap: bool,
415
+ skip: bool,
375
416
  from: ?Move,
376
417
  residual: *bool,
377
418
  log: anytype,
@@ -383,9 +424,10 @@ fn executeMove(
383
424
  return null;
384
425
  }
385
426
 
427
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
428
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
429
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
386
430
  if (side.last_selected_move == .SKIP_TURN) {
387
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
388
- // Binding instead gets handled in beforeMove after sleep and freeze
389
431
  assert(!showdown);
390
432
  if (battle.foe(player).active.volatiles.Binding) {
391
433
  try log.cant(battle.active(player), .Bound);
@@ -404,9 +446,9 @@ fn executeMove(
404
446
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
405
447
  assert(!showdown);
406
448
  mslot = @intCast(u4, if (player == .P1)
407
- battle.last_selected_indexes.p1
449
+ battle.last_moves.p1_index
408
450
  else
409
- battle.last_selected_indexes.p2);
451
+ battle.last_moves.p2_index);
410
452
  const stored = side.stored();
411
453
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
412
454
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -428,14 +470,14 @@ fn executeMove(
428
470
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
429
471
  assert(mslot == 1);
430
472
  mslot = @intCast(u4, if (player == .P1)
431
- battle.last_selected_indexes.p1
473
+ battle.last_moves.p1_index
432
474
  else
433
- battle.last_selected_indexes.p2);
475
+ battle.last_moves.p2_index);
434
476
  }
435
477
 
436
478
  var skip_can = false;
437
479
  var skip_pp = false;
438
- switch (try beforeMove(battle, player, from, residual, log)) {
480
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
439
481
  .done => return null,
440
482
  .skip_can => skip_can = true,
441
483
  .skip_pp => skip_pp = true,
@@ -447,14 +489,15 @@ fn executeMove(
447
489
  return null;
448
490
  }
449
491
 
450
- return doMove(battle, player, mslot, auto, residual, log);
492
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
451
493
  }
452
494
 
453
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
495
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
454
496
 
455
497
  fn beforeMove(
456
498
  battle: anytype,
457
499
  player: Player,
500
+ skip: bool,
458
501
  from: ?Move,
459
502
  residual: *bool,
460
503
  log: anytype,
@@ -486,13 +529,15 @@ fn beforeMove(
486
529
  return .done;
487
530
  }
488
531
 
489
- if (foe.active.volatiles.Binding) {
532
+ if (skip or foe.active.volatiles.Binding) {
490
533
  try log.cant(ident, .Bound);
491
534
  return .done;
492
535
  }
493
536
 
494
- if (!showdown and volatiles.Flinch) {
495
- volatiles.Flinch = false;
537
+ if (volatiles.Flinch) {
538
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
539
+ // Pokémon can sometimes flinch multiple times from the same original hit
540
+ if (!showdown) volatiles.Flinch = false;
496
541
  try log.cant(ident, .Flinch);
497
542
  return .done;
498
543
  }
@@ -513,13 +558,6 @@ fn beforeMove(
513
558
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
514
559
  if (showdown and try disabled(side, ident, log)) return .done;
515
560
 
516
- // Pokémon Showdown checks for Flinch *after* instead of before
517
- if (showdown and volatiles.Flinch) {
518
- volatiles.Flinch = false;
519
- try log.cant(ident, .Flinch);
520
- return .done;
521
- }
522
-
523
561
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
524
562
  // before the side had a selected a move - we simply need to assume this leads to a desync
525
563
  if (side.last_selected_move == .None) {
@@ -537,12 +575,7 @@ fn beforeMove(
537
575
  } else {
538
576
  try log.activate(ident, .Confusion);
539
577
 
540
- const confused = if (showdown)
541
- !battle.rng.chance(u8, 128, 256)
542
- else
543
- battle.rng.next() >= Gen12.percent(50) + 1;
544
-
545
- if (confused) {
578
+ if (Rolls.confused(battle)) {
546
579
  assert(!volatiles.MultiHit);
547
580
  if (!volatiles.Rage) volatiles.state = 0;
548
581
  volatiles.Bide = false;
@@ -568,8 +601,12 @@ fn beforeMove(
568
601
  player
569
602
  else
570
603
  player.foe();
604
+
605
+ const uncapped = battle.last_damage;
571
606
  // Skipping adjustDamage / randomizeDamage / checkHit
572
607
  _ = try applyDamage(battle, player, target, .Confusion, log);
608
+ // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
609
+ if (showdown) battle.last_damage = uncapped;
573
610
 
574
611
  return .done;
575
612
  }
@@ -578,28 +615,28 @@ fn beforeMove(
578
615
 
579
616
  if (!showdown and try disabled(side, ident, log)) return .done;
580
617
 
581
- if (Status.is(stored.status, .PAR)) {
582
- const paralyzed = if (showdown)
583
- battle.rng.chance(u8, 63, 256)
584
- else
585
- battle.rng.next() < Gen12.percent(25);
586
-
587
- if (paralyzed) {
588
- if (!volatiles.Rage) volatiles.state = 0;
589
- volatiles.Bide = false;
590
- volatiles.Thrashing = false;
591
- volatiles.Charging = false;
592
- volatiles.Binding = false;
593
- // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
594
- try log.cant(ident, .Paralysis);
595
- return .done;
596
- }
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;
597
627
  }
598
628
 
599
629
  if (volatiles.Bide) {
600
630
  assert(!volatiles.Thrashing and !volatiles.Rage);
601
631
 
602
- volatiles.state +%= battle.last_damage;
632
+ if (showdown) {
633
+ // Pokémon Showdown doesn't implement Bide potentially overflowing in the event of
634
+ // OHKO-move damage, but we can fake this incorrect behavior by simply saturating the
635
+ // addition because 65535 is sufficient to faint any Pokémon anyway
636
+ volatiles.state +|= battle.last_damage;
637
+ } else {
638
+ volatiles.state +%= battle.last_damage;
639
+ }
603
640
 
604
641
  assert(volatiles.attacks > 0);
605
642
 
@@ -643,8 +680,13 @@ fn beforeMove(
643
680
  try log.start(battle.active(player), .ConfusionSilent);
644
681
  }
645
682
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
646
- if (showdown and handleThrashing(battle, active)) {
647
- try log.start(battle.active(player), .ConfusionSilent);
683
+ if (showdown) {
684
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
685
+ // used in useMove and doesn't have the notion of skipping canMove semantics
686
+ side.last_used_move = side.last_selected_move;
687
+ if (handleThrashing(battle, active)) {
688
+ try log.start(battle.active(player), .ConfusionSilent);
689
+ }
648
690
  }
649
691
  return .skip_can;
650
692
  }
@@ -659,7 +701,11 @@ fn beforeMove(
659
701
  side.last_selected_move,
660
702
  );
661
703
  if (showdown or battle.last_damage != 0) {
704
+ const sub = showdown and foe.active.volatiles.Substitute;
662
705
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
706
+ if (battle.foe(player).stored().hp > 0 and !sub) {
707
+ try buildRage(battle, player.foe(), log);
708
+ }
663
709
  }
664
710
  return .done;
665
711
  }
@@ -691,6 +737,7 @@ fn canMove(
691
737
  side.active.volatiles.Invulnerable = false;
692
738
  } else if (move.effect == .Charge) {
693
739
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
+ setCounterable(battle, player, side, move);
694
741
  try Effects.charge(battle, player, log);
695
742
  return false;
696
743
  }
@@ -707,6 +754,7 @@ fn canMove(
707
754
  // regrettable, but this information is more "nice to have" than required
708
755
  const f = if (from != null and from.? == .None) null else from;
709
756
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
+ setCounterable(battle, player, side, move);
710
758
 
711
759
  if (move.effect.onBegin()) {
712
760
  try onBegin(battle, player, move, mslot, residual, log);
@@ -717,12 +765,27 @@ fn canMove(
717
765
  Effects.thrashing(battle, player);
718
766
  } else if (!showdown and move.effect == .Binding) {
719
767
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
720
- Effects.binding(battle, player);
768
+ Effects.binding(battle, player, false);
721
769
  }
722
770
 
723
771
  return true;
724
772
  }
725
773
 
774
+ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data) void {
775
+ // The Counter desync is caused by the cartridge not calling GetCurrentMove until now, meaning
776
+ // in cases where an early return happens the data for a players last selected move does not get
777
+ // reloaded and HandleCounterMove actually bases its success/failure off of stale information.
778
+ // This boolean state we track here doesn't exist on the cartridge because it instead manifests
779
+ // as the desync results from actually having two separate battle states that subtly disagree
780
+ const counterable = side.last_selected_move != .Counter and move.bp > 0 and
781
+ (move.type == .Normal or move.type == .Fighting);
782
+ if (player == .P1) {
783
+ battle.last_moves.p1_counterable = @boolToInt(counterable);
784
+ } else {
785
+ battle.last_moves.p2_counterable = @boolToInt(counterable);
786
+ }
787
+ }
788
+
726
789
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
727
790
  if (side.last_selected_move == .Struggle) return;
728
791
 
@@ -758,6 +821,7 @@ fn doMove(
758
821
  battle: anytype,
759
822
  player: Player,
760
823
  mslot: u4,
824
+ rewrap: bool,
761
825
  auto: bool,
762
826
  residual: *bool,
763
827
  log: anytype,
@@ -796,7 +860,7 @@ fn doMove(
796
860
  };
797
861
  assert(!immune or miss or (showdown and move.effect == .Binding));
798
862
 
799
- var late = showdown;
863
+ var late = showdown and move.effect != .Explode;
800
864
  const skip = status or immune;
801
865
  if ((!showdown or (!skip or counter)) and !miss) blk: {
802
866
  if (showdown and move.effect.isMulti()) {
@@ -849,9 +913,9 @@ fn doMove(
849
913
 
850
914
  if (!showdown or !miss) {
851
915
  if (move.effect == .MirrorMove) {
852
- return mirrorMove(battle, player, mslot, auto, residual, log);
916
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
853
917
  } else if (move.effect == .Metronome) {
854
- return metronome(battle, player, mslot, auto, residual, log);
918
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
855
919
  } else if (move.effect.onEnd()) {
856
920
  try onEnd(battle, player, move, log);
857
921
  return null;
@@ -866,7 +930,7 @@ fn doMove(
866
930
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
867
931
  if (ohko) {
868
932
  try log.immune(foe_ident, .OHKO);
869
- } else if (immune and !invulnerable) {
933
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
870
934
  try log.immune(foe_ident, .None);
871
935
  } else if (mist) {
872
936
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -916,7 +980,7 @@ fn doMove(
916
980
  if (hit == 0 and ohko) try log.ohko();
917
981
  hit += 1;
918
982
  if (foe.stored().hp == 0) break;
919
- if (!late and !sub) try buildRage(battle, player.foe(), log);
983
+ if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
920
984
  // If the substitute breaks during a multi-hit attack, the attack ends
921
985
  if (nullified) break;
922
986
  }
@@ -932,11 +996,17 @@ fn doMove(
932
996
  } else if (showdown) {
933
997
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
934
998
  if (move.effect == .Binding) {
935
- Effects.binding(battle, player);
999
+ Effects.binding(battle, player, rewrap);
936
1000
  if (immune) {
937
1001
  battle.last_damage = 0;
1002
+ assert(foe.stored().hp > 0);
938
1003
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
939
- try log.damage(battle.active(player.foe()), foe.stored(), .None);
1004
+ if (sub) {
1005
+ try log.activate(battle.active(player.foe()), .Substitute);
1006
+ } else {
1007
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1008
+ try buildRage(battle, player.foe(), log);
1009
+ }
940
1010
  return null;
941
1011
  }
942
1012
  }
@@ -987,8 +1057,7 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
987
1057
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
988
1058
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
989
1059
 
990
- if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
991
- return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1060
+ return Rolls.criticalHit(battle, chance);
992
1061
  }
993
1062
 
994
1063
  fn calcDamage(
@@ -1092,16 +1161,7 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1092
1161
 
1093
1162
  fn randomizeDamage(battle: anytype) void {
1094
1163
  if (battle.last_damage <= 1) return;
1095
-
1096
- const random = if (showdown)
1097
- battle.rng.range(u8, 217, 256)
1098
- else loop: {
1099
- while (true) {
1100
- const r = std.math.rotr(u8, battle.rng.next(), 1);
1101
- if (r >= 217) break :loop r;
1102
- }
1103
- };
1104
-
1164
+ const random = Rolls.damage(battle);
1105
1165
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1106
1166
  }
1107
1167
 
@@ -1119,16 +1179,9 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1119
1179
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1120
1180
  .Psywave => power: {
1121
1181
  const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
1122
- if (showdown) {
1123
- break :power battle.rng.range(u8, 0, max);
1124
- } else {
1125
- // GLITCH: Psywave infinite glitch loop
1126
- if (max <= 1) return Result.Error;
1127
- while (true) {
1128
- const r = battle.rng.next();
1129
- if (r < max) break :power r;
1130
- }
1131
- }
1182
+ // GLITCH: Psywave infinite glitch loop
1183
+ if (!showdown and max <= 1) return Result.Error;
1184
+ break :power Rolls.psywave(battle, max);
1132
1185
  },
1133
1186
  else => unreachable,
1134
1187
  };
@@ -1150,19 +1203,17 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1150
1203
  return null;
1151
1204
  }
1152
1205
 
1153
- // Pretend Counter was used as a stand-in when no move has been used to fail below with 0 BP
1154
- const foe_last_used_move =
1155
- Move.get(if (foe.last_used_move == .None) .Counter else foe.last_used_move);
1206
+ // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1156
1207
  const foe_last_selected_move =
1157
1208
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1158
- .Counter
1209
+ .Splash
1159
1210
  else
1160
1211
  foe.last_selected_move);
1161
1212
 
1162
- const used = foe_last_used_move.bp > 0 and
1163
- foe.last_used_move != .Counter and
1164
- (foe_last_used_move.type == .Normal or
1165
- foe_last_used_move.type == .Fighting);
1213
+ const used = (if (player.foe() == .P1)
1214
+ battle.last_moves.p1_counterable
1215
+ else
1216
+ battle.last_moves.p2_counterable) != 0;
1166
1217
 
1167
1218
  const selected = foe_last_selected_move.bp > 0 and
1168
1219
  foe.last_selected_move != .Counter and
@@ -1181,12 +1232,14 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1181
1232
  return null;
1182
1233
  }
1183
1234
 
1184
- battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1235
+ battle.last_damage *|= 2;
1185
1236
 
1186
1237
  // Pokémon Showdown calls checkHit before Counter
1187
1238
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1188
1239
 
1240
+ const sub = showdown and foe.active.volatiles.Substitute;
1189
1241
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1242
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1190
1243
  return null;
1191
1244
  }
1192
1245
 
@@ -1230,6 +1283,7 @@ fn mirrorMove(
1230
1283
  battle: anytype,
1231
1284
  player: Player,
1232
1285
  mslot: u4,
1286
+ rewrap: bool,
1233
1287
  auto: bool,
1234
1288
  residual: *bool,
1235
1289
  log: anytype,
@@ -1242,41 +1296,30 @@ fn mirrorMove(
1242
1296
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1243
1297
  try log.fail(battle.active(player), .None);
1244
1298
  return null;
1299
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1300
+ incrementPP(side, mslot);
1245
1301
  }
1246
1302
 
1247
- incrementPP(side, mslot);
1248
-
1249
1303
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1250
- return doMove(battle, player, mslot, auto, residual, log);
1304
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1251
1305
  }
1252
1306
 
1253
1307
  fn metronome(
1254
1308
  battle: anytype,
1255
1309
  player: Player,
1256
1310
  mslot: u4,
1311
+ rewrap: bool,
1257
1312
  auto: bool,
1258
1313
  residual: *bool,
1259
1314
  log: anytype,
1260
1315
  ) !?Result {
1261
1316
  var side = battle.side(player);
1262
1317
 
1263
- side.last_selected_move = if (showdown) blk: {
1264
- const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
1265
- const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
1266
- break :blk @intToEnum(Move, r + mod);
1267
- } else loop: {
1268
- while (true) {
1269
- const r = battle.rng.next();
1270
- if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
1271
- if (r >= @enumToInt(Move.Struggle)) continue;
1272
- break :loop @intToEnum(Move, r);
1273
- }
1274
- };
1275
-
1318
+ side.last_selected_move = Rolls.metronome(battle);
1276
1319
  incrementPP(side, mslot);
1277
1320
 
1278
1321
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1279
- return doMove(battle, player, mslot, auto, residual, log);
1322
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1280
1323
  }
1281
1324
 
1282
1325
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1330,23 +1373,24 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1330
1373
  }
1331
1374
 
1332
1375
  // GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
1333
- var state = &side.active.volatiles.state;
1334
- const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
1335
- assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
1336
- var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
1376
+ const state = side.active.volatiles.state;
1377
+ var overwrite = move.effect == .Rage or move.effect == .Thrashing;
1378
+ const overwritten = overwrite and state > 0;
1379
+ assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1380
+
1381
+ var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1337
1382
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1338
1383
  accuracy = accuracy * boost[0] / boost[1];
1339
1384
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1340
1385
  accuracy = accuracy * boost[0] / boost[1];
1341
1386
  accuracy = @min(255, @max(1, accuracy));
1342
1387
 
1343
- if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1388
+ // Pokémon Showdown only overwrites if the volatile is present
1389
+ if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
1390
+ if (overwrite) side.active.volatiles.state = accuracy;
1344
1391
 
1345
1392
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1346
- break :miss if (showdown)
1347
- !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1348
- else
1349
- battle.rng.next() >= accuracy;
1393
+ break :miss !Rolls.hit(battle, accuracy);
1350
1394
  };
1351
1395
 
1352
1396
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1478,9 +1522,6 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1478
1522
  damage *= volatiles.toxic;
1479
1523
  }
1480
1524
 
1481
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1482
- if (showdown) battle.last_damage = damage;
1483
-
1484
1525
  const amount = @min(damage, stored.hp);
1485
1526
  stored.hp -= amount;
1486
1527
 
@@ -1499,13 +1540,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1499
1540
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1500
1541
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1501
1542
 
1543
+ battle.turn += 1;
1544
+
1502
1545
  if (showdown and options.ebc and checkEBC(battle)) {
1503
1546
  try log.tie();
1504
1547
  return Result.Tie;
1505
1548
  }
1506
1549
 
1507
- battle.turn += 1;
1508
-
1509
1550
  if (showdown and battle.turn >= 1000) {
1510
1551
  try log.tie();
1511
1552
  return Result.Tie;
@@ -1572,6 +1613,42 @@ fn checkEBC(battle: anytype) bool {
1572
1613
  return false;
1573
1614
  }
1574
1615
 
1616
+ fn disabled(side: *Side, ident: ID, log: anytype) !bool {
1617
+ if (side.active.volatiles.disabled_move != 0) {
1618
+ // A Pokémon that transforms after being disabled may end up with less move slots
1619
+ const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1620
+ // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1621
+ const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1622
+ m
1623
+ else
1624
+ side.last_selected_move;
1625
+ if (m != .None and m == last) {
1626
+ side.active.volatiles.Charging = false;
1627
+ try log.disabled(ident, last);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
1635
+ const side = battle.side(who);
1636
+ if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
1637
+ try Effects.boost(battle, who, Move.get(.Rage), log);
1638
+ }
1639
+ }
1640
+
1641
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
1642
+ var volatiles = &active.volatiles;
1643
+ assert(volatiles.Thrashing);
1644
+ if (volatiles.attacks > 0) return false;
1645
+
1646
+ volatiles.Thrashing = false;
1647
+ volatiles.Confusion = true;
1648
+ volatiles.confusion = Rolls.confusionDuration(battle);
1649
+ return true;
1650
+ }
1651
+
1575
1652
  inline fn onBegin(
1576
1653
  battle: anytype,
1577
1654
  player: Player,
@@ -1662,10 +1739,7 @@ pub const Effects = struct {
1662
1739
  side.active.volatiles.Bide = true;
1663
1740
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1664
1741
  side.active.volatiles.state = 0;
1665
- side.active.volatiles.attacks = @intCast(u3, if (showdown)
1666
- battle.rng.range(u4, 2, 4)
1667
- else
1668
- (battle.rng.next() & 1) + 2);
1742
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1669
1743
 
1670
1744
  try log.start(battle.active(player), .Bide);
1671
1745
  }
@@ -1687,14 +1761,8 @@ pub const Effects = struct {
1687
1761
  return;
1688
1762
  }
1689
1763
 
1690
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1691
- battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
1692
- else
1693
- battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
1694
- Gen12.percent(10)
1695
- else
1696
- Gen12.percent(30));
1697
- if (!chance) return;
1764
+ if (foe.active.types.includes(move.type)) return;
1765
+ if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
1698
1766
 
1699
1767
  foe_stored.status = Status.init(.BRN);
1700
1768
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1718,11 +1786,7 @@ pub const Effects = struct {
1718
1786
  const sub = foe.active.volatiles.Substitute;
1719
1787
 
1720
1788
  if (move.effect == .ConfusionChance) {
1721
- const chance = if (showdown)
1722
- battle.rng.chance(u8, 25, 256)
1723
- else
1724
- battle.rng.next() < Gen12.percent(10);
1725
- if (!chance) return;
1789
+ if (!Rolls.confusionChance(battle)) return;
1726
1790
  } else {
1727
1791
  if (showdown) {
1728
1792
  if (!try checkHit(battle, player, move, log)) {
@@ -1741,10 +1805,7 @@ pub const Effects = struct {
1741
1805
 
1742
1806
  if (foe.active.volatiles.Confusion) return;
1743
1807
  foe.active.volatiles.Confusion = true;
1744
- foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1745
- battle.rng.range(u8, 2, 6)
1746
- else
1747
- (battle.rng.next() & 3) + 2);
1808
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1748
1809
 
1749
1810
  try log.start(battle.active(player.foe()), .Confusion);
1750
1811
  }
@@ -1794,11 +1855,8 @@ pub const Effects = struct {
1794
1855
  return Result.Error;
1795
1856
  }
1796
1857
 
1797
- volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
1798
- volatiles.disabled_duration = @intCast(u4, if (showdown)
1799
- battle.rng.range(u8, 1, 9)
1800
- else
1801
- (battle.rng.next() & 7) + 1);
1858
+ volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
+ volatiles.disabled_duration = Rolls.disableDuration(battle);
1802
1860
 
1803
1861
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1804
1862
  return null;
@@ -1810,7 +1868,7 @@ pub const Effects = struct {
1810
1868
 
1811
1869
  if (battle.last_damage == 0) {
1812
1870
  assert(showdown);
1813
- return;
1871
+ if (!battle.foe(player).active.volatiles.Substitute) return;
1814
1872
  }
1815
1873
 
1816
1874
  const drain = @max(battle.last_damage / 2, 1);
@@ -1837,15 +1895,7 @@ pub const Effects = struct {
1837
1895
  var volatiles = &battle.foe(player).active.volatiles;
1838
1896
 
1839
1897
  if (volatiles.Substitute) return;
1840
-
1841
- const chance = if (showdown)
1842
- battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
1843
- else
1844
- battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
1845
- Gen12.percent(10)
1846
- else
1847
- Gen12.percent(30));
1848
- if (!chance) return;
1898
+ if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) return;
1849
1899
 
1850
1900
  volatiles.Flinch = true;
1851
1901
  volatiles.Recharging = false;
@@ -1871,12 +1921,8 @@ pub const Effects = struct {
1871
1921
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1872
1922
  }
1873
1923
 
1874
- const chance = !foe.active.types.includes(move.type) and if (showdown)
1875
- battle.rng.chance(u8, 26, 256)
1876
- else
1877
- battle.rng.next() < 1 + Gen12.percent(10);
1878
- if (!chance) return;
1879
-
1924
+ if (foe.active.types.includes(move.type)) return;
1925
+ if (!Rolls.secondaryChance(battle, true)) return;
1880
1926
  // Freeze Clause Mod
1881
1927
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1882
1928
 
@@ -2013,15 +2059,15 @@ pub const Effects = struct {
2013
2059
  }
2014
2060
  break :has_mimic false;
2015
2061
  };
2016
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
2017
- // trigger |-miss| instead of |-fail|
2018
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2062
+ if (!has_mimic) {
2063
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2064
+ if (!try checkHit(battle, player, move, log)) return;
2019
2065
  return try log.fail(battle.active(player.foe()), .None);
2020
2066
  }
2021
2067
  }
2022
2068
  if (!try checkHit(battle, player, move, log)) return;
2023
2069
 
2024
- const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2070
+ const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2025
2071
  side.active.move(oslot).id = foe.active.move(rslot).id;
2026
2072
 
2027
2073
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2042,7 +2088,8 @@ pub const Effects = struct {
2042
2088
  assert(!side.active.volatiles.MultiHit);
2043
2089
  side.active.volatiles.MultiHit = true;
2044
2090
 
2045
- side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2091
+ side.active.volatiles.attacks =
2092
+ if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2046
2093
  }
2047
2094
 
2048
2095
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2086,14 +2133,8 @@ pub const Effects = struct {
2086
2133
  }
2087
2134
 
2088
2135
  // Body Slam can't paralyze a Normal type Pokémon
2089
- const chance = !foe.active.types.includes(move.type) and if (showdown)
2090
- battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
2091
- else
2092
- battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
2093
- Gen12.percent(10)
2094
- else
2095
- Gen12.percent(30));
2096
- if (!chance) return;
2136
+ if (foe.active.types.includes(move.type)) return;
2137
+ if (!Rolls.secondaryChance(battle, move.effect == .ParalyzeChance1)) return;
2097
2138
 
2098
2139
  foe_stored.status = Status.init(.PAR);
2099
2140
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2136,16 +2177,7 @@ pub const Effects = struct {
2136
2177
 
2137
2178
  if (move.effect == .Poison) {
2138
2179
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2139
- } else {
2140
- const chance = if (showdown)
2141
- battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
2142
- else
2143
- battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
2144
- Gen12.percent(20)
2145
- else
2146
- Gen12.percent(40));
2147
- if (!chance) return;
2148
- }
2180
+ } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) return;
2149
2181
 
2150
2182
  foe_stored.status = Status.init(.PSN);
2151
2183
  if (toxic) {
@@ -2159,10 +2191,8 @@ pub const Effects = struct {
2159
2191
 
2160
2192
  fn rage(battle: anytype, player: Player) !void {
2161
2193
  var volatiles = &battle.side(player).active.volatiles;
2162
- if (volatiles.Rage) return;
2163
- volatiles.Rage = true;
2164
2194
  assert(!volatiles.Bide);
2165
- volatiles.state = 0;
2195
+ volatiles.Rage = true;
2166
2196
  }
2167
2197
 
2168
2198
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2214,16 +2244,7 @@ pub const Effects = struct {
2214
2244
  }
2215
2245
  foe.active.volatiles.Recharging = false;
2216
2246
 
2217
- const duration = @intCast(u3, if (showdown)
2218
- battle.rng.range(u8, 1, 8)
2219
- else loop: {
2220
- while (true) {
2221
- const r = battle.rng.next() & 7;
2222
- if (r != 0) break :loop r;
2223
- }
2224
- });
2225
-
2226
- foe_stored.status = Status.slp(duration);
2247
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2227
2248
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2228
2249
  }
2229
2250
 
@@ -2276,11 +2297,7 @@ pub const Effects = struct {
2276
2297
  assert(!volatiles.Bide);
2277
2298
 
2278
2299
  volatiles.Thrashing = true;
2279
- volatiles.state = 0;
2280
- volatiles.attacks = @intCast(u3, if (showdown)
2281
- battle.rng.range(u8, 2, 4)
2282
- else
2283
- (battle.rng.next() & 1) + 2);
2300
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2284
2301
  }
2285
2302
 
2286
2303
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2312,7 +2329,7 @@ pub const Effects = struct {
2312
2329
  try log.transform(battle.active(player), foe_ident);
2313
2330
  }
2314
2331
 
2315
- fn binding(battle: anytype, player: Player) void {
2332
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2316
2333
  var side = battle.side(player);
2317
2334
  var foe = battle.foe(player);
2318
2335
 
@@ -2320,9 +2337,11 @@ pub const Effects = struct {
2320
2337
  side.active.volatiles.Binding = true;
2321
2338
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2322
2339
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2323
- if (!showdown) foe.active.volatiles.Recharging = false;
2340
+ if (!showdown) {
2341
+ foe.active.volatiles.Recharging = false;
2342
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2324
2343
 
2325
- side.active.volatiles.attacks = distribution(battle) - 1;
2344
+ side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2326
2345
  }
2327
2346
 
2328
2347
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2349,7 +2368,7 @@ pub const Effects = struct {
2349
2368
  return try log.fail(ident, .None);
2350
2369
  }
2351
2370
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2352
- const stat = unmodifiedStats(battle, player).atk;
2371
+ const stat = unmodifiedStats(battle, side).atk;
2353
2372
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2354
2373
  try log.boost(ident, reason, n);
2355
2374
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2369,7 +2388,7 @@ pub const Effects = struct {
2369
2388
  return try log.fail(ident, .None);
2370
2389
  }
2371
2390
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2372
- const stat = unmodifiedStats(battle, player).def;
2391
+ const stat = unmodifiedStats(battle, side).def;
2373
2392
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2374
2393
  try log.boost(ident, .Defense, n);
2375
2394
  },
@@ -2386,7 +2405,7 @@ pub const Effects = struct {
2386
2405
  return try log.fail(ident, .None);
2387
2406
  }
2388
2407
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2389
- const stat = unmodifiedStats(battle, player).spe;
2408
+ const stat = unmodifiedStats(battle, side).spe;
2390
2409
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2391
2410
  try log.boost(ident, .Speed, 2);
2392
2411
  },
@@ -2406,7 +2425,7 @@ pub const Effects = struct {
2406
2425
  return try log.fail(ident, .None);
2407
2426
  }
2408
2427
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2409
- const stat = unmodifiedStats(battle, player).spc;
2428
+ const stat = unmodifiedStats(battle, side).spc;
2410
2429
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2411
2430
  try log.boost(ident, .SpecialAttack, n);
2412
2431
  try log.boost(ident, .SpecialDefense, n);
@@ -2433,11 +2452,7 @@ pub const Effects = struct {
2433
2452
  }
2434
2453
 
2435
2454
  if (move.effect.isStatDownChance()) {
2436
- const chance = if (showdown)
2437
- battle.rng.chance(u8, 85, 256)
2438
- else
2439
- battle.rng.next() < Gen12.percent(33) + 1;
2440
- if (!chance or foe.active.volatiles.Invulnerable) return;
2455
+ if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
2441
2456
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2442
2457
  return; // checkHit already checks for Invulnerable
2443
2458
  }
@@ -2459,7 +2474,7 @@ pub const Effects = struct {
2459
2474
  return try log.fail(foe_ident, .None);
2460
2475
  }
2461
2476
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2462
- const stat = unmodifiedStats(battle, player.foe()).atk;
2477
+ const stat = unmodifiedStats(battle, foe).atk;
2463
2478
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2464
2479
  try log.boost(foe_ident, .Attack, -1);
2465
2480
  },
@@ -2477,7 +2492,7 @@ pub const Effects = struct {
2477
2492
  return try log.fail(foe_ident, .None);
2478
2493
  }
2479
2494
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2480
- const stat = unmodifiedStats(battle, player.foe()).def;
2495
+ const stat = unmodifiedStats(battle, foe).def;
2481
2496
  stats.def = @max(1, stat * mod[0] / mod[1]);
2482
2497
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2483
2498
  },
@@ -2494,7 +2509,7 @@ pub const Effects = struct {
2494
2509
  return try log.fail(foe_ident, .None);
2495
2510
  }
2496
2511
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2497
- const stat = unmodifiedStats(battle, player.foe()).spe;
2512
+ const stat = unmodifiedStats(battle, foe).spe;
2498
2513
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2499
2514
  try log.boost(foe_ident, .Speed, -1);
2500
2515
  assert(boosts.spe >= -6);
@@ -2514,7 +2529,7 @@ pub const Effects = struct {
2514
2529
  return try log.fail(foe_ident, .None);
2515
2530
  }
2516
2531
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2517
- const stat = unmodifiedStats(battle, player.foe()).spc;
2532
+ const stat = unmodifiedStats(battle, foe).spc;
2518
2533
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2519
2534
  try log.boost(foe_ident, .SpecialAttack, -1);
2520
2535
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2533,8 +2548,7 @@ pub const Effects = struct {
2533
2548
  }
2534
2549
  };
2535
2550
 
2536
- fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2537
- const side = battle.side(who);
2551
+ fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2538
2552
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2539
2553
  const id = ID.from(side.active.volatiles.transform);
2540
2554
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2600,80 +2614,140 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2600
2614
  }
2601
2615
  }
2602
2616
 
2603
- fn disabled(side: *Side, ident: ID, log: anytype) !bool {
2604
- if (side.active.volatiles.disabled_move != 0) {
2605
- // A Pokémon that transforms after being disabled may end up with less move slots
2606
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
2607
- // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
2608
- const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
2609
- m
2610
- else
2611
- side.last_selected_move;
2612
- if (m != .None and m == last) {
2613
- side.active.volatiles.Charging = false;
2614
- try log.disabled(ident, last);
2615
- return true;
2617
+ pub const Rolls = struct {
2618
+ inline fn speedTie(battle: anytype) bool {
2619
+ if (showdown) return battle.rng.range(u8, 0, 2) == 0;
2620
+ return battle.rng.next() < Gen12.percent(50) + 1;
2621
+ }
2622
+
2623
+ inline fn criticalHit(battle: anytype, chance: u16) bool {
2624
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
2625
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
2626
+ }
2627
+
2628
+ inline fn damage(battle: anytype) u8 {
2629
+ if (showdown) return battle.rng.range(u8, 217, 256);
2630
+ while (true) {
2631
+ const r = std.math.rotr(u8, battle.rng.next(), 1);
2632
+ if (r >= 217) return r;
2616
2633
  }
2617
2634
  }
2618
- return false;
2619
- }
2620
2635
 
2621
- inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
2622
- const side = battle.side(who);
2623
- if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
2624
- try Effects.boost(battle, who, Move.get(.Rage), log);
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;
2625
2639
  }
2626
- }
2627
2640
 
2628
- fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2629
- var volatiles = &active.volatiles;
2630
- assert(volatiles.Thrashing);
2631
- if (volatiles.attacks > 0) return false;
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
+ }
2632
2645
 
2633
- volatiles.Thrashing = false;
2634
- volatiles.Confusion = true;
2635
- volatiles.confusion = @intCast(u3, if (showdown)
2636
- battle.rng.range(u8, 2, 6)
2637
- else
2638
- (battle.rng.next() & 3) + 2);
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
+ }
2639
2650
 
2640
- return true;
2641
- }
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
+ }
2642
2655
 
2643
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
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
+ }
2644
2660
 
2645
- fn distribution(battle: anytype) u3 {
2646
- if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
2647
- const r = (battle.rng.next() & 3);
2648
- return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
2649
- }
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
+ }
2650
2665
 
2651
- fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2652
- if (showdown) {
2653
- if (check_pp == 0) {
2654
- var i: usize = moves.len;
2655
- while (i > 0) {
2656
- i -= 1;
2657
- if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
2658
- }
2659
- } else {
2660
- var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
2661
- var i: usize = 0;
2662
- while (i < moves.len and r > 0) : (i += 1) {
2663
- if (moves[i].pp > 0) {
2664
- r -= 1;
2665
- if (r == 0) break;
2666
+ inline fn poisonChance(battle: anytype, low: bool) bool {
2667
+ if (showdown) return battle.rng.chance(u8, @as(u8, if (low) 52 else 103), 256);
2668
+ return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2669
+ }
2670
+
2671
+ inline fn sleepDuration(battle: anytype) u3 {
2672
+ if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2673
+ while (true) {
2674
+ const r = battle.rng.next() & 7;
2675
+ if (r != 0) return @intCast(u3, r);
2676
+ }
2677
+ }
2678
+
2679
+ inline fn bideThrashDuration(battle: anytype) u3 {
2680
+ if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2681
+ return @intCast(u3, (battle.rng.next() & 1) + 2);
2682
+ }
2683
+
2684
+ fn moveSlot(battle: anytype, moves: []MoveSlot, check_pp: u4) u4 {
2685
+ if (showdown) {
2686
+ if (check_pp == 0) {
2687
+ var i: usize = moves.len;
2688
+ while (i > 0) {
2689
+ i -= 1;
2690
+ if (moves[i].id != .None) {
2691
+ return battle.rng.range(u4, 0, @intCast(u4, i + 1)) + 1;
2692
+ }
2693
+ }
2694
+ } else {
2695
+ var r = battle.rng.range(u4, 0, @intCast(u4, check_pp)) + 1;
2696
+ var i: usize = 0;
2697
+ while (i < moves.len and r > 0) : (i += 1) {
2698
+ if (moves[i].pp > 0) {
2699
+ r -= 1;
2700
+ if (r == 0) break;
2701
+ }
2666
2702
  }
2703
+ return @intCast(u4, i + 1);
2667
2704
  }
2668
- return @intCast(u4, i + 1);
2705
+ }
2706
+
2707
+ while (true) {
2708
+ const r = @intCast(u4, battle.rng.next() & 3);
2709
+ if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2669
2710
  }
2670
2711
  }
2671
2712
 
2672
- while (true) {
2673
- const r = @intCast(u4, rand.next() & 3);
2674
- if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
2713
+ inline fn disableDuration(battle: anytype) u4 {
2714
+ if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
2715
+ return @intCast(u4, (battle.rng.next() & 7) + 1);
2675
2716
  }
2676
- }
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
+ };
2677
2751
 
2678
2752
  test "RNG agreement" {
2679
2753
  if (!showdown) return;