@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.fb5bf955

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;
@@ -379,16 +349,7 @@ fn doTurn(
379
349
 
380
350
  residual = true;
381
351
  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;
352
+ if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
392
353
  if (!replace) {
393
354
  if (foe_choice.type != .Switch) {
394
355
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -411,8 +372,6 @@ fn executeMove(
411
372
  battle: anytype,
412
373
  player: Player,
413
374
  choice: Choice,
414
- rewrap: bool,
415
- skip: bool,
416
375
  from: ?Move,
417
376
  residual: *bool,
418
377
  log: anytype,
@@ -424,10 +383,9 @@ fn executeMove(
424
383
  return null;
425
384
  }
426
385
 
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
386
  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
431
389
  assert(!showdown);
432
390
  if (battle.foe(player).active.volatiles.Binding) {
433
391
  try log.cant(battle.active(player), .Bound);
@@ -446,9 +404,9 @@ fn executeMove(
446
404
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
447
405
  assert(!showdown);
448
406
  mslot = @intCast(u4, if (player == .P1)
449
- battle.last_moves.p1_index
407
+ battle.last_selected_indexes.p1
450
408
  else
451
- battle.last_moves.p2_index);
409
+ battle.last_selected_indexes.p2);
452
410
  const stored = side.stored();
453
411
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
454
412
  auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
@@ -470,14 +428,14 @@ fn executeMove(
470
428
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
471
429
  assert(mslot == 1);
472
430
  mslot = @intCast(u4, if (player == .P1)
473
- battle.last_moves.p1_index
431
+ battle.last_selected_indexes.p1
474
432
  else
475
- battle.last_moves.p2_index);
433
+ battle.last_selected_indexes.p2);
476
434
  }
477
435
 
478
436
  var skip_can = false;
479
437
  var skip_pp = false;
480
- switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
+ switch (try beforeMove(battle, player, from, residual, log)) {
481
439
  .done => return null,
482
440
  .skip_can => skip_can = true,
483
441
  .skip_pp => skip_pp = true,
@@ -489,15 +447,14 @@ fn executeMove(
489
447
  return null;
490
448
  }
491
449
 
492
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
+ return doMove(battle, player, mslot, auto, residual, log);
493
451
  }
494
452
 
495
- const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
+ const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
496
454
 
497
455
  fn beforeMove(
498
456
  battle: anytype,
499
457
  player: Player,
500
- skip: bool,
501
458
  from: ?Move,
502
459
  residual: *bool,
503
460
  log: anytype,
@@ -529,15 +486,13 @@ fn beforeMove(
529
486
  return .done;
530
487
  }
531
488
 
532
- if (skip or foe.active.volatiles.Binding) {
489
+ if (foe.active.volatiles.Binding) {
533
490
  try log.cant(ident, .Bound);
534
491
  return .done;
535
492
  }
536
493
 
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;
494
+ if (!showdown and volatiles.Flinch) {
495
+ volatiles.Flinch = false;
541
496
  try log.cant(ident, .Flinch);
542
497
  return .done;
543
498
  }
@@ -558,6 +513,13 @@ fn beforeMove(
558
513
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
559
514
  if (showdown and try disabled(side, ident, log)) return .done;
560
515
 
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
+
561
523
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
562
524
  // before the side had a selected a move - we simply need to assume this leads to a desync
563
525
  if (side.last_selected_move == .None) {
@@ -575,7 +537,12 @@ fn beforeMove(
575
537
  } else {
576
538
  try log.activate(ident, .Confusion);
577
539
 
578
- if (Rolls.confused(battle)) {
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) {
579
546
  assert(!volatiles.MultiHit);
580
547
  if (!volatiles.Rage) volatiles.state = 0;
581
548
  volatiles.Bide = false;
@@ -601,12 +568,8 @@ fn beforeMove(
601
568
  player
602
569
  else
603
570
  player.foe();
604
-
605
- const uncapped = battle.last_damage;
606
571
  // Skipping adjustDamage / randomizeDamage / checkHit
607
572
  _ = 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
573
 
611
574
  return .done;
612
575
  }
@@ -615,28 +578,28 @@ fn beforeMove(
615
578
 
616
579
  if (!showdown and try disabled(side, ident, log)) return .done;
617
580
 
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;
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
+ }
627
597
  }
628
598
 
629
599
  if (volatiles.Bide) {
630
600
  assert(!volatiles.Thrashing and !volatiles.Rage);
631
601
 
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
- }
602
+ volatiles.state +%= battle.last_damage;
640
603
 
641
604
  assert(volatiles.attacks > 0);
642
605
 
@@ -680,13 +643,8 @@ fn beforeMove(
680
643
  try log.start(battle.active(player), .ConfusionSilent);
681
644
  }
682
645
  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
- }
646
+ if (showdown and handleThrashing(battle, active)) {
647
+ try log.start(battle.active(player), .ConfusionSilent);
690
648
  }
691
649
  return .skip_can;
692
650
  }
@@ -701,11 +659,7 @@ fn beforeMove(
701
659
  side.last_selected_move,
702
660
  );
703
661
  if (showdown or battle.last_damage != 0) {
704
- const sub = showdown and foe.active.volatiles.Substitute;
705
662
  _ = 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
663
  }
710
664
  return .done;
711
665
  }
@@ -737,7 +691,6 @@ fn canMove(
737
691
  side.active.volatiles.Invulnerable = false;
738
692
  } else if (move.effect == .Charge) {
739
693
  try log.move(player_ident, side.last_selected_move, .{}, from);
740
- setCounterable(battle, player, side, move);
741
694
  try Effects.charge(battle, player, log);
742
695
  return false;
743
696
  }
@@ -754,7 +707,6 @@ fn canMove(
754
707
  // regrettable, but this information is more "nice to have" than required
755
708
  const f = if (from != null and from.? == .None) null else from;
756
709
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
757
- setCounterable(battle, player, side, move);
758
710
 
759
711
  if (move.effect.onBegin()) {
760
712
  try onBegin(battle, player, move, mslot, residual, log);
@@ -765,27 +717,12 @@ fn canMove(
765
717
  Effects.thrashing(battle, player);
766
718
  } else if (!showdown and move.effect == .Binding) {
767
719
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
768
- Effects.binding(battle, player, false);
720
+ Effects.binding(battle, player);
769
721
  }
770
722
 
771
723
  return true;
772
724
  }
773
725
 
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
726
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
790
727
  if (side.last_selected_move == .Struggle) return;
791
728
 
@@ -821,7 +758,6 @@ fn doMove(
821
758
  battle: anytype,
822
759
  player: Player,
823
760
  mslot: u4,
824
- rewrap: bool,
825
761
  auto: bool,
826
762
  residual: *bool,
827
763
  log: anytype,
@@ -860,7 +796,7 @@ fn doMove(
860
796
  };
861
797
  assert(!immune or miss or (showdown and move.effect == .Binding));
862
798
 
863
- var late = showdown and move.effect != .Explode;
799
+ var late = showdown;
864
800
  const skip = status or immune;
865
801
  if ((!showdown or (!skip or counter)) and !miss) blk: {
866
802
  if (showdown and move.effect.isMulti()) {
@@ -913,9 +849,9 @@ fn doMove(
913
849
 
914
850
  if (!showdown or !miss) {
915
851
  if (move.effect == .MirrorMove) {
916
- return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
852
+ return mirrorMove(battle, player, mslot, auto, residual, log);
917
853
  } else if (move.effect == .Metronome) {
918
- return metronome(battle, player, mslot, rewrap, auto, residual, log);
854
+ return metronome(battle, player, mslot, auto, residual, log);
919
855
  } else if (move.effect.onEnd()) {
920
856
  try onEnd(battle, player, move, log);
921
857
  return null;
@@ -930,7 +866,7 @@ fn doMove(
930
866
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
931
867
  if (ohko) {
932
868
  try log.immune(foe_ident, .OHKO);
933
- } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
869
+ } else if (immune and !invulnerable) {
934
870
  try log.immune(foe_ident, .None);
935
871
  } else if (mist) {
936
872
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -980,7 +916,7 @@ fn doMove(
980
916
  if (hit == 0 and ohko) try log.ohko();
981
917
  hit += 1;
982
918
  if (foe.stored().hp == 0) break;
983
- if (!late and (!sub or move.effect == .Explode)) try buildRage(battle, player.foe(), log);
919
+ if (!late and !sub) try buildRage(battle, player.foe(), log);
984
920
  // If the substitute breaks during a multi-hit attack, the attack ends
985
921
  if (nullified) break;
986
922
  }
@@ -996,17 +932,11 @@ fn doMove(
996
932
  } else if (showdown) {
997
933
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
998
934
  if (move.effect == .Binding) {
999
- Effects.binding(battle, player, rewrap);
935
+ Effects.binding(battle, player);
1000
936
  if (immune) {
1001
937
  battle.last_damage = 0;
1002
- assert(foe.stored().hp > 0);
1003
938
  // 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
- }
939
+ try log.damage(battle.active(player.foe()), foe.stored(), .None);
1010
940
  return null;
1011
941
  }
1012
942
  }
@@ -1057,7 +987,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
1057
987
  chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
1058
988
  chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
1059
989
 
1060
- return Rolls.criticalHit(battle, chance);
990
+ if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
991
+ return std.math.rotl(u8, battle.rng.next(), 3) < chance;
1061
992
  }
1062
993
 
1063
994
  fn calcDamage(
@@ -1161,7 +1092,16 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
1161
1092
 
1162
1093
  fn randomizeDamage(battle: anytype) void {
1163
1094
  if (battle.last_damage <= 1) return;
1164
- const random = Rolls.damage(battle);
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
+
1165
1105
  battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
1166
1106
  }
1167
1107
 
@@ -1179,9 +1119,16 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1179
1119
  // GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
1180
1120
  .Psywave => power: {
1181
1121
  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);
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
+ }
1185
1132
  },
1186
1133
  else => unreachable,
1187
1134
  };
@@ -1203,17 +1150,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1203
1150
  return null;
1204
1151
  }
1205
1152
 
1206
- // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
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);
1207
1156
  const foe_last_selected_move =
1208
1157
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1209
- .Splash
1158
+ .Counter
1210
1159
  else
1211
1160
  foe.last_selected_move);
1212
1161
 
1213
- const used = (if (player.foe() == .P1)
1214
- battle.last_moves.p1_counterable
1215
- else
1216
- battle.last_moves.p2_counterable) != 0;
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);
1217
1166
 
1218
1167
  const selected = foe_last_selected_move.bp > 0 and
1219
1168
  foe.last_selected_move != .Counter and
@@ -1232,14 +1181,12 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1232
1181
  return null;
1233
1182
  }
1234
1183
 
1235
- battle.last_damage *|= 2;
1184
+ battle.last_damage = if (battle.last_damage > 0x7FFF) 0xFFFF else battle.last_damage * 2;
1236
1185
 
1237
1186
  // Pokémon Showdown calls checkHit before Counter
1238
1187
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1239
1188
 
1240
- const sub = showdown and foe.active.volatiles.Substitute;
1241
1189
  _ = 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
1190
  return null;
1244
1191
  }
1245
1192
 
@@ -1283,7 +1230,6 @@ fn mirrorMove(
1283
1230
  battle: anytype,
1284
1231
  player: Player,
1285
1232
  mslot: u4,
1286
- rewrap: bool,
1287
1233
  auto: bool,
1288
1234
  residual: *bool,
1289
1235
  log: anytype,
@@ -1296,30 +1242,41 @@ fn mirrorMove(
1296
1242
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1297
1243
  try log.fail(battle.active(player), .None);
1298
1244
  return null;
1299
- } else if (!showdown or foe.last_used_move != .Struggle) {
1300
- incrementPP(side, mslot);
1301
1245
  }
1302
1246
 
1247
+ incrementPP(side, mslot);
1248
+
1303
1249
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1304
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1250
+ return doMove(battle, player, mslot, auto, residual, log);
1305
1251
  }
1306
1252
 
1307
1253
  fn metronome(
1308
1254
  battle: anytype,
1309
1255
  player: Player,
1310
1256
  mslot: u4,
1311
- rewrap: bool,
1312
1257
  auto: bool,
1313
1258
  residual: *bool,
1314
1259
  log: anytype,
1315
1260
  ) !?Result {
1316
1261
  var side = battle.side(player);
1317
1262
 
1318
- side.last_selected_move = Rolls.metronome(battle);
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
+
1319
1276
  incrementPP(side, mslot);
1320
1277
 
1321
1278
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1322
- return doMove(battle, player, mslot, rewrap, auto, residual, log);
1279
+ return doMove(battle, player, mslot, auto, residual, log);
1323
1280
  }
1324
1281
 
1325
1282
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1373,24 +1330,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1373
1330
  }
1374
1331
 
1375
1332
  // 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));
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));
1382
1337
  var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
1383
1338
  accuracy = accuracy * boost[0] / boost[1];
1384
1339
  boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
1385
1340
  accuracy = accuracy * boost[0] / boost[1];
1386
1341
  accuracy = @min(255, @max(1, accuracy));
1387
1342
 
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;
1343
+ if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
1391
1344
 
1392
1345
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1393
- break :miss !Rolls.hit(battle, accuracy);
1346
+ break :miss if (showdown)
1347
+ !battle.rng.chance(u8, @intCast(u8, accuracy), 256)
1348
+ else
1349
+ battle.rng.next() >= accuracy;
1394
1350
  };
1395
1351
 
1396
1352
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1522,6 +1478,9 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1522
1478
  damage *= volatiles.toxic;
1523
1479
  }
1524
1480
 
1481
+ // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1482
+ if (showdown) battle.last_damage = damage;
1483
+
1525
1484
  const amount = @min(damage, stored.hp);
1526
1485
  stored.hp -= amount;
1527
1486
 
@@ -1540,13 +1499,13 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1540
1499
  assert(!battle.side(.P1).active.volatiles.MultiHit);
1541
1500
  assert(!battle.side(.P2).active.volatiles.MultiHit);
1542
1501
 
1543
- battle.turn += 1;
1544
-
1545
1502
  if (showdown and options.ebc and checkEBC(battle)) {
1546
1503
  try log.tie();
1547
1504
  return Result.Tie;
1548
1505
  }
1549
1506
 
1507
+ battle.turn += 1;
1508
+
1550
1509
  if (showdown and battle.turn >= 1000) {
1551
1510
  try log.tie();
1552
1511
  return Result.Tie;
@@ -1613,42 +1572,6 @@ fn checkEBC(battle: anytype) bool {
1613
1572
  return false;
1614
1573
  }
1615
1574
 
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
1575
  inline fn onBegin(
1653
1576
  battle: anytype,
1654
1577
  player: Player,
@@ -1739,7 +1662,10 @@ pub const Effects = struct {
1739
1662
  side.active.volatiles.Bide = true;
1740
1663
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1741
1664
  side.active.volatiles.state = 0;
1742
- side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1665
+ side.active.volatiles.attacks = @intCast(u3, if (showdown)
1666
+ battle.rng.range(u4, 2, 4)
1667
+ else
1668
+ (battle.rng.next() & 1) + 2);
1743
1669
 
1744
1670
  try log.start(battle.active(player), .Bide);
1745
1671
  }
@@ -1761,8 +1687,14 @@ pub const Effects = struct {
1761
1687
  return;
1762
1688
  }
1763
1689
 
1764
- if (foe.active.types.includes(move.type)) return;
1765
- if (!Rolls.secondaryChance(battle, move.effect == .BurnChance1)) return;
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;
1766
1698
 
1767
1699
  foe_stored.status = Status.init(.BRN);
1768
1700
  foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
@@ -1786,7 +1718,11 @@ pub const Effects = struct {
1786
1718
  const sub = foe.active.volatiles.Substitute;
1787
1719
 
1788
1720
  if (move.effect == .ConfusionChance) {
1789
- if (!Rolls.confusionChance(battle)) return;
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;
1790
1726
  } else {
1791
1727
  if (showdown) {
1792
1728
  if (!try checkHit(battle, player, move, log)) {
@@ -1805,7 +1741,10 @@ pub const Effects = struct {
1805
1741
 
1806
1742
  if (foe.active.volatiles.Confusion) return;
1807
1743
  foe.active.volatiles.Confusion = true;
1808
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle);
1744
+ foe.active.volatiles.confusion = @intCast(u3, if (showdown)
1745
+ battle.rng.range(u8, 2, 6)
1746
+ else
1747
+ (battle.rng.next() & 3) + 2);
1809
1748
 
1810
1749
  try log.start(battle.active(player.foe()), .Confusion);
1811
1750
  }
@@ -1855,8 +1794,11 @@ pub const Effects = struct {
1855
1794
  return Result.Error;
1856
1795
  }
1857
1796
 
1858
- volatiles.disabled_move = @intCast(u3, Rolls.moveSlot(battle, &foe.active.moves, n));
1859
- volatiles.disabled_duration = Rolls.disableDuration(battle);
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);
1860
1802
 
1861
1803
  try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
1862
1804
  return null;
@@ -1868,7 +1810,7 @@ pub const Effects = struct {
1868
1810
 
1869
1811
  if (battle.last_damage == 0) {
1870
1812
  assert(showdown);
1871
- if (!battle.foe(player).active.volatiles.Substitute) return;
1813
+ return;
1872
1814
  }
1873
1815
 
1874
1816
  const drain = @max(battle.last_damage / 2, 1);
@@ -1895,7 +1837,15 @@ pub const Effects = struct {
1895
1837
  var volatiles = &battle.foe(player).active.volatiles;
1896
1838
 
1897
1839
  if (volatiles.Substitute) return;
1898
- if (!Rolls.secondaryChance(battle, move.effect == .FlinchChance1)) 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;
1899
1849
 
1900
1850
  volatiles.Flinch = true;
1901
1851
  volatiles.Recharging = false;
@@ -1921,8 +1871,12 @@ pub const Effects = struct {
1921
1871
  return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
1922
1872
  }
1923
1873
 
1924
- if (foe.active.types.includes(move.type)) return;
1925
- if (!Rolls.secondaryChance(battle, true)) return;
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
+
1926
1880
  // Freeze Clause Mod
1927
1881
  if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
1928
1882
 
@@ -2059,15 +2013,15 @@ pub const Effects = struct {
2059
2013
  }
2060
2014
  break :has_mimic false;
2061
2015
  };
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;
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) {
2065
2019
  return try log.fail(battle.active(player.foe()), .None);
2066
2020
  }
2067
2021
  }
2068
2022
  if (!try checkHit(battle, player, move, log)) return;
2069
2023
 
2070
- const rslot = Rolls.moveSlot(battle, &foe.active.moves, 0);
2024
+ const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
2071
2025
  side.active.move(oslot).id = foe.active.move(rslot).id;
2072
2026
 
2073
2027
  try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
@@ -2088,8 +2042,7 @@ pub const Effects = struct {
2088
2042
  assert(!side.active.volatiles.MultiHit);
2089
2043
  side.active.volatiles.MultiHit = true;
2090
2044
 
2091
- side.active.volatiles.attacks =
2092
- if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
2045
+ side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
2093
2046
  }
2094
2047
 
2095
2048
  fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2133,8 +2086,14 @@ pub const Effects = struct {
2133
2086
  }
2134
2087
 
2135
2088
  // 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;
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;
2138
2097
 
2139
2098
  foe_stored.status = Status.init(.PAR);
2140
2099
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2177,7 +2136,16 @@ pub const Effects = struct {
2177
2136
 
2178
2137
  if (move.effect == .Poison) {
2179
2138
  if (!showdown and !try checkHit(battle, player, move, log)) return;
2180
- } else if (!Rolls.poisonChance(battle, move.effect == .PoisonChance1)) 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
+ }
2181
2149
 
2182
2150
  foe_stored.status = Status.init(.PSN);
2183
2151
  if (toxic) {
@@ -2191,8 +2159,10 @@ pub const Effects = struct {
2191
2159
 
2192
2160
  fn rage(battle: anytype, player: Player) !void {
2193
2161
  var volatiles = &battle.side(player).active.volatiles;
2194
- assert(!volatiles.Bide);
2162
+ if (volatiles.Rage) return;
2195
2163
  volatiles.Rage = true;
2164
+ assert(!volatiles.Bide);
2165
+ volatiles.state = 0;
2196
2166
  }
2197
2167
 
2198
2168
  fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
@@ -2244,7 +2214,16 @@ pub const Effects = struct {
2244
2214
  }
2245
2215
  foe.active.volatiles.Recharging = false;
2246
2216
 
2247
- foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
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);
2248
2227
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2249
2228
  }
2250
2229
 
@@ -2297,7 +2276,11 @@ pub const Effects = struct {
2297
2276
  assert(!volatiles.Bide);
2298
2277
 
2299
2278
  volatiles.Thrashing = true;
2300
- volatiles.attacks = Rolls.bideThrashDuration(battle);
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);
2301
2284
  }
2302
2285
 
2303
2286
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2329,7 +2312,7 @@ pub const Effects = struct {
2329
2312
  try log.transform(battle.active(player), foe_ident);
2330
2313
  }
2331
2314
 
2332
- fn binding(battle: anytype, player: Player, rewrap: bool) void {
2315
+ fn binding(battle: anytype, player: Player) void {
2333
2316
  var side = battle.side(player);
2334
2317
  var foe = battle.foe(player);
2335
2318
 
@@ -2337,11 +2320,9 @@ pub const Effects = struct {
2337
2320
  side.active.volatiles.Binding = true;
2338
2321
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2339
2322
  // (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);
2323
+ if (!showdown) foe.active.volatiles.Recharging = false;
2343
2324
 
2344
- side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2325
+ side.active.volatiles.attacks = distribution(battle) - 1;
2345
2326
  }
2346
2327
 
2347
2328
  fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2368,7 +2349,7 @@ pub const Effects = struct {
2368
2349
  return try log.fail(ident, .None);
2369
2350
  }
2370
2351
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2371
- const stat = unmodifiedStats(battle, side).atk;
2352
+ const stat = unmodifiedStats(battle, player).atk;
2372
2353
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2373
2354
  try log.boost(ident, reason, n);
2374
2355
  // Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
@@ -2388,7 +2369,7 @@ pub const Effects = struct {
2388
2369
  return try log.fail(ident, .None);
2389
2370
  }
2390
2371
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2391
- const stat = unmodifiedStats(battle, side).def;
2372
+ const stat = unmodifiedStats(battle, player).def;
2392
2373
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2393
2374
  try log.boost(ident, .Defense, n);
2394
2375
  },
@@ -2405,7 +2386,7 @@ pub const Effects = struct {
2405
2386
  return try log.fail(ident, .None);
2406
2387
  }
2407
2388
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2408
- const stat = unmodifiedStats(battle, side).spe;
2389
+ const stat = unmodifiedStats(battle, player).spe;
2409
2390
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2410
2391
  try log.boost(ident, .Speed, 2);
2411
2392
  },
@@ -2425,7 +2406,7 @@ pub const Effects = struct {
2425
2406
  return try log.fail(ident, .None);
2426
2407
  }
2427
2408
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2428
- const stat = unmodifiedStats(battle, side).spc;
2409
+ const stat = unmodifiedStats(battle, player).spc;
2429
2410
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2430
2411
  try log.boost(ident, .SpecialAttack, n);
2431
2412
  try log.boost(ident, .SpecialDefense, n);
@@ -2452,7 +2433,11 @@ pub const Effects = struct {
2452
2433
  }
2453
2434
 
2454
2435
  if (move.effect.isStatDownChance()) {
2455
- if (!Rolls.unboost(battle) or foe.active.volatiles.Invulnerable) return;
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;
2456
2441
  } else if (!showdown and !try checkHit(battle, player, move, log)) {
2457
2442
  return; // checkHit already checks for Invulnerable
2458
2443
  }
@@ -2474,7 +2459,7 @@ pub const Effects = struct {
2474
2459
  return try log.fail(foe_ident, .None);
2475
2460
  }
2476
2461
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
2477
- const stat = unmodifiedStats(battle, foe).atk;
2462
+ const stat = unmodifiedStats(battle, player.foe()).atk;
2478
2463
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2479
2464
  try log.boost(foe_ident, .Attack, -1);
2480
2465
  },
@@ -2492,7 +2477,7 @@ pub const Effects = struct {
2492
2477
  return try log.fail(foe_ident, .None);
2493
2478
  }
2494
2479
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
2495
- const stat = unmodifiedStats(battle, foe).def;
2480
+ const stat = unmodifiedStats(battle, player.foe()).def;
2496
2481
  stats.def = @max(1, stat * mod[0] / mod[1]);
2497
2482
  try log.boost(foe_ident, .Defense, -@as(i8, n));
2498
2483
  },
@@ -2509,7 +2494,7 @@ pub const Effects = struct {
2509
2494
  return try log.fail(foe_ident, .None);
2510
2495
  }
2511
2496
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
2512
- const stat = unmodifiedStats(battle, foe).spe;
2497
+ const stat = unmodifiedStats(battle, player.foe()).spe;
2513
2498
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2514
2499
  try log.boost(foe_ident, .Speed, -1);
2515
2500
  assert(boosts.spe >= -6);
@@ -2529,7 +2514,7 @@ pub const Effects = struct {
2529
2514
  return try log.fail(foe_ident, .None);
2530
2515
  }
2531
2516
  var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
2532
- const stat = unmodifiedStats(battle, foe).spc;
2517
+ const stat = unmodifiedStats(battle, player.foe()).spc;
2533
2518
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2534
2519
  try log.boost(foe_ident, .SpecialAttack, -1);
2535
2520
  try log.boost(foe_ident, .SpecialDefense, -1);
@@ -2548,7 +2533,8 @@ pub const Effects = struct {
2548
2533
  }
2549
2534
  };
2550
2535
 
2551
- fn unmodifiedStats(battle: anytype, side: *Side) *Stats(u16) {
2536
+ fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
2537
+ const side = battle.side(who);
2552
2538
  if (!side.active.volatiles.Transform) return &side.stored().stats;
2553
2539
  const id = ID.from(side.active.volatiles.transform);
2554
2540
  return &battle.side(id.player).pokemon[id.id - 1].stats;
@@ -2614,140 +2600,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2614
2600
  }
2615
2601
  }
2616
2602
 
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;
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;
2633
2616
  }
2634
2617
  }
2618
+ return false;
2619
+ }
2635
2620
 
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);
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);
2654
2625
  }
2626
+ }
2655
2627
 
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
- }
2628
+ fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
2629
+ var volatiles = &active.volatiles;
2630
+ assert(volatiles.Thrashing);
2631
+ if (volatiles.attacks > 0) return false;
2660
2632
 
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
- }
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);
2665
2639
 
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
- }
2640
+ return true;
2641
+ }
2670
2642
 
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
- }
2643
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2678
2644
 
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
- }
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
+ }
2683
2650
 
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
- }
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;
2702
2666
  }
2703
- return @intCast(u4, i + 1);
2704
2667
  }
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;
2668
+ return @intCast(u4, i + 1);
2710
2669
  }
2711
2670
  }
2712
2671
 
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);
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;
2716
2675
  }
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
- };
2676
+ }
2751
2677
 
2752
2678
  test "RNG agreement" {
2753
2679
  if (!showdown) return;